AST:前端常见语法树工具链一览(TypeScript / Babel / ESLint / SWC)
前端世界里“有 AST 的东西”非常多:TypeScript 自己的一套,Babel / ESTree 一套,ESLint 又包了一层,近几年还有 SWC 之类的“编译器即基础设施”。
它们名字听起来都差不多,但适合用在什么场景,其实差异挺大。
几个常见名字先对上号
在 JS / TS 这块,最常打交道的大概是这几类:
- TypeScript Compiler API
- TS 官方编译器内部那一整套 API,可以直接拿来解析
.ts/.tsx。 - AST 节点类型紧贴 TypeScript 语言特性,本身就带类型系统和符号表那一套东西。
- TS 官方编译器内部那一整套 API,可以直接拿来解析
- Babel + ESTree 家族
- Babel 解析出来的 AST 近似遵循 ESTree 规范,主要面向 JavaScript / JSX / 各种提案语法。
- 很多工具(ESLint、Prettier 等)都选择以 ESTree 为“共同语言”。
- ESLint
- 自己不实现解析器,而是“站在 AST 上面”的一套规则框架。
- 默认用
espree(也是基于 ESTree),也可以替换成@typescript-eslint/parser、babel-eslint等。
- SWC / esbuild 这一类编译器
- 更偏“高性能构建器”,内部当然有 AST,但对外暴露的 AST 能力相对有限(更多是内置 transform)。
- 用来做“打包 + 简单语法转换”非常合适,但要写精细规则和重构工具时,体验还不如 TS/Babel 这类编译器家族。
实际做东西时,基本上是在这几种之间做取舍。
TypeScript AST:TS 项目里的“首选工具”
在一个以 TypeScript 为主的代码仓库里,如果目标是做和类型信息强相关的事情,TypeScript Compiler API 很难绕开。
- 优势
- 直接支持
.ts/.tsx,语法支持永远是最新的; - AST 节点和 TS 语言特性一一对应(比如
InterfaceDeclaration、TypeAliasDeclaration、AsExpression等); - 可以顺手拿到 TypeChecker / Symbol / Type,做静态分析、重构、IDE 功能时非常有用。
- 直接支持
- 典型用法
- 写一个 CLI 工具,在整个仓库里找出所有
any/@deprecated/ 自定义 Decorator 使用; - 做重构:批量改 import 路径、改接口名字、拆模块等;
- IDE / LSP 里的语言服务器,实现补全 / 跳转 / 重命名等。
- 写一个 CLI 工具,在整个仓库里找出所有
对 Theia 这条线来说:很多语言服务器(尤其是 TypeScript / JavaScript)内部就是直接用 TS 的 AST + 类型系统在干活。
不过 TS Compiler API 的学习曲线会稍陡一些:
API 面很大,类型相关的部分也比较“编译器味”,一开始用的时候需要多查文档/源码。
Babel / ESTree:JS 世界的“通用语法树语言”
如果场景是“以 JavaScript 为主,语言特性/提案很多”,或者要和各种既有工具打交道,Babel + ESTree 往往是更务实的选择。
- Babel 本身
- 负责解析 + 转换 + 生成:
@babel/parser把源码变 AST,@babel/traverse遍历并修改,@babel/generator再打印回代码。 - 生态里已经有大量可用插件(class 属性、装饰器、各种提案、React 相关语法等)。
- 负责解析 + 转换 + 生成:
- ESTree 规范
- 把 JavaScript 语法树结构标准化,规定了
Program/FunctionDeclaration/CallExpression等节点应该长什么样; - Babel AST 在此基础上略有扩展,但整体风格是兼容的。
- 把 JavaScript 语法树结构标准化,规定了
- 适合做的事情
- 对 JS / JSX 代码做各种语法级 transform(比如编译器插件、代码 mod 工具);
- 和 ESLint / Prettier 等其它工具共享 AST 结构,减少心智负担。
缺点是:
如果你需要非常强的类型信息(TS 的 TypeChecker 那种),就得额外接入 TypeScript 或 @typescript-eslint 那一层。
ESLint:站在 AST 上的一层“规则框架”
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
SWC、esbuild 这类工具,内部都有自己的 AST 设计,但对外暴露出来的一般只是:
- 一些基础的 transform 能力(比如编译 TS/JSX、做 tree-shaking、压缩);
- 少量插件或 hook 接口,让你在编译过程中插几刀。
它们的特点更像是:
- 强项是“快”:Rust / Go 实现,对大项目的构建速度有明显优势;
- 适合作为打包器/转换器:比如取代 Babel/webpack 的一部分工作;
- 不太适合作为“通用 AST 玩具”:要写细粒度的规则、重构工具时,生态和文档都没那么友好。
在做 IDE / 语言服务器、或者需要精细代码分析的时候,通常还是会回到 TypeScript / Babel / ESLint 这一家子上来。