AST 原理分析

做者:東北烤冷麪@毛豆前端javascript

1、什麼是AST

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

2、AST有什麼做用

抽象語法樹在不少領域有普遍的應用,好比瀏覽器,智能編輯器,編譯器等。在JavaScript中,雖然咱們並不會經常與AST直接打交道,但卻也會常常的涉及到它。例如使用UglifyJS來壓縮代碼,bable對代碼進行轉換,ts類型檢查,語法高亮等,實際這背後就是在對JavaScript的抽象語法樹進行操做。java

3、AST生成過程

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

1.詞法分析(Lexical Analysis)

詞法分析是計算機科學中將字符序列轉換爲單詞(Token)序列的過程,進行詞法分析的程序叫作詞法分析器,也叫掃描器(Scanner)。webpack

//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)

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

{
  "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有不少:github

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

4、拿babel爲例

Babel是一個經常使用的工具,它的工做過程通過三個階段,解析(parsing)、轉換(transform)、生成(generate),以下圖所示,在parse階段,babel使用babylon庫將源代碼轉換爲AST,在transform階段,利用各類插件進行代碼轉換,在generator階段,再利用代碼生成工具,將AST轉換成代碼。
web

一個簡單的需求進行說明

咱們想在代碼中的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

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

相關文章
相關標籤/搜索