AST:语法树到底是什么,以及和日常开发有什么关系

这一篇先不讲编译原理大图谱,只把一件事说清楚:
AST(Abstract Syntax Tree,抽象语法树)到底是什么,以及作为一个写前端 / 工具 / IDE 的开发者,为什么值得关心它。

平时我们写的代码,本质上都是一段字符串。
比如:

1function greet(name: string) {
2  console.log('Hello ' + name);
3}

在编译器 / 语言服务眼里,第一步不是直接执行,而是先把这段字符串“读懂”——
拆成一个个 Token(词法单元),再根据语法规则,拼成一棵 语法树(AST)

用很粗糙的类比看一下它的结构,大概会长这样(伪结构):

 1FunctionDeclaration
 2  name: Identifier("greet")
 3  parameters:
 4    - Parameter
 5        name: Identifier("name")
 6        type: StringKeyword
 7  body:
 8    Block
 9      - ExpressionStatement
10          CallExpression
11            callee: PropertyAccessExpression
12              object: Identifier("console")
13              property: Identifier("log")
14            arguments:
15              - BinaryExpression
16                  left: StringLiteral("Hello ")
17                  operator: PlusToken
18                  right: Identifier("name")

几个关键信息:

  • 每个节点都有“类型”:比如 FunctionDeclarationIdentifierCallExpression
  • 节点可以嵌套形成树:函数里有参数列表、函数体,函数体里有表达式语句,表达式里又有调用表达式等;
  • 很多细节被抽象掉了:比如空格、换行在 AST 里通常不会一一出现,这也是“抽象(Abstract)”这个词的由来。

对我们来说,一个最重要的认知是:

AST 就是“代码结构化后的数据”,一旦有了它,我们就可以像操作普通对象/树结构一样去分析和改造代码。

为什么要关心 AST?因为你现在看到的几乎所有“稍微聪明一点”的编辑器功能,背后或多或少都在依赖它。

  • 语义级跳转 / 重命名
    • “跳转到定义”“查找所有引用”“重命名符号”这些能力,本质上都要先在 AST + 符号表里找到“这个标识符到底是谁”。
  • 智能补全
    • LSP 里 textDocument/completion 返回的那堆候选项,很多时候是基于 AST + 类型信息推出来的(比如当前作用域里有什么变量、当前对象有哪些成员)。
  • 诊断与 Lint
    • TypeScript 编译器报错、ESLint 的各种规则(比如禁止 any、禁止未使用变量),基本都是在遍历 AST 的时候顺便检查。
  • 自动修复 / 代码重构
    • “把所有 var 替换成 let”“把某个函数抽出来单独放到 utils 文件里”,这类操作如果靠正则会非常脆弱,而基于 AST 可以做到“语义级别”的安全修改。
  • 代码格式化
    • Prettier/TS 的格式化,其实也是:先解析成 AST,再按照一套风格规则重新“打印”回字符串。

如果往你现在这条线(Theia / Monaco / LSP)里塞一个标签,大概可以这么看:

  • Monaco:负责“文本编辑 + 光标 + 展示”;
  • LSP / Language Server:负责“语言智能”;
  • AST:是语言服务器在内部做各种分析时的“主战场数据结构”。

在完整的编译流程里,一般会看到一串术语:

1源码(字符串) → 词法分析(Tokens) → 语法分析(AST)
2  → 语义分析(类型检查等) → 中间表示(IR) → 机器码 / 字节码

这条链路如果展开,可以写一本书,但在做编辑器 / 工具的时候,其实不需要都掌握。

  • Token 更偏“按字符切段”function(){ 等都被拆出来;
  • AST 更偏“把这些片段重新组织成语义结构”:知道哪个函数、哪个参数、哪个调用。

至于后面的 IR / Bytecode,更多和“如何高效执行”有关,
而我们在 Theia / Monaco / LSP 这一层,更多是用 AST 做“理解和变换代码”的工作。

从工程实践的角度,如果你愿意多走一步理解 AST,大概会带来几类收益:

  • 写 Lint / 代码检查规则不会只停留在正则
    • 一旦会读 AST,你可以很自如地写出“只针对某种语法结构”的规则,而不是用易碎的字符串匹配。
  • 可以做一些“偏 IDE 化”的功能
    • 即便不接 LSP,你也可以在前端用 AST 做一些小工具:比如结构视图、简单重构、代码统计等。
  • 更好地看懂一些工具的源码和配置
    • TypeScript Compiler API、Babel 插件、ESLint 规则、SWC 等,都围绕 AST 在做文章。
    • 看得懂 AST,大部分“高级用法”其实就只剩下 API 细节问题了。
  • 和语言服务器 / LSP 互动时心里更有数
    • 当你在 Theia 里调一个 Language Server 的补全 / 重命名时,可以大致想象:
      • 对方先用 AST 定位节点;
      • 再基于类型信息算出补全项或所有引用位置;
      • 最后封装成 LSP 的响应发回来。

换句话说,AST 是“语言层面的 DOM”
DOM 是浏览器理解 HTML 的方式,AST 是编译器/语言服务理解代码的方式。