AST 原理分析

毛豆前端發表於2019-04-28

作者:東北烤冷麵@毛豆前端

一、什麼是AST

抽象語法樹Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是原始碼語法結構的一種抽象表示。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。

二、AST有什麼作用

抽象語法樹在很多領域有廣泛的應用,比如瀏覽器,智慧編輯器,編譯器等。在JavaScript中,雖然我們並不會常常與AST直接打交道,但卻也會經常的涉及到它。例如使用UglifyJS來壓縮程式碼,bable對程式碼進行轉換,ts型別檢查,語法高亮等,實際這背後就是在對JavaScript的抽象語法樹進行操作。

三、AST生成過程

javascript的抽象語法樹的生成主要依靠的是Javascript Parser(js解析器),整個解析過程分為兩個階段:

1.詞法分析(Lexical Analysis)

詞法分析是電腦科學中將字元序列轉換為單詞(Token)序列的過程,進行詞法分析的程式叫做詞法分析器,也叫掃描器(Scanner)。

//code
let age='18'

//tokens
[
  {
    value: 'let',
    type:'identifier'
  },
  {
    type:'whitespace',
    value:' '
  },
  {
    value: 'age',
    type:'identifier'
  },
  {
    value: '=',
    type:'operator'
  },
  {
    value: '=',
    type:'operator'
  },
  {
    value: '18',
    type:'num'
  },
]

複製程式碼

2.語法分析(Parse Analysis)

語法分析是編譯過程的一個邏輯階段。語法分析的任務是在詞法分析的基礎上將單詞序列組合成語法樹,如“程式”,“語句”,“表示式”等等.語法分析程式判斷源程式在結構上是否正確。源程式的結構由上下文無關文法描述。

{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "age"
          },
          "init": {
            "type": "Literal",
            "value": "18",
            "raw": "'18'"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}
複製程式碼

常見的Javascript Parser有很多:

  • babylon:應用於bable
  • acorn:應用於webpack
  • espree:應用於eslint

四、拿babel為例

Babel是一個常用的工具,它的工作過程經過三個階段,解析(parsing)、轉換(transform)、生成(generate),如下圖所示,在parse階段,babel使用babylon庫將原始碼轉換為AST,在transform階段,利用各種外掛進行程式碼轉換,在generator階段,再利用程式碼生成工具,將AST轉換成程式碼。

AST 原理分析

一個簡單的需求進行說明

我們想在程式碼中的console列印出來的內容前面加上它所在的函式名稱,程式碼如下:

// index.js
function compile(code) {
   // todo
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)
複製程式碼

首先我們先安裝bable的全家桶工具:

yarn add @babel/{parser,traverse,types,generator}
複製程式碼

然後將其引入檔案中:

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types") 
function compile(code) {
    //tode
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

複製程式碼

我們可以通過AST Explorer檢視code程式碼的抽象語法樹結構,注意,這裡面我們的解析工具要選用babylon7,這樣和我們例子中程式碼解析出的結構才匹配

image.png

先解析拿到AST,直接生成程式碼片段:

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
    //   1. 解析
    const ast = parser.parse(code)
    //   2. 遍歷
   
    //   3. 生成程式碼片段
    return generator.default(ast, {}, code)
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

複製程式碼

執行一下

node index.js
複製程式碼

輸出結果

image.png

說明我們的程式碼沒有問題,已經跑通了!剩下的只需要我們在第二階段進行處理了。

第二階段
需要使用到訪問者(Visitors),訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個物件,定義了用於在一個樹狀結構中獲取具體節點的方法。這麼說有些抽象所以讓我們來看一個例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 你也可以先建立一個訪問者物件,並在稍後給它新增方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
複製程式碼

這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中遇見一個 Identifier 的時候會呼叫 Identifier() 方法。
所以在下面的程式碼中 Identifier() 方法會被呼叫四次(包括 square 在內,總共有四個 Identifier)。).

function square(n) {
  return n * n;
}

path.traverse(MyVisitor);
Called!
Called!
Called!
Called!
複製程式碼

回到我們的例子,我們只需要建立一個訪問者,訪問到CallExpression節點,然後通過判斷,去修改它arguments屬性的引數就可以完成我們的任務了

image.png

修改我們的程式碼

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
    //   1. 解析
    const ast = parser.parse(code)
    //   2. 遍歷
   		 //visitor可以對特定節點進行處理
    const visitor = {
      //定義需要轉換的節點CallExpression
        CallExpression(path) {
          	//獲取當前的節點
            const { callee } = path.node;
          	//判斷
            if (
                t.isMemberExpression(callee)
                &&
                callee.object.name === 'console'
                &&
                callee.property.name === 'log'
            ) {
              	// 獲取上層FunctionDeclaration路徑
                const funcPath = path.findParent(p => {
                    return p.isFunctionDeclaration();
                })
               	// 將上層函式名新增到引數前
                path.node.arguments.unshift(
                    t.stringLiteral(`function name ${funcPath.node.id.name}:`)
                )
            }
        }
    }
    traverse.default(ast, visitor)
    //   3. 生成程式碼片段
    return generator.default(ast, {}, code)
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

複製程式碼

我們再來列印下

image.png

這樣我們就完成了整個任務,當然這只是一個很簡單的例子,在實際開發中,我們還需要進行更復雜的判斷才能保證我們的功能完善。

相關文章