本文将介绍AST背景,基本语法,然后再从一个全网最小编译器聊起带你理解AST如何转换代码,理解什么是访问者模式,最后再带你手写一个babel插件。
AST概念
在计算机科学中,抽象语法树(Abstract Syntax Tree, AST),或简称语法树,是源代码语法结构的一种抽象表示。
它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似if-condition-then这样的条件跳转语句,可以使用带有三个分支节点的语句表示
Webpack Babel Postcss Eslint TypeScriptVue的模版编译,React的jsx解析首先JS语言在目标浏览器上运行需要降级,将高级的语法特性转换成目标浏览器上可以运行的语法,对语法的解析转换需要AST。
有人可能会问为什么不采用字符串读取或正则替换,这岂不更简单?如果直接对字符串操作,具有很大局限性,代码耦合难懂,不利于迭代维护扩展等。
AST就是提供一种通用的数据结构描述程序, 通过对树结构的修改,达到修改源代码的能力。这种树形结构的语法确实增加了学习成本, 通过AST这种语法树,解决了程序耦合问题,为团队协作插件化机制提供了可能,它是对复杂问题提供通用化解决方案的必然结果。
那么AST是谁定义的?
AST也是有标准的, JS parser的AST 大多是 estree 标准,Babel的编译也是遵循这个标准来的。
AST与编译之间的关系
首先编译,就是从一种高级语言,转换成另一种低级语言。
JavaScript、Java、 C 有很多用于描述逻辑的语言特性,比如 分支、循环、函数、OOP、判断,接近人的思维,可以让开发人员快速的掌握。汇编语言、 机器语言 这些是和硬件和执行细节有关的,会操作寄存器、内存。需要开发者理解熟悉计算机的工作原理。其次前端的编译,通常是指 ES6+ 到 ES5 的转换,AST 是中间产物,当让将TS转换为JS、JSX转换到JS也是编译.
以babel为例,
[{token}, {token}, {token}]以babel为例先熟悉下AST的树形结构。 点这里 ast在线预览 注意使用@babel/parser解析器
字面量的意思
jsconst a = 'zhangsan'; // StringLiteral
const b = `lisi`; // TemplateLiteral
const c = 123; // NumericLiteral
const reg = /^[a-z]+/; // RegExpLiteral
const d = true; // BooleanLiteral
const e = null; // NullLiteral
标识符: 变量名、属性名、参数名等各种声明和引用的名字
jsconst name = 'ww';
function foo(name) {
console.log(name);
}
name foo name console log namejs// BreakStatement
// ContinueStatement
// ReturnStatement
// DebuggerStatement
// ThrowStatement
{} // BlockStatement
try{}catch(e){}finally{} // TryStatement
// ForinStatement
// ForStatement
// WhileStatement
// DoWhileStatement
// ...
jsconst a = 1; // VariableDeclaration
function b() {} // FunctionDeclaration
class c {} // ClassDeclaration
import E from 'e'; // ImportDeclaration
export
// ExportDefautDeclaration
// ExportDeclaration
// ExportAllDeclaration
jsa=1;
1+2
[1, 2, 3]
start end loc {line, column}关于ast这一块,要多动手敲敲代码,多调试调试,推荐 全网最小编译器 这个案例多写几遍, 体会一下访问者模式,
这里简单记一下实现思路
src/test.tsts/**
* 1. string --> tokens
* 2. tokens --> ast
* 3. ast --> newAst
* 4. newAst --> code
* // Literal Identifier Statement Expression
*/
import { tokenizer, parse, traverser, codeGenerator } from './ast';
const input = `(add 2 (substract 4 2))`;
// 1. string --> tokens
/* [
{type: 'paren', value: '('},
{type: 'name', value: 'add'},
{type: 'number', value: '2'},
{type: 'paren', value: '('},
{type: 'name', value: 'substract'},
{type: 'number', value: '4'},
{type: 'number', value: '2'},
{type: 'paren', value: ')'},
{type: 'paren', value: ')'},
] */
const tokens = tokenizer(input);
// 2. tokens --> ast
/* {
type: 'Program',
body: [
{
type: 'CallExpression',
name: 'add',
params: [
{type: 'NumberLiteral', value: '2'},
{
type: 'CallExpression',
name: 'substract',
params: [
{type: 'NumberLiteral', value: '4'},
{type: 'NumberLiteral', value: '2'},
]
}
]
}
]
} */
const ast = parse(tokens);
// 3. ast -> newAst
/* var myNewAst = {
type: 'Program',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'add',
},
arguments:[
{type: 'NumberLiteral', value: '2'},
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'substract',
},
arguments: [
{type: 'NumberLiteral', value: '4'},
{type: 'NumberLiteral', value: '2'},
]
}
]
}
}
]
} */
const newAst = {
type: 'Program',
body: []
}
ast._context = newAst.body;
traverser(ast, {
CallExpression: {
entry(node: any, parent: any){
// ...
}
},
NumberLiteral: {
entry(node: any, parent: any) {
// ... 同上
}
}
}/* visiter */, );
// 4. newAst --> code
// add(2, substract(4, 2));
const output = codeGenerator(newAst);
console.log('output', output);
src/ast.tstsexport function tokenizer(input: string) {
const tokens: any[] = [];
// ...
return tokens;
}
export function parse(tokens: any[]): any {
const ast = {
type: 'Program',
body: []
}
// ...
return ast;
}
export function traverser(ast: any, visitor: any) {
// ...
return ast;
}
export function codeGenerator(newAst: any): string {
let output = '';
// ...
return output;
}
上述案例采用 ts编写,可以借助 ts-node和typescript这两个npm包运行
npx ts-node src/test.ts --files
--files 表示 让ts-node读取tsconfig.jsonts-node同样可以在VSCode进行断点调试, 调试方法与js调试完全一致
步骤简要描述
ast 和 newAst两个对象存在纠葛,表现在 asttree内部有多次newAst内部的结构,这样修改了ast newAst也会发生变更。_context),通过对该属性的增加修改操作,再利用引用类型的纠葛,达到修改newAst的效果vistor 和 对AST遍历的方法
visitor 它根据AST的每个Type类型提供进入节点和离开节点的方法(非必需),进入和离开节点的方法都遵循指责单一原则,即仅用于生成和维护新ast的节点。traverser 方法,它也是指责单一, 只提供遍历ast的run方法(注意:遍历ast的每个节点前后分别调用了visitor对象中的进入和退出方法)。深度优先遍历,遍历完成,也就生成了完整的newAst
完整代码: 点击这里
在学习babel插件编写前 建议先上前面的全网最小编译器练习几遍,调试下,否则阅读官方插件文档,云里雾里的,比较晦涩难懂。
Ast或AstNodeVisitor 形如 
traverser(ast: Ast, visitor: Visitor) 对 Ast 进行深度优先遍历,遍历的同时执行Visitor对应的钩子方法@babel/parser
之前称为 babylon,它是Babel的解析器。最初是从Acorn项目fork出来的。Acorn非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性)设计了一个基于插件的架构
作用 将源代码转换为AST
@babel/traverse 可以遍历 ast,并调用 visitor 函数修改 ast。对于 ast节点的判断、创建、修改等,可以用 @babel/types 包,当需要批量创建 AST 的时候可以用 @babel/template 简化逻辑。
@babel/generate 会把 ast 打印为目标代码字符串,同时生成 sourcemap
@babel/code-frame 用于中途遇到错误想打印代码位置
经过上述三个工具,就可以完成代码转换的过程,以下是一个精简案例
javascriptimport * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
traverse.default(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});
console.log(generate.default(ast, {}, code))
其他工具
console.log(123) ---> console.log('第5行第8列', 123);
console.warn(1, 2) ---> console.warn('第5行第8列', 1, 2);
js// log.mjs
// 测试 node log.mjs
import { parse } from "@babel/parser";
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import types from '@babel/types'
const sourceCode = `function add(a, b) {
console.log(a, b);
return a+b;
}`;
// https://astexplorer.net/ 查看ast树结构
// 对照着 方便编写Visiter对象方法
const ast = parse(sourceCode, {
sourceType: 'script'
});
// 要修改的节点
// ast.program.body[0].body.body[0] // CallExpression
traverse.default(ast, {
CallExpression(path, state) {
// path.node // 这个就是真实的ast节点, 可以参考 https://astexplorer.net
const calleeName = generate.default(path.node.callee).code; // console.log
if (calleeName === 'console.log') {
const { line, column } = path.node.loc.start;
path.node.arguments.unshift(
types.stringLiteral(`第${line}行第${column}列`);
);
}
}
})
// console.log 参数已经改变
// console.log(ast.program.body[0].body.body[0].expression.arguments);
// ast 代码转目标代码
const { code, map } = generate.default(ast);
console.log(code);
获取节点信息
path.node 当前节点path.parent 父节点path.parentPath 父节点的 pathpath.scope javaScript 中的作用域链信息path.hub
path.hub.file 拿到最外层 File 对象path.hub.getScope 拿到最外层作用域,path.hub.getCode 拿到源码字符串判断 AST 类型
path.isFunctionDeclaration()path.isTemplateLiteral()...增删改 AST 类型
path.insertBefore、path.insertAfterpath.replaceWith、path.replaceWithMultiple、replaceWithSourceStringpath.remove在开发时我们可以通过 debugger 或者直接查看 babel 源码测试用例了解更多的操作 api。

state(状态)
可以从 state 中获取插件的配置项 opts 以及 file 对象:

npm init -ypnpm add @babel/cli @babel/core @babel/generator @babel/parserjs// babel.config.js
module.exports = {
"presets": [],
plugins: [
"./babel-plugin-log.js",
]
}
// babel-plugin-log.js
const generate = require('@babel/generator') ;
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression (path) {
const calleeName = generate.default(path.node.callee).code;
if (calleeName !== 'console.log') return
path.shouldSkip = true
const args = path.node.arguments
const newArgs = []
args.forEach((arg, index) => {
const label = getExpressionLiteral(arg, path)
newArgs.push(t.stringLiteral(label), arg)
})
path.node.arguments = newArgs
}
}
}
}
function getExpressionLiteral (expression, path) {
const {
start,
end
} = expression
return path.hub.file.code.slice(start, end)
}
// input.js
console.log(a);
console.log(a, b, c);
console.log(a + b);
npx babel input.js --out-file output.js本段主要参考内容
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!