基于 AST 的 lint 与自动修复 + 代码生成(收官)

本节目标

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
// }

全课串讲:你现在已经掌握的东西

下一步建议

  1. 打开 AST Explorer,把本课每个例子粘进去对照节点类型。
  2. 读 @babel/parser / @babel/traverse 的官方 README,认全节点类型。
  3. 给本课的 lint 加一条规则(例如“禁止 if (x = 1) 的赋值误用”),亲手练手。
  4. 想继续深入编译原理,可回看本站《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 三件套,已经足够你读写世界上绝大多数“处理代码的工具”。下一步,就把这些能力用在你自己的项目里。