本文将介绍AST背景,基本语法,然后再从一个全网最小编译器聊起带你理解AST如何转换代码,理解什么是访问者模式,最后再带你手写一个babel插件。
AST概念
在计算机科学中,抽象语法树(Abstract Syntax Tree, AST),或简称语法树,是源代码语法结构的一种抽象表示。
它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似if-condition-then
这样的条件跳转语句,可以使用带有三个分支节点的语句表示
Webpack
Babel
Postcss
Eslint
TypeScript
Vue
的模版编译,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
name
js// 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.ts
ts/**
* 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.ts
tsexport 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.json
ts-node
同样可以在VSCode进行断点调试, 调试方法与js调试完全一致步骤简要描述
ast
和 newAst
两个对象存在纠葛,表现在 ast
tree内部有多次newAst内部的结构,这样修改了ast newAst也会发生变更。_context
),通过对该属性的增加修改操作,再利用引用类型的纠葛,达到修改newAst
的效果vistor
和 对AST遍历的方法
visitor
它根据AST的每个Type类型提供进入节点和离开节点的方法(非必需),进入和离开节点的方法都遵循指责单一原则,即仅用于生成和维护新ast的节点。traverser
方法,它也是指责单一, 只提供遍历ast的run方法(注意:遍历ast的每个节点前后分别调用了visitor
对象中的进入和退出方法)。深度优先遍历,遍历完成,也就生成了完整的newAst
完整代码: 点击这里
在学习babel插件编写前 建议先上前面的全网最小编译器练习几遍,调试下,否则阅读官方插件文档,云里雾里的,比较晦涩难懂。
Ast
或AstNode
Visitor
形如 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.insertAfter
path.replaceWith
、path.replaceWithMultiple
、replaceWithSourceString
path.remove
在开发时我们可以通过 debugger
或者直接查看 babel
源码测试用例了解更多的操作 api
。
state(状态)
可以从 state
中获取插件的配置项 opts
以及 file
对象:
npm init -y
pnpm add @babel/cli @babel/core @babel/generator @babel/parser
js// 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 许可协议。转载请注明出处!