作者:东北烤冷面@
一、什么是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
- :应用于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)
复制代码
我们可以通过查看code代码的抽象语法树结构,注意,这里面我们的解析工具要选用babylon7,这样和我们例子中代码解析出的结构才匹配
先解析拿到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
复制代码
输出结果
说明我们的代码没有问题,已经跑通了!剩下的只需要我们在第二阶段进行处理了。
第二阶段
需要使用到访问者(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属性的参数就可以完成我们的任务了
修改我们的代码
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)
复制代码
我们再来打印下
这样我们就完成了整个任务,当然这只是一个很简单的例子,在实际开发中,我们还需要进行更复杂的判断才能保证我们的功能完善。