零散專題35 AST抽象語(yǔ)法樹.md

什么是抽象語(yǔ)法樹

抽象語(yǔ)法樹(abstract syntax tree,AST,或者簡(jiǎn)稱語(yǔ)法樹)是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹狀表現(xiàn)形式,它以樹狀的形式表現(xiàn)編程語(yǔ)言的語(yǔ)法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。

之所以說是抽象的,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié),比如嵌套括號(hào)被隱含在樹結(jié)構(gòu)中,并不會(huì)以節(jié)點(diǎn)的形式呈現(xiàn)出來(lái)。而類似于if...else這樣的條件語(yǔ)句,可以使用兩個(gè)分支的節(jié)點(diǎn)來(lái)表示。

比如下面這段代碼:

while (b !== 0) {
  if (a > b) {
      a = a - b
  } else {
      b = b - a
  }
}
return a;

它的對(duì)應(yīng)的抽象語(yǔ)法樹如下:

image

可以通過astexplorer這個(gè)網(wǎng)站,將我們輸入的代碼轉(zhuǎn)換為AST展示出來(lái),可以展示為書結(jié)構(gòu),也可以直接以JSON格式展示,比如在左側(cè)輸入let a = 123;,對(duì)應(yīng)的語(yǔ)法樹:

image

也可以直接輸入JSON:

{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 12,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 11,
            "value": 123,
            "raw": "123"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

語(yǔ)法樹中每個(gè)元素可以成為一個(gè)節(jié)點(diǎn)(node),類型包括:

(parameter) node: 
Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression |
ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement |
BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement |
LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | 
TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement |
VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression |
YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression |
LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression |
SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty |
AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement |
ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition |
ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration |
ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier

使用場(chǎng)景

平時(shí)我們好像從來(lái)沒有接觸過AST,但是實(shí)際上它的適用場(chǎng)景一直相伴左右,比如:

  • JS反編譯,語(yǔ)法解析
  • Babel編譯ES6語(yǔ)法
  • 代碼高亮
  • 關(guān)鍵字匹配
  • 作用域判斷
  • 代碼壓縮

AST分析

使用剛才提到的astexplorer這個(gè)網(wǎng)站,解析1 + 2 * 3,得到的AST語(yǔ)法樹

image

從語(yǔ)法樹我們開出來(lái),body里面的exporession(表達(dá)式)是BinaryExpress(二院表達(dá)式),startend標(biāo)識(shí)了我們這個(gè)表達(dá)式字符的起止位置,operator+,然后left屬性的值是一個(gè)Literal字面量,value2,right屬性的值又是另外一個(gè)BinaryExpress,left的值是2operator*,right右側(cè)值是3

這樣,通過一個(gè)樹狀結(jié)構(gòu),將我們輸入的表達(dá)式明明白白展示出來(lái)了。

將表達(dá)式添加一個(gè)括號(hào),變成(1 + 2) * 3,得到的新的語(yǔ)法樹:

image

可以看出來(lái),left值變?yōu)榱艘粋€(gè)BinaryExpress,operator*right值是3。

比較兩個(gè)表達(dá)式的語(yǔ)法樹,可以發(fā)現(xiàn):

  1. 在確定類型為ExpressionSatatement后,會(huì)按照代碼執(zhí)行的先后順序,將表達(dá)式BinaryExpression分解為left、operatorright三部分
  2. 每部分都標(biāo)明了類型、起止位置、值等信息
  3. 標(biāo)明了操作符類型

再來(lái)看一下常用的箭頭函數(shù):

const fn = (a, b) => {
  return a + b
};

AST的JSON結(jié)果:

{
  "type": "Program",
  "start": 0,
  "end": 40,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 40,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 39,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 8,
            "name": "fn"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 11,
            "end": 39,
            "id": null,
            "expression": false,
            "generator": false,
            "params": [
              {
                "type": "Identifier",
                "start": 12,
                "end": 13,
                "name": "a"
              },
              {
                "type": "Identifier",
                "start": 15,
                "end": 16,
                "name": "b"
              }
            ],
            "body": {
              "type": "BlockStatement",
              "start": 21,
              "end": 39,
              "body": [
                {
                  "type": "ReturnStatement",
                  "start": 25,
                  "end": 37,
                  "argument": {
                    "type": "BinaryExpression",
                    "start": 32,
                    "end": 37,
                    "left": {
                      "type": "Identifier",
                      "start": 32,
                      "end": 33,
                      "name": "a"
                    },
                    "operator": "+",
                    "right": {
                      "type": "Identifier",
                      "start": 36,
                      "end": 37,
                      "name": "b"
                    }
                  }
                }
              ]
            }
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

耐下性子,仔細(xì)讀一下語(yǔ)法樹,其實(shí)也不難理解,首先bodytype變?yōu)榱?code>VariableDeclaration,declarations是一個(gè)數(shù)組,首先聲明了fn這個(gè)變量,然后在init里面是一個(gè)typeArrowFunctionExpression的節(jié)點(diǎn),里面有params、bodyReturnStatement等各種用來(lái)標(biāo)識(shí)箭頭函數(shù)的屬性值,

實(shí)現(xiàn)

