AST的簡單實踐

本文首發於 hzzly的博客javascript

原文連接:AST的簡單實踐前端

什麼是AST(抽象語法樹)?

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.vue

AST是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。java

AST是一個很是基礎可是同時很是重要的知識點,咱們熟知的 TypeScript、babel、webpack、vue-cli 都是依賴 AST 進行開發的。node

這裏咱們就以 babel 爲例來實踐一下 AST。webpack

Babel運行原理

Babel 做爲當今最爲經常使用的 JavaScript 編譯器,在前端開發中扮演着極爲重要的角色。大多數狀況下,Babel 被用來轉譯 ECMAScript 2015+ 至可兼容瀏覽器的版本。web

Babel 的三個主要處理步驟分別是:vue-cli

  • 解析(parse)
  • 轉換(transform)
  • 生成(generate)

Babel處理步驟

整個過程當中,parsing和generation是固定不變的,最關鍵的是transforming步驟,經過babel插件來支持,這是其擴展性的關鍵。npm

這三個階段分別由 @babel/parser、@babel/core、@babel/generator 執行。Babel 本質上只是一個代碼的搬運工,若是不給 Babel 裝上插件,它將會把輸入的代碼原封不動地輸出。正是由於有插件的存在, Babel 才能將輸入的代碼進行轉變,從而生成新的代碼。編程

解析

輸入JS源碼,輸出AST

parsing(解析),對應於編譯器的詞法分析,及語法分析階段。輸入的源碼字符序列通過詞法分析,生成具備詞法意義的token序列(可以區分出關鍵字、數值、標點符號等),接着通過語法分析,生成具備語法意義的AST(可以區分出語句塊、註釋、變量聲明、函數參數等)。

利用 @babel/parser 對源代碼進行解析 獲得 AST。

慄如:

console.log(info)
複製代碼

通過parsing後,生成的AST以下:

{
  "type": "CallExpression",
  "callee": {
    "type": "MemberExpression",
    "object": {
      "type": "Identifier",
      "loc": {
        "identifierName": "console",
      },
      "name": "console",
    },
    "property": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "log",
    }
  },
  "arguments": [
    "Identifier": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "info",
    }
  ]
}
複製代碼

🔥Tip: JS代碼對應的AST結構能夠經過AST Explorer工具查看

仔細的小夥伴可能就會發現從咱們的源代碼到AST的過程其實就是一個分詞的過程,將咱們的 console.log(info) 分紅 console、log、info。

有了這個 AST 樹結構,咱們就能進行語義層面轉換了。

轉換

輸入AST,輸出修改過的AST

利用 @babel/traverse 對 AST 進行遍歷,並解析出整個樹的 path,經過掛載的 metadataVisitor 讀取對應的元信息,這一步叫 set AST 過程。

@babel/traverse 是一款用來自動遍歷抽象語法樹的工具,它會訪問樹中的全部節點,在進入每一個節點時觸發 enter 鉤子函數,退出每一個節點時觸發 exit 鉤子函數。開發者可在鉤子函數中對 AST 進行修改。

import traverse from "@babel/traverse";

traverse(ast, {
  enter(path) {
    // 進入 path 後觸發
  },
  exit(path) {
    // 退出 path 前觸發
  },
});
複製代碼

transforming(轉換),對應於編譯器的機器無關代碼優化階段(稍微有點牽強,但兩者工做內容都是修改AST),對 AST 作一些修改,好比針對上面的 log 增長一些信息方便咱們調試:

console.log(info) => console.log('[info]', info)
複製代碼

修改事後的 AST 結構:

{
  "type": "CallExpression",
  "callee": {
    // ....
  },
  "arguments": [
    "StringLiteral": {
      "type": "StringLiteral",
      "value": "'[info]'",
    },
    "Identifier": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "info",
    }
  ]
}
複製代碼

語義層面的轉換具體而言就是對AST進行增、刪、改操做,修改後的AST可能具備不一樣的語義,映射回代碼字符串也不一樣

生成

輸入AST,輸出JS源碼

generation(生成),對應於編譯器的代碼生成階段,把AST映射回代碼字符串。

利用 @babel/generator 將 AST 樹輸出爲轉碼後的代碼字符串。

實踐

說了這麼多接下來咱們就用代碼實踐一下上面的例子

相關npm包

  • @babel/parser 解析輸入源碼,建立AST
  • @babel/traverse 遍歷操做AST
  • @babel/generator 把AST轉回JS代碼
  • @babel/types AST操做工具庫

代碼

const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const generate = require('@babel/generator');
const t = require('@babel/types');

function compile(code) {
  // 1. parse
  const ast = parser.parse(code);

  // 2. traverse
  const visitor = {
    CallExpression(path) {
      const { callee, arguments } = path.node;
      if (
        t.isMemberExpression(callee)
        && callee.object.name === 'console'
        && callee.property.name === 'log'
        && arguments.length > 0
      ) {
        const variableName = arguments[0].name;
        path.node.arguments.unshift(
          t.StringLiteral(`[${variableName}]`)
        )
      }
    },
  };
  traverse.default(ast, visitor);

  // 3. generate
  return generate.default(ast, {}, code);
}

const code = `console.log(info)`;

const result = compile(code);
console.log(result.code);
複製代碼

總結

看到這,咱們的 AST 實踐也告一段落了。固然,文章所講的只是一個簡單的例子,但基本的原理思路八九不離十,更多的類型還得本身去探究。總之,掌握好 AST,你真的能夠作不少事情。

相關文章
相關標籤/搜索