基于 AST 的 lint 与自动修复 + 代码生成(收官)
本节目标
- 用 AST 写一个真正的 lint 规则:发现
==提示用===,发现裸console.log报警 - 实现 --fix 自动修复(
==→===) - 用 @babel/generator 把改好的 AST 重新生成代码(收官“生成”这一步)
- 串起全课:Parse → Transform → Generate
ESLint 的本质就是“遍历 AST + 按规则报错 + 可选自动修复”。我们用 30 行实现它的核心。最后用 @babel/generator 把 AST 画回代码,正好补齐第 2 节那条流水线的第三步。
动手:lint + 自动修复 + 生成代码
// 运行环境:Node.js 18+,需安装: npm install @babel/parser @babel/traverse @babel/generator
// 运行方式:把整段保存为 app-l10.cjs,终端执行 node app-l10.cjs
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const code = [
"function check(a, b) {",
" if (a == b) {",
" console.log('相等')",
" }",
" return a == b",
"}"
].join('\n')
const ast = parser.parse(code)
const problems = []
traverse(ast, {
// 规则 1:发现 == 建议用 ===(可自动修复)
BinaryExpression(path) {
if (path.node.operator === '==') {
problems.push({ line: path.node.loc.start.line, msg: '建议用 === 代替 ==', fixable: true, path })
}
},
// 规则 2:裸 console.log 报警(不自动修)
CallExpression(path) {
const callee = path.node.callee
if (callee.type === 'MemberExpression' &&
callee.object.name === 'console' && callee.property.name === 'log') {
problems.push({ line: path.node.loc.start.line, msg: '生产环境请移除 console.log', fixable: false })
}
}
})
// 打印 lint 报告
console.log('=== Lint 报告 ===')
problems.forEach((p) => console.log('第 ' + p.line + ' 行:' + p.msg + (p.fixable ? '(可修复)' : '')))
// 自动修复:把 == 改成 ===(通过 path.replaceWith 重建节点)
problems.filter((p) => p.fixable).forEach((p) => {
const n = p.path.node
p.path.replaceWith(
require('@babel/types').binaryExpression('===', n.left, n.right)
)
})
// 生成:把(可能修复过的)AST 重新画回代码字符串
const output = generate(ast, {}, code).code
console.log('\n=== 修复并生成后的代码 ===')
console.log(output)
// 大致输出:
// function check(a, b) {
// if (a === b) {
// console.log('相等')
// }
// return a === b
// }全课串讲:你现在已经掌握的东西
- Parse(解析):手写 tokenizer/Parser 理解原理;真实用 acorn / @babel/parser。
- Transform(转换):visitor + Path +
@babel/types在树上增删改;写插件、写 lint 都是它。 - Generate(生成):
@babel/generator把 AST 画回代码,并生成 source map。 - 真实世界:Babel、TypeScript、ESLint、Prettier、webpack、Rollup、Vue 模板编译……底层全是 AST。
下一步建议
- 打开 AST Explorer,把本课每个例子粘进去对照节点类型。
- 读
@babel/parser/@babel/traverse的官方 README,认全节点类型。 - 给本课的 lint 加一条规则(例如“禁止
if (x = 1)的赋值误用”),亲手练手。 - 想继续深入编译原理,可回看本站《AST 实操:解释器与编译器》课程(手写求值器 / 迷你编译器)。
名词解释
lint(静态检查):在不运行代码的前提下,扫描源码找出“可能有问题”的写法(比如用 ==、留下裸 console.log)。它的本质就是本节做的三件事:遍历 AST + 按规则报错 + 可选自动修复。ESLint 的全部魔法都在这三步里。
自动修复(Autofix):lint 工具发现“可修复问题”时,直接改写 AST 并重新生成代码的能力。本节的 == → === 就是一例——通过 path.replaceWith 重建节点实现,对应 ESLint 的 --fix。
课后练习
练习 1:本节的 lint 对下面代码会报告几处问题?分别是什么?
function check(a, b) { if (a == b) { console.log('x') } return a == b }答案:共 3 处——两处
==被建议改用===(标记为可修复);一处裸console.log报警(标记为不可修复)。
练习 2:自动修复后,用 @babel/generator 生成的代码里,a == b 和 console.log 分别变成了什么?
答案:两处
a == b都被修复成a === b;console.log('x')保持不变(因为规则 2 标记为不可修复,只报警不修)。
本节小结(观点与完整描述)
收官一节,你把整条流水线真正跑圆了:Parse → Transform → Generate,而“lint + 自动修复”正是这条流水线最接地气的应用。你写的 30 行代码,本质上就是 ESLint 的核心——遍历 AST、按规则往 problems 里塞报告、对可修复的用 path.replaceWith 重建节点、最后用 @babel/generator 把树重新画回代码。值得记住的是“可修复”与“只报警”的区分:能不能自动改,取决于你愿不愿意(以及安不安全)替用户做决定。当你回头看全课会发现,AST 从来不是玄学,它就是“代码的树状结构 + 一套在该树上操作的标准工具”;而 Parse / Transform / Generate 三步,加上 visitor / path / t 三件套,已经足够你读写世界上绝大多数“处理代码的工具”。下一步,就把这些能力用在你自己的项目里。