编译原理工程实践—05使用babel操作AST实现代码转换
1. 操作步骤
babel 是一个 JavaScript 编译器,使用 babel 可以随心所欲地转化和操作 AST,实现对代码的分析、优化、变更等。可以在 https://esprima.org/demo/parse.html 体验转换查看 js 代码的词法、语法和AST。
babel操作AST的流程如下图所示,主要分为三步:
- parse: 首先使用
@babel/parser
库将js代码解析为AST抽象语法树 - transform: 然后使用
@babel/traverse
库遍历并修改AST,得到新的AST - generate: 最后使用
@babel/generator
库将新的AST生成为js代码,实现代码的转换
以下案例演示了使用 babel 修改箭头函数为普通函数:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
// 1. 将代码解析为 AST
const code = `const sum = (a, b) => a + b;`;
const ast = parsercsxiaoyao.com .parse(code, {
sourceType: 'module', // 或 'script'
plugins: ['jsx', 'typescript']
});
// 2. 遍历 AST
traverse(ast, {
// 访问所有 Identifier 节点
Identifier(path) {
console.log(`找到标识符: ${path.node.name}`);
},
// 访问箭头函数表达式
ArrowFunctionExpression(path) {
// 修改箭头函数为普通函数
path.replaceWith(
t.functionExpression(
null, // 匿名函数
path.node.params,
t.blockStatement([t.returnStatement(path.node.body)])
)
);
}
});
// 3. 生成代码(需 @babel/generator)
const { code: transformedCode } = require('@babel/generator').default(ast);
console.log(transformedCode);
// 输出: const sum = function(a, b) { return a + b; };
2. 核心操作方法
babel 的 @babel/traverse
模块提供了几个核心方法用于遍历和操作 AST:
-
traverse(ast, visitors)
: 这是最基础的AST遍历方法,它接受一个 AST 对象和一个访问器,开发者可以通过开发访问器实现操作各类节点 -
path.traverse(visitors)
: 用于在遍历过程中(访问器内部)继续遍历和操作当前节点的子节点 -
path.replaceWith(node)
: 用于在遍历过程中(访问器内部)替换当前节点为指定节点 -
path.remove()
: 用于在遍历过程中(访问器内部)删除当前节点及其子节点 -
path.skip()
: 用于在访问器内部跳过当前节点的子节点的遍历,避免深度遍历以提高效率 -
path.stop()
: 用于停止整个访问器的遍历
3. visitors访问器格式
上面 traverse
的 visitors 参数是用于遍历 AST 的一种特殊的访问器参数,它有两类遍历方式:
- 通用钩子访问: 以节点类型为key,设置遍历方法,如下方所示,每个 Identifier 类型的节点会被调用两次
traverse(ast, {
enter(path) {
// 进入任何节点时触发
},
exit(path) {
// 离开任何节点时触发
}
});
- 按类型访问: 以节点类型为key,支持的节点类型和 path / state 的具体参数值后面介绍
traverse(ast, {
Identifier(path, state) { // 节点类型
// path 提供了许多有用的属性和方法来操作和查询 AST 节点
// state 对象是一个可选的共享状态对象,用于在遍历过程中跨节点传递数据
}
});
4. 支持的节点类型
Babel 的 @babel/traverse
支持遍历所有 Babel AST 节点类型,这些节点类型与 @babel/types
包中定义的 AST 节点类型完全对应。以下是完整的分类列表:
4.1 基础节点类型
类型名 | 说明 | 示例 |
---|---|---|
Identifier |
标识符 | 变量名 、函数名 |
StringLiteral |
字符串字面量 | "hello" |
NumericLiteral |
数字字面量 | 42 、3.14 |
BooleanLiteral |
布尔字面量 | true 、false |
NullLiteral |
null 字面量 |
null |
RegExpLiteral |
正则表达式字面量 | /pattern/g |
BigIntLiteral |
BigInt 字面量 | 100n |
DecimalLiteral |
Decimal 字面量 | 3.14m (提案阶段) |
4.2 表达式(Expressions)
类型名 | 说明 | 示例 |
---|---|---|
ArrayExpression |
数组表达式 | [1, 2, 3] |
ObjectExpression |
对象表达式 | { key: value } |
FunctionExpression |
函数表达式 | function() {} |
ArrowFunctionExpression |
箭头函数表达式 | () => {} |
ClassExpression |
类表达式 | class {} |
CallExpression |
函数调用 | fn() |
NewExpression |
new 调用 |
new Date() |
MemberExpression |
成员访问 | obj.property |
OptionalMemberExpression |
可选链成员访问 | obj?.property |
AssignmentExpression |
赋值表达式 | x = 1 |
LogicalExpression |
逻辑表达式 | a && b |
BinaryExpression |
二元运算表达式 | a + b |
UnaryExpression |
一元运算表达式 | !x |
UpdateExpression |
更新表达式 | i++ |
ConditionalExpression |
三元条件表达式 | a ? b : c |
TemplateLiteral |
模板字符串 |
|
TaggedTemplateExpression |
标签模板字符串 | tag text |
SequenceExpression |
逗号分隔的表达式序列 | (a, b, c) |
YieldExpression |
yield 表达式 |
yield 1 |
AwaitExpression |
await 表达式 |
await promise |
ImportExpression |
动态 import() |
import('module') |
ChainExpression |
可选链整体表达式 | a?.b() |
4.3 语句(Statements)
类型名 | 说明 | 示例 |
---|---|---|
ExpressionStatement |
表达式语句 | console.log(x); |
BlockStatement |
代码块 | { ... } |
EmptyStatement |
空语句 | ; |
DebuggerStatement |
debugger 语句 |
debugger; |
ReturnStatement |
return 语句 |
return x; |
ThrowStatement |
throw 语句 |
throw error; |
TryStatement |
try/catch/finally |
try { ... } catch {} |
IfStatement |
if 语句 |
if (x) { ... } |
SwitchStatement |
switch 语句 |
switch (x) { ... } |
ForStatement |
for 循环 |
for (;;) { ... } |
ForInStatement |
for...in 循环 |
for (x in obj) { ... } |
ForOfStatement |
for...of 循环 |
for (x of arr) { ... } |
WhileStatement |
while 循环 |
while (x) { ... } |
DoWhileStatement |
do...while 循环 |
do { ... } while (x); |
LabeledStatement |
标签语句 | label: ... |
BreakStatement |
break 语句 |
break; |
ContinueStatement |
continue 语句 |
continue; |
WithStatement |
with 语句 |
with (obj) { ... } |
4.4 声明(Declarations)
类型名 | 说明 | 示例 |
---|---|---|
VariableDeclaration |
变量声明 | let x = 1; |
FunctionDeclaration |
函数声明 | function fn() {} |
ClassDeclaration |
类声明 | class C {} |
ImportDeclaration |
import 声明 |
import x from 'y'; |
ExportDeclaration |
export 声明 |
export default x; |
ExportNamedDeclaration |
具名导出声明 | export { x }; |
ExportDefaultDeclaration |
默认导出声明 | export default x; |
ExportAllDeclaration |
全部导出声明 | export * from 'x'; |
TSInterfaceDeclaration |
TypeScript 接口声明 | interface I {} |
TSTypeAliasDeclaration |
TypeScript 类型别名 | type T = string; |
4.5 特殊节点
类型名 | 说明 | 示例 |
---|---|---|
ThisExpression |
this 表达式 |
this |
Super |
super 调用 |
super.method() |
SpreadElement |
展开元素 | [...arr] |
RestElement |
剩余参数 | function(...args) {} |
MetaProperty |
元属性 | import.meta |
JSXElement |
JSX 元素 | <div /> |
JSXFragment |
JSX 片段 | <></> |
JSXAttribute |
JSX 属性 | <div key="value" /> |
JSXSpreadAttribute |
JSX 展开属性 | <div {...props} /> |
JSXExpressionContainer |
JSX 表达式容器 | <div>{x}</div> |
JSXText |
JSX 文本 | <div>text</div> |
JSXEmptyExpression |
JSX 空表达式 | <div>{}</div> |
TSTypeAssertion |
TS 类型断言 | x as string |
TSNonNullExpression |
TS 非空断言 | x! |
4.6 模块相关节点
类型名 | 说明 | 示例 |
---|---|---|
ImportSpecifier |
具名导入 | import { x } from 'y' |
ImportDefaultSpecifier |
默认导入 | import x from 'y' |
ImportNamespaceSpecifier |
命名空间导入 | import * as x from 'y' |
ExportSpecifier |
具名导出 | export { x } |
4.7 类型注解(TypeScript/Flow)
类型名 | 说明 |
---|---|
TSTypeAnnotation |
TS 类型注解 |
TSArrayType |
数组类型 |
TSUnionType |
联合类型 |
TSIntersectionType |
交叉类型 |
TSTypeReference |
类型引用 |
TSLiteralType |
字面量类型 |
5. visitor-path 常用属性和方法
babel 的 visitor path 对象是在 AST 遍历过程中传递给 visitor 函数的参数,它提供了许多有用的属性和方法来操作和查询 AST 节点。
5.1 核心属性
属性 | 说明 |
---|---|
node |
当前 AST 节点 |
parent |
当前节点的直接父节点(AST 对象) |
parentPath |
父节点对应的 NodePath 对象 |
container |
若当前节点在数组或对象中,表示包裹它的容器(如数组的父节点) |
key |
当前节点在父节点中的属性名(如 body 、arguments 等) |
listKey |
若当前节点在数组中,表示数组的键名(如 body 中的元素) |
scope |
当前节点的作用域(Scope 对象) |
hub |
Babel 的全局共享对象(包含配置、元数据等) |
contexts |
遍历的上下文信息(如是否在函数、循环中) |
data |
用于存储插件自定义数据的对象 |
type |
当前节点的类型(等同于 node.type ) |
opts |
传递给插件的配置选项(来自 babel.transform 的 options ) |
state |
遍历过程中的共享状态(可用于跨插件传递数据) |
removed |
标记当前节点是否已被移除 |
5.2 核心方法
1. 查询与遍历
方法 | 说明 |
---|---|
get(key) |
获取子节点的 NodePath (如 path.get('body') ) |
getSibling(index) |
获取数组中相邻节点的 NodePath |
getAncestor(maxLevel) |
递归获取祖先路径(可指定最大层级) |
findParent(callback) |
从父路径开始查找符合回调条件的路径 |
find(callback) |
从当前路径开始查找符合回调条件的路径 |
isAncestor(path) |
检查当前路径是否是另一个路径的祖先 |
isDescendant(path) |
检查当前路径是否是另一个路径的后代 |
inList() |
检查当前节点是否在数组中(结合 listKey 使用) |
getDeepestCommonAncestorFrom(paths) |
获取多个路径的最近公共祖先路径 |
2. 节点操作
方法 | 说明 |
---|---|
replaceWith(node) |
用新节点替换当前节点 |
replaceWithMultiple(nodes) |
用多个节点替换当前节点(适用于数组容器) |
insertBefore(nodes) |
在当前节点前插入节点 |
insertAfter(nodes) |
在当前节点后插入节点 |
remove() |
移除当前节点 |
unshiftContainer(key, nodes) |
在数组容器的开头插入节点(如函数体的 body 数组) |
pushContainer(key, nodes) |
在数组容器的末尾插入节点 |
replaceWithSourceString(code) |
用源代码字符串替换当前节点(自动解析为 AST) |
3. 作用域操作
方法 | 说明 |
---|---|
scope.generateUidIdentifier(name) |
生成唯一的标识符(避免命名冲突) |
scope.rename(oldName, newName) |
重命名当前作用域内的变量绑定 |
scope.hasBinding(name) |
检查当前作用域是否存在某个变量绑定 |
scope.getBinding(name) |
获取变量的绑定信息(包括声明、引用等) |
scope.getOwnBinding(name) |
仅检查当前作用域自身(不包含父级)的绑定 |
scope.push(binding) |
向作用域内添加新的绑定(手动操作) |
scope.removeBinding(na |
移除作用域内的某个绑定 |
4. 类型检查
方法 | 说明 |
---|---|
isXxx() |
一系列类型检查方法(如 isIdentifier() 、isFunctionDeclaration() ) |
assertXxx() |
断言当前节点类型(失败会抛错,如 assertExpression() ) |
matchesPattern(pattern) |
检查节点是否匹配特定的对象模式(如 React.createElement ) |
5. 流程控制
方法 | 说明 |
---|---|
skip() |
跳过当前节点的子节点遍历 |
stop() |
停止整个遍历过程 |
resync() |
在修改 AST 后重新同步遍历状态(用于手动修复路径) |
6. 代码生成与错误
方法 | 说明 |
---|---|
buildCodeFrameError(message) |
生成包含源代码位置信息的错误对象(用于插件报错) |
7. 高级工具
方法 | 说明 |
---|---|
hoist() |
将当前节点提升到父级作用域(用于变量或函数提升) |
setScope(scope) |
手动设置当前路径的作用域 |
getBindingIdentifierPaths() |
获取所有绑定标识符的路径(用于分析变量引用) |
getOuterBindingIdentifierPaths() |
获取外层作用域的绑定标识符路径 |
5.3 特殊场景方法
-
JSX 操作:
isJSXElement()
,getJSXAttribute()
,buildJSXElement()
等。 -
类型注解:
getTypeAnnotation()
,setTypeAnnotation()
(用于 Flow/TypeScript)。 -
模版字面量:
getTemplateLiteralElements()
.
5.4 注意事项
- 避免直接修改
node
属性:推荐使用replaceWith
或insertBefore
等方法,以确保 AST 的完整性。 - 作用域管理:在修改变量名或声明时,务必通过
scope
方法操作,避免破坏作用域链。 - 性能优化:在遍历大型 AST 时,尽量使用
skip()
或条件判断减少不必要的遍历。