AST 实战应用
问题
如何利用 AST 进行代码转换?Babel 插件和 ESLint 规则如何开发?
答案
1. Babel 插件开发
Babel 插件是一个返回 visitor 对象的函数,用于遍历和修改 AST 节点。
基本结构
import type { PluginObj, NodePath } from '@babel/core';
import type { Identifier, CallExpression } from '@babel/types';
// Babel 插件基本结构
export default function myPlugin(): PluginObj {
return {
name: 'my-plugin',
visitor: {
// 节点类型 → 处理函数
Identifier(path: NodePath<Identifier>) {
// path.node: 当前节点
// path.parent: 父节点
// path.scope: 作用域信息
},
},
};
}
实战:自动移除 console.log
import type { PluginObj, NodePath } from '@babel/core';
import type { CallExpression, MemberExpression } from '@babel/types';
import * as t from '@babel/types';
export default function removeConsolePlugin(): PluginObj {
return {
name: 'remove-console',
visitor: {
CallExpression(path: NodePath<CallExpression>) {
const callee = path.node.callee;
// 匹配 console.log / console.warn / console.error
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: 'console' }) &&
t.isIdentifier(callee.property) &&
['log', 'warn', 'info', 'debug'].includes(callee.property.name)
) {
path.remove();
}
},
},
};
}
// 使用
// babel.config.js
// { plugins: ['./plugins/remove-console'] }
实战:自动注入埋点
import type { PluginObj, NodePath } from '@babel/core';
import type {
FunctionDeclaration,
ArrowFunctionExpression,
FunctionExpression
} from '@babel/types';
import * as t from '@babel/types';
import template from '@babel/template';
// 在每个函数入口注入性能追踪代码
export default function trackPlugin(): PluginObj {
// 使用 template 创建 AST 节点
const buildTrack = template(`
__track(FUNCTION_NAME);
`);
return {
name: 'auto-track',
visitor: {
// 同时匹配多种函数节点
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(
path: NodePath<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
) {
const funcName =
path.node.type === 'FunctionDeclaration' && path.node.id
? path.node.id.name
: 'anonymous';
const trackAST = buildTrack({
FUNCTION_NAME: t.stringLiteral(funcName),
});
// 箭头函数可能没有 body block
if (t.isBlockStatement(path.node.body)) {
(path.node.body.body as any[]).unshift(trackAST);
}
},
},
};
}
Path API 常用方法
| 方法 | 说明 |
|---|---|
path.node | 当前 AST 节点 |
path.parent | 父节点 |
path.parentPath | 父路径 |
path.scope | 作用域信息 |
path.replaceWith(node) | 替换当前节点 |
path.replaceWithMultiple(nodes) | 替换为多个节点 |
path.insertBefore(node) | 在前面插入 |
path.insertAfter(node) | 在后面插入 |
path.remove() | 删除节点 |
path.skip() | 跳过子节点遍历 |
path.stop() | 停止整个遍历 |
path.traverse(visitor) | 从当前节点开始遍历 |
path.get('property') | 获取子路径 |
path.findParent(cb) | 向上查找匹配的祖先 |
path.isIdentifier() | 类型检查 |
2. ESLint 规则开发
ESLint 规则同样基于 AST Visitor 模式,但使用 espree(基于 acorn)解析器。
基本结构
import type { Rule } from 'eslint';
const rule: Rule.RuleModule = {
meta: {
type: 'suggestion', // 'problem' | 'suggestion' | 'layout'
docs: {
description: '禁止使用 var',
recommended: true,
},
fixable: 'code', // 是否提供自动修复
schema: [], // 规则选项的 JSON Schema
messages: {
noVar: '请使用 let 或 const 替代 var',
},
},
create(context: Rule.RuleContext) {
return {
// 返回 visitor 对象
VariableDeclaration(node) {
if (node.kind === 'var') {
context.report({
node,
messageId: 'noVar',
fix(fixer) {
// 自动修复:var → let
return fixer.replaceTextRange(
[node.start!, node.start! + 3],
'let'
);
},
});
}
},
};
},
};
export default rule;
实战:禁止魔法数字
import type { Rule } from 'eslint';
const ALLOWED_NUMBERS = [0, 1, -1, 2];
const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: { description: '禁止使用魔法数字(未具名的数字常量)' },
schema: [{
type: 'object',
properties: {
allowed: { type: 'array', items: { type: 'number' } },
},
}],
messages: {
noMagicNumber: '避免使用魔法数字 {{value}},请定义为具名常量',
},
},
create(context) {
const options = context.options[0] || {};
const allowed = new Set(options.allowed || ALLOWED_NUMBERS);
return {
Literal(node) {
if (typeof node.value !== 'number') return;
if (allowed.has(node.value)) return;
// 排除:数组索引、枚举值、类型注解中的数字
const parent = node.parent;
if (
parent?.type === 'VariableDeclarator' &&
parent.parent?.type === 'VariableDeclaration' &&
parent.parent.kind === 'const'
) {
return; // const MAX = 100; 允许
}
context.report({
node,
messageId: 'noMagicNumber',
data: { value: String(node.value) },
});
},
};
},
};
export default rule;
3. Codemod — 大规模代码迁移
Codemod 是利用 AST 转换进行批量代码迁移的工具,常用于框架升级。
jscodeshift
jscodeshift 是 Facebook 开发的 Codemod 工具,提供链式 API 操作 AST:
import type { Transform, JSCodeshift } from 'jscodeshift';
// 示例:将 React.PropTypes 迁移为独立的 prop-types 包
const transform: Transform = (fileInfo, api) => {
const j: JSCodeshift = api.jscodeshift;
const root = j(fileInfo.source);
// 将 React.PropTypes.xxx → PropTypes.xxx
root
.find(j.MemberExpression, {
object: {
type: 'MemberExpression',
object: { name: 'React' },
property: { name: 'PropTypes' },
},
})
.forEach((path) => {
// React.PropTypes.string → PropTypes.string
j(path).replaceWith(
j.memberExpression(
j.identifier('PropTypes'),
path.node.property
)
);
});
// 添加 import PropTypes from 'prop-types'
const imports = root.find(j.ImportDeclaration);
const hasPropTypesImport = imports.some(
(p) => p.node.source.value === 'prop-types'
);
if (!hasPropTypesImport) {
const newImport = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier('PropTypes'))],
j.literal('prop-types')
);
imports.at(0).insertAfter(newImport);
}
return root.toSource({ quote: 'single' });
};
export default transform;
运行 Codemod:
# 对 src 目录下所有文件执行转换
npx jscodeshift -t ./transforms/react-proptypes.ts src/
常见 Codemod 场景
| 场景 | 说明 |
|---|---|
| 框架升级 | React class → Hooks、Vue 2 → Vue 3 |
| API 重命名 | 修改函数名、参数顺序 |
| 导入路径调整 | 组件库路径变更 |
| 模式替换 | lodash.get → 可选链 |
| 废弃 API 迁移 | componentWillMount → useEffect |
4. TypeScript Compiler API
TypeScript 提供了完整的编译器 API,可以进行类型感知的代码转换:
import ts from 'typescript';
// 1. 解析代码为 AST
const sourceFile = ts.createSourceFile(
'example.ts',
`
interface User {
name: string;
age: number;
}
function greet(user: User): string {
return \`Hello, \${user.name}!\`;
}
`,
ts.ScriptTarget.Latest,
true
);
// 2. 遍历 AST
function visit(node: ts.Node, depth = 0) {
const indent = ' '.repeat(depth);
console.log(`${indent}${ts.SyntaxKind[node.kind]}`);
ts.forEachChild(node, (child) => visit(child, depth + 1));
}
visit(sourceFile);
// 3. 使用 Transformer 修改 AST
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
return (sourceFile) => {
const visitor = (node: ts.Node): ts.Node => {
// 将所有 interface 添加 readonly
if (ts.isPropertySignature(node) && node.name) {
return ts.factory.updatePropertySignature(
node,
[ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)],
node.name,
node.questionToken,
node.type
);
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sourceFile, visitor) as ts.SourceFile;
};
};
// 4. 应用转换器并生成代码
const result = ts.transform(sourceFile, [transformer]);
const printer = ts.createPrinter();
const output = printer.printFile(result.transformed[0]);
console.log(output);
5. 实战:自定义 CSS-in-JS 编译插件
import type { PluginObj } from '@babel/core';
import * as t from '@babel/types';
// 编译时将 css`` 模板字面量提取为 CSS 文件
// css`color: red; font-size: 14px;` → 'generated-class-xxx'
export default function cssExtractPlugin(): PluginObj {
let classIndex = 0;
const styles: Map<string, string> = new Map();
return {
name: 'css-extract',
visitor: {
TaggedTemplateExpression(path) {
if (!t.isIdentifier(path.node.tag, { name: 'css' })) return;
// 提取 CSS 文本
const quasis = path.node.quasi.quasis;
const cssText = quasis.map((q) => q.value.raw).join('');
// 生成类名
const className = `_css_${classIndex++}`;
styles.set(className, `.${className} { ${cssText} }`);
// 替换为类名字符串
path.replaceWith(t.stringLiteral(className));
},
},
// 编译结束后输出 CSS 文件
post(state) {
const css = Array.from(styles.values()).join('\n');
// 在实际插件中,这里写入文件或通过 Webpack loader 处理
console.log('Extracted CSS:\n', css);
},
};
}
6. 工具选择对比
| 工具 | 适用场景 | API 风格 | 语言 |
|---|---|---|---|
@babel/* | JS/TS 转译 | Visitor | JS |
jscodeshift | 批量 Codemod | jQuery-like 链式 | JS |
ts-morph | TS 项目操作 | 高级封装 | TS |
TypeScript API | 类型感知转换 | 底层 API | TS |
recast | 保留格式的转换 | AST 操作 | JS |
magicast | 配置文件修改 | 高级封装 | JS |
ts-morph 简化 TS 操作
ts-morph 是 TypeScript Compiler API 的高级封装,API 更友好:
import { Project } from 'ts-morph';
const project = new Project();
const sourceFile = project.createSourceFile('temp.ts', `
export function add(a: number, b: number) {
return a + b;
}
`);
// 获取所有函数
const functions = sourceFile.getFunctions();
functions.forEach(fn => {
console.log(fn.getName()); // 'add'
console.log(fn.getReturnType().getText()); // 'number'
});
常见面试问题
Q1: 如何编写一个 Babel 插件?
答案:
Babel 插件是一个返回对象的函数,核心是 visitor:
// 插件:将 == 替换为 ===
export default function strictEqualPlugin(): PluginObj {
return {
name: 'strict-equal',
visitor: {
BinaryExpression(path) {
if (path.node.operator === '==') {
path.node.operator = '===';
}
if (path.node.operator === '!=') {
path.node.operator = '!==';
}
},
},
};
}
// 配置 babel.config.js
// { plugins: ['./plugins/strict-equal'] }
开发流程:
- 在 AST Explorer 观察目标代码的 AST 结构
- 确定要处理的节点类型
- 编写 visitor 方法
- 用
@babel/types构建新节点 - 测试:
@babel/core的transformSync方法验证
Q2: ESLint 自定义规则如何实现自动修复(Auto Fix)?
答案:
在 context.report() 中提供 fix 函数:
const rule: Rule.RuleModule = {
meta: {
fixable: 'code', // 必须声明!
},
create(context) {
return {
Literal(node) {
if (typeof node.value === 'string' && node.raw?.[0] === '"') {
context.report({
node,
message: '请使用单引号',
fix(fixer) {
// fixer 方法:
// replaceText / replaceTextRange — 替换
// insertTextBefore / insertTextAfter — 插入
// remove — 删除
return fixer.replaceText(node, `'${node.value}'`);
},
});
}
},
};
},
};
注意事项:
meta.fixable必须声明为'code'或'whitespace'fix函数返回fixer操作或操作数组- ESLint 会自动处理多次修复的冲突
Q3: @babel/types、@babel/template、@babel/traverse 各自的作用?
答案:
| 包 | 作用 | 使用场景 |
|---|---|---|
@babel/parser | 将源代码解析为 AST | 输入阶段 |
@babel/traverse | 遍历 AST,提供 path API | 访问/修改节点 |
@babel/types | AST 节点构建与类型判断 | 创建新节点、类型守卫 |
@babel/template | 用模板字符串创建 AST 片段 | 构建复杂代码结构 |
@babel/generator | AST → 源代码字符串 | 输出阶段 |
import * as t from '@babel/types';
import template from '@babel/template';
// @babel/types: 手动构建节点
const node = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('x'), t.numericLiteral(42)),
]);
// @babel/template: 用模板快速构建(更直观)
const buildRequire = template(`
const %%importName%% = require(%%source%%);
`);
const ast = buildRequire({
importName: t.identifier('React'),
source: t.stringLiteral('react'),
});
Q4: Codemod 和手动替换相比有什么优势?
答案:
| 维度 | Codemod(AST) | 正则/全局替换 |
|---|---|---|
| 准确性 | 理解语法结构,精确匹配 | 可能误匹配注释/字符串 |
| 安全性 | 类型感知,不破坏代码 | 可能破坏代码结构 |
| 复杂转换 | 支持条件判断、作用域分析 | 难以处理复杂场景 |
| 可复用 | 写一次,跑遍全项目 | 每次手动操作 |
| 可测试 | 可编写单元测试 | 难以测试 |
适用场景:框架大版本升级(React class → Hooks)、API 重命名、废弃用法迁移。
Q5: 如何在编译阶段做性能优化?
答案:
编译时优化的常见手段:
- 静态分析与 Tree Shaking:分析
import/export删除未使用代码 - 常量折叠:
1 + 2编译时直接算出3 - 死代码删除:
if (false) { ... }直接移除 - 模板预编译:Vue template → render 函数(避免运行时编译)
- CSS 原子化:编译时提取原子类(Tailwind、UnoCSS)
- 宏替换:
process.env.NODE_ENV→"production"后配合 Tree Shaking - 静态提升:Vue 3 将静态节点提升到渲染函数外部
// Babel 常量折叠示例
// 输入
const x = 1 + 2 + 3;
const y = 'hello' + ' ' + 'world';
// 编译后
const x = 6;
const y = 'hello world';
相关链接
- AST Explorer — 在线观察 AST
- Babel 插件开发手册
- ESLint 自定义规则开发
- jscodeshift — Facebook Codemod 工具
- ts-morph — TypeScript AST 操作库
- 编译原理基础 — 编译流程与 AST 基础