抽象语法树(
abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。
AST 被广泛应用于翻译、格式化、代码检查、编译、构建等方面。前端开发中 babel、eslint、prettier、webpack 等工具,无一例外的应用了AST。这些工具底层可能依赖了不同的解析器生成 AST,比如 eslint 使用了espree、babel 使用了 Babylon 等。AST 树主要使用场景如下:
- JS 反编译,语法解析
- Babel 编译 ES6 语法
- 代码高亮
- 关键字匹配
- 作用域判断
- 代码压缩
在本文中主要以 Babel 为例来介绍 AST。Babel,从ECMAScript的诞生后,它便充当了代码和运行环境的翻译官,让我们随心所欲的使用js的新语法进行代码编写。Babel 单词的本意是通天塔的意思。Babel 广义上可以理解成一个生态圈,基于一个叫做 Babylon 的解析器。
一. 常用节点和常用方法概括
具体推荐 AST在线解析网站 astexplorer.net/
常用节点:
常用方法:
二. AST 分析步骤的拆解
1.解析(parse):解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:
-
词法分析
Lexical Analysis:词法分析阶段把字符串形式的代码转换为令牌(tokens) 流。你可以把令牌看作是一个扁平的语法片段数组,如下:[ { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } }, { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } }, { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } }, ... ]
每一个 type 有一组属性来描述该令牌,和 AST 节点一样它们也有 start,end,loc 属性:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
- 语法分析
Syntactic Analysis:语法分析阶段会把一个令牌流转换成AST的形式。 这个阶段会使用令牌中的信息把它们转换成一个AST的表述结构,这样更易于后续的操作。
2.转换(transform):接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分。
3.生成(generate):代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
const fs = require('fs');
//babel库相关,解析,转换,构建,生产
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const generator = require("@babel/generator").default;
const jscode = 'var a = 123, b = 321'
// 第一步
// 将 code 转换为 AST 树
const ast = parser.parse(jscode);
// 第二步 遍历 AST 树,转换到相应格式的源码 AST
traverse(ast, { /** 转换逻辑 */ });
// 第三步
// 生成新的js code,并保存到文件中输出
const {code} = generator(ast);
fs.writeFile('./build.js', code, (err)=>{});
三. 实例解析
function add(a, b) {
return a + b
}
先将这个函数拆成了基本的三块:
- 一个 id,就是它的名字,即add
- 两个 params,就是它的参数,即[a, b]
- 一块 body,也就是大括号内的一堆东西
add 没办法继续拆下去了,它是一个最基础 Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。
params 继续拆下去,其实是两个 Identifier 组成的数组 [a, b]。之后也没办法拆下去了。
body 其实是一个 BlockStatement(块状域)对象,用来表示是{return a + b}
{
"type": "File",
"start": 0,
"end": 38,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 38,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 38,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 1
}
},
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
},
"identifierName": "add"
},
"name": "add"
},
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 14
},
"identifierName": "a"
},
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 16
},
"end": {
"line": 1,
"column": 17
},
"identifierName": "b"
},
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 38,
"loc": {
"start": {
"line": 1,
"column": 19
},
"end": {
"line": 3,
"column": 1
}
},
"body": [
{
"type": "ReturnStatement",
"start": 24,
"end": 36,
"loc": {
"start": {
"line": 2,
"column": 3
},
"end": {
"line": 2,
"column": 15
}
},
"argument": {
"type": "BinaryExpression",
"start": 31,
"end": 36,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 15
}
},
"left": {
"type": "Identifier",
"start": 31,
"end": 32,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 11
},
"identifierName": "a"
},
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 35,
"end": 36,
"loc": {
"start": {
"line": 2,
"column": 14
},
"end": {
"line": 2,
"column": 15
},
"identifierName": "b"
},
"name": "b"
}
}
}
],
"directives": []
}
}
],
"directives": []
},
"comments": []
}
四. AST 实战
- 打印当前路径所对应的源代码
var a = 123,b = 321 的 AST 树
{
"type": "File",
"start": 0,
"end": 20,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 20
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 20,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 20
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 20,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 20
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 4
},
"end": {
"line": 1,
"column": 11
}
},
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"loc": {
"start": {
"line": 1,
"column": 4
},
"end": {
"line": 1,
"column": 5
},
"identifierName": "a"
},
"name": "a"
},
"init": {
"type": "NumericLiteral",
"start": 8,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 8
},
"end": {
"line": 1,
"column": 11
}
},
"extra": {
"rawValue": 123,
"raw": "123"
},
"value": 123
}
},
{
"type": "VariableDeclarator",
"start": 13,
"end": 20,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 20
}
},
"id": {
"type": "Identifier",
"start": 13,
"end": 14,
"loc": {
"start": {
"line": 1,
"column": 13
},
"end": {
"line": 1,
"column": 14
},
"identifierName": "b"
},
"name": "b"
},
"init": {
"type": "NumericLiteral",
"start": 17,
"end": 20,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 20
}
},
"extra": {
"rawValue": 321,
"raw": "321"
},
"value": 321
}
}
],
"kind": "var"
}
],
"directives": []
},
"comments": []
}
traverse(ast,{
// 此处访问的是VariableDeclarator的节点路径(VariableDeclarator未在常用节点列出),下同
VariableDeclarator(path)
{
console.log(path.toString()); // a = 123, b = 321;
console.log(generator(path.node).code) // a = 123, b = 321;
}
});
- 判断当前path的类型type(使用isxxx,如:isVariableDeclarator)
traverse(ast,{
VariableDeclarator(path)
{
console.log(path.type === 'VariableDeclarator'); // true
console.log(path.isVariableDeclarator()) // true
console.log(types.isVariableDeclarator(path.node)) //true
}
});
- 删除指定节点 remove
traverse(ast,{
VariableDeclarator(path)
{
console.log(path.toString()) // b = 321
if (path.node.id.name === 'b'){
path.remove()};
}
}
});
- 删除空语句
traverse(ast,{
EmptyStatement(path){
path.remove();
}
});
- 处理Unicode字符串 -> 将源码中Unicode字符串转换成我们易读的字符串
/** 处理Unicode字符串 -> 将源码中Unicode字符串转换成我们易读的字符串
* 假如: jscode = 'var _0x350e = ['\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21', '\x38\x36\x54\x66\x76\x53\x44\x73', '\x32\x37\x31\x39\x38\x36\x32\x76\x65\x77\x71\x65\x56', '\x31\x7a\x62\x78\x4c\x6a\x46', '\x31\x36\x34\x37\x31\x30\x32\x45\x64\x79\x68\x66\x6a', '\x35\x33\x36\x37\x31\x39\x59\x69\x47\x42\x65\x6c', '\x36\x38\x32\x35\x38\x33\x5a\x71\x61\x43\x76\x76', '\x32\x30\x35\x30\x34\x32\x44\x5a\x42\x66\x6e\x75', '\x31\x35\x32\x34\x34\x35\x43\x65\x54\x51\x51\x6d', '\x34\x39\x31\x35\x6f\x52\x43\x4e\x4f\x6f', '\x6c\x6f\x67', '\x31\x54\x62\x71\x48\x45\x4e', '\x31\x31\x6f\x61\x57\x75\x79\x4e'];'
* 对照在线网站进行解析时,其value节点是可阅读的字符串,
* 难以识别的字符串放在了extra节点里,所以直接进行删除即可。
*/
traverse(ast,{
NumericLiteral({node}) {
if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
node.extra = undefined;
}
},
StringLiteral({node})
{
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra = undefined;
}
}
});