AST:前端常见语法树工具链一览(TypeScript / Babel / ESLint / SWC)

前端世界里“有 AST 的东西”非常多:TypeScript 自己的一套,Babel / ESTree 一套,ESLint 又包了一层,近几年还有 SWC 之类的“编译器即基础设施”。
它们名字听起来都差不多,但适合用在什么场景,其实差异挺大。

在 JS / TS 这块,最常打交道的大概是这几类:

  • TypeScript Compiler API
    • TS 官方编译器内部那一整套 API,可以直接拿来解析 .ts / .tsx
    • AST 节点类型紧贴 TypeScript 语言特性,本身就带类型系统和符号表那一套东西。
  • Babel + ESTree 家族
    • Babel 解析出来的 AST 近似遵循 ESTree 规范,主要面向 JavaScript / JSX / 各种提案语法。
    • 很多工具(ESLint、Prettier 等)都选择以 ESTree 为“共同语言”。
  • ESLint
    • 自己不实现解析器,而是“站在 AST 上面”的一套规则框架。
    • 默认用 espree(也是基于 ESTree),也可以替换成 @typescript-eslint/parserbabel-eslint 等。
  • SWC / esbuild 这一类编译器
    • 更偏“高性能构建器”,内部当然有 AST,但对外暴露的 AST 能力相对有限(更多是内置 transform)。
    • 用来做“打包 + 简单语法转换”非常合适,但要写精细规则和重构工具时,体验还不如 TS/Babel 这类编译器家族。

实际做东西时,基本上是在这几种之间做取舍。

在一个以 TypeScript 为主的代码仓库里,如果目标是做和类型信息强相关的事情,TypeScript Compiler API 很难绕开。

  • 优势
    • 直接支持 .ts / .tsx,语法支持永远是最新的;
    • AST 节点和 TS 语言特性一一对应(比如 InterfaceDeclarationTypeAliasDeclarationAsExpression 等);
    • 可以顺手拿到 TypeChecker / Symbol / Type,做静态分析、重构、IDE 功能时非常有用。
  • 典型用法
    • 写一个 CLI 工具,在整个仓库里找出所有 any / @deprecated / 自定义 Decorator 使用;
    • 做重构:批量改 import 路径、改接口名字、拆模块等;
    • IDE / LSP 里的语言服务器,实现补全 / 跳转 / 重命名等。

对 Theia 这条线来说:很多语言服务器(尤其是 TypeScript / JavaScript)内部就是直接用 TS 的 AST + 类型系统在干活。

不过 TS Compiler API 的学习曲线会稍陡一些:
API 面很大,类型相关的部分也比较“编译器味”,一开始用的时候需要多查文档/源码。

如果场景是“以 JavaScript 为主,语言特性/提案很多”,或者要和各种既有工具打交道,Babel + ESTree 往往是更务实的选择。

  • Babel 本身
    • 负责解析 + 转换 + 生成:@babel/parser 把源码变 AST,@babel/traverse 遍历并修改,@babel/generator 再打印回代码。
    • 生态里已经有大量可用插件(class 属性、装饰器、各种提案、React 相关语法等)。
  • ESTree 规范
    • 把 JavaScript 语法树结构标准化,规定了 Program / FunctionDeclaration / CallExpression 等节点应该长什么样;
    • Babel AST 在此基础上略有扩展,但整体风格是兼容的。
  • 适合做的事情
    • 对 JS / JSX 代码做各种语法级 transform(比如编译器插件、代码 mod 工具);
    • 和 ESLint / Prettier 等其它工具共享 AST 结构,减少心智负担。

缺点是:
如果你需要非常强的类型信息(TS 的 TypeChecker 那种),就得额外接入 TypeScript 或 @typescript-eslint 那一层。

ESLint 自己并不关心 AST 长什么样,只要求“有一个 ESTree 风格的语法树”。
所以它比较像是:

  • 提供一个“规则注册 + 遍历钩子”的框架;
  • 把解析器抽象成可插拔模块(parser);
  • 用 visitor 模式调用你的规则:
    • 比如你写 CallExpression(node) { ... },ESLint 在遍历 AST 时就会在每个调用表达式上把这个函数喊一遍。

在日常开发里,ESLint 比较适合用来做:

  • 团队级的代码风格 / 规范约束:比如禁止某些 API、约定 import 顺序、限制 any 使用;
  • 简单的语义检查:比如检查 React Hook 依赖、检查某些模式是否被误用;
  • 配合 --fix 做轻量自动修复

如果要做的是“对特定项目写一堆规则”,直接用 ESLint 的规则系统会比自己手撸 TS/Babel 遍历要省事很多。

在 Theia / LSP 这个背景下,可以把 ESLint 看成一类**“只跑在本地/CI 的语言智能”**:
不用协议通信,也不一定要接到编辑器里,但同样是基于 AST 做语义检查和(可选的)修复。

SWC、esbuild 这类工具,内部都有自己的 AST 设计,但对外暴露出来的一般只是:

  • 一些基础的 transform 能力(比如编译 TS/JSX、做 tree-shaking、压缩);
  • 少量插件或 hook 接口,让你在编译过程中插几刀。

它们的特点更像是:

  • 强项是“快”:Rust / Go 实现,对大项目的构建速度有明显优势;
  • 适合作为打包器/转换器:比如取代 Babel/webpack 的一部分工作;
  • 不太适合作为“通用 AST 玩具”:要写细粒度的规则、重构工具时,生态和文档都没那么友好。

在做 IDE / 语言服务器、或者需要精细代码分析的时候,通常还是会回到 TypeScript / Babel / ESLint 这一家子上来。