跳到主要内容

AST 实战应用

问题​

如何利用 AST 进行代码转换?Babel 插件和 ESLint 规则如何开发?

面试速答版

如何利用 AST 进行代码转换? 核心是「解析→修改→生成」三步走:

  • 用 @babel/parser 把源码解析成 AST。
  • 用 @babel/traverse 遍历,访问到目标节点时增/删/改。
  • 用 @babel/generator 把 AST 打印回代码,配合 source-map 保留行列映射。
  • 工具选型:Babel 生态最完整,Recast 适合保留原格式做 Codemod,jscodeshift 是 Facebook 出的 Codemod 工具。

Babel 插件怎么写? 本质是返回一个 visitor 对象,按节点类型注册回调:

  • 比如 CallExpression(path) { ... } 会拦截所有函数调用。
  • 在回调里通过 path.node 拿当前节点、path.scope 查作用域、@babel/types 构造新节点。
  • 常用操作:path.remove() 删节点、path.replaceWith() 替换、path.insertAfter() 插入。
  • 典型场景:移除 console.log、自动注入埋点、按需引入、JSX 转换。

ESLint 规则和 Babel 插件有啥区别? 两者都基于 AST,但目标不同:

  • ESLint 用 espree 解析,主要做静态检查,调用 context.report() 上报问题,可选给 fix 函数实现自动修复。
  • Babel 是代码转换,直接改 AST 输出新代码。
  • ESLint 还能拿到 scope manager、TypeScript Parser 提供类型信息(@typescript-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 转译VisitorJS
jscodeshift批量 CodemodjQuery-like 链式JS
ts-morphTS 项目操作高级封装TS
TypeScript API类型感知转换底层 APITS
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'] }

开发流程:

  1. 在 AST Explorer 观察目标代码的 AST 结构
  2. 确定要处理的节点类型
  3. 编写 visitor 方法
  4. 用 @babel/types 构建新节点
  5. 测试:@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/typesAST 节点构建与类型判断创建新节点、类型守卫
@babel/template用模板字符串创建 AST 片段构建复杂代码结构
@babel/generatorAST → 源代码字符串输出阶段
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: 如何在编译阶段做性能优化?​

答案:

编译时优化的常见手段:

  1. 静态分析与 Tree Shaking:分析 import/export 删除未使用代码
  2. 常量折叠:1 + 2 编译时直接算出 3
  3. 死代码删除:if (false) { ... } 直接移除
  4. 模板预编译:Vue template → render 函数(避免运行时编译)
  5. CSS 原子化:编译时提取原子类(Tailwind、UnoCSS)
  6. 宏替换:process.env.NODE_ENV → "production" 后配合 Tree Shaking
  7. 静态提升:Vue 3 将静态节点提升到渲染函数外部
// Babel 常量折叠示例
// 输入
const x = 1 + 2 + 3;
const y = 'hello' + ' ' + 'world';

// 编译后
const x = 6;
const y = 'hello world';

相关链接​