跳到主要内容

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 迁移componentWillMountuseEffect

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/coretransformSync 方法验证

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';

相关链接