從上面可以看出來(lái),抽象語(yǔ)法樹其實(shí)就是將一類標(biāo)簽轉(zhuǎn)化成通用標(biāo)識(shí)符,從而歸納出的一個(gè)類似于樹形結(jié)構(gòu)的語(yǔ)法樹。

那么這個(gè)過程是如何實(shí)現(xiàn)的呢?

簡(jiǎn)單學(xué)習(xí)了一下,首先需要幾個(gè)工具包用來(lái)解析JS語(yǔ)法、遍歷樹結(jié)構(gòu)以及生成新的樹結(jié)構(gòu):

const esprima = require('esprima'); // 解析js的語(yǔ)法的包
const estraverse = require('estraverse'); // 遍歷樹的包
const escodegen = require('escodegen'); // 生成新的樹的包

let code = `function getAST(){}`;

//解析js的語(yǔ)法
let tree = esprima.parseScript(code);
//遍歷樹
estraverse.traverse(tree, {
  enter(node) {
    console.log('enter: ' + node.type);
  },
  leave(node) {
    console.log('leave: ' + node.type);
  }
});

//生成新的樹
let r = escodegen.generate(tree);
console.log(r);

通過遍歷、修改抽象語(yǔ)法樹,我們可以去改變?nèi)魏屋敵鼋Y(jié)果,也就是說,在編譯的過程中,借助抽象語(yǔ)法樹,我們可以去改變?cè)瓉?lái)的輸入,得到想要的結(jié)果。

這也就是Babel轉(zhuǎn)義JavaScript代碼的原理。

關(guān)于Babel

Babel使用一個(gè)基于ESTree并修改過的AST,它的內(nèi)核說明文檔可以在這里找到。

Babel的三個(gè)主要處理步驟分別是: 解析(parse),轉(zhuǎn)換(transform),生成(generate)。

(1)解析

接受代碼并輸出AST,這個(gè)步驟又分為兩個(gè)階段:詞法分析(Lexical Aanalysis)和語(yǔ)法分析(Syntactic Analysis)

詞法分析階段把字符串形式的代碼轉(zhuǎn)換為令牌流,可以將令牌看做一個(gè)扁平的語(yǔ)法片段數(shù)組

n * n;

↓

[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

其中每一個(gè)type都有一組數(shù)形來(lái)描述該令牌,和AST節(jié)點(diǎn)一樣也有start、end等屬性:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

語(yǔ)法分析階段會(huì)吧一個(gè)令牌轉(zhuǎn)換成AST的形式,這個(gè)階段會(huì)使用令牌中的信息把他們轉(zhuǎn)換成一個(gè)AST的表述結(jié)構(gòu),便于后續(xù)操作

(2)轉(zhuǎn)換

接受AST并對(duì)其進(jìn)行遍歷,此過程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新和移除等操作。這是Babel最復(fù)雜的過程,也是Babel插件接入工作的部分。

(3)生成

深度優(yōu)先遍歷將經(jīng)過一系列轉(zhuǎn)換后的AST,并將其轉(zhuǎn)換成字符串形式的代碼,同時(shí)還會(huì)創(chuàng)建源碼映射。

單元測(cè)試覆蓋率

JavaScript單元測(cè)試覆蓋率統(tǒng)計(jì)方法的核心思想,是在源代碼響應(yīng)的位置注入設(shè)定的統(tǒng)計(jì)代碼,當(dāng)執(zhí)行測(cè)試代碼的時(shí)候,代碼運(yùn)行到注入的地方,就會(huì)執(zhí)行對(duì)應(yīng)的統(tǒng)計(jì)代碼,生成覆蓋率統(tǒng)計(jì)報(bào)告。大概步驟如下:

(1)對(duì)源代碼進(jìn)行語(yǔ)法分析、解析,然后生成抽象語(yǔ)法樹

(2)在語(yǔ)法樹相應(yīng)的位置注入統(tǒng)計(jì)代碼。

在程序執(zhí)行到這個(gè)位置的時(shí)候?qū)ο鄳?yīng)的全局變量賦值,確保執(zhí)行之后能夠根據(jù)全局變量知道代碼的執(zhí)行流程。

(3)通過注入統(tǒng)計(jì)代碼的抽象語(yǔ)法樹,生成對(duì)應(yīng)的JavaScript代碼

(4)將生成好的JavaScript代碼交給執(zhí)行環(huán)境(Node或者瀏覽器)運(yùn)行

(5)執(zhí)行單元測(cè)試,產(chǎn)生的統(tǒng)計(jì)信息,放到全局變量里面。

(6)根據(jù)全局變量中的覆蓋率信息生成特定格式的報(bào)告。

總結(jié)

大致學(xué)習(xí)了一下AST的知識(shí),并沒有很深入,只是了解到AST是對(duì)源代碼的一種樹狀結(jié)構(gòu)的表示形式,可以用來(lái)在編譯階段對(duì)代碼進(jìn)行處理。Babel之所以能夠?qū)崿F(xiàn)JavaScript的轉(zhuǎn)換,就是利用了AST,經(jīng)歷了解析、轉(zhuǎn)換、生成三個(gè)步驟,其中轉(zhuǎn)換是最復(fù)雜的步驟,各種Babel的插件也是在這個(gè)步驟中發(fā)揮作用。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容