AST 原理分析

7,074 阅读4分钟

作者:东北烤冷面@毛豆前端

一、什么是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转换成代码。

一个简单的需求进行说明

我们想在代码中的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

这样我们就完成了整个任务,当然这只是一个很简单的例子,在实际开发中,我们还需要进行更复杂的判断才能保证我们的功能完善。