Back

深入 AST:工具如何理解代码

深入 AST:工具如何理解代码

每当 ESLint 在你运行代码之前标记出问题,或者 Prettier 在你保存文件的瞬间重新格式化混乱的函数时,底层都在发生一些精确的操作。这些工具并不是将你的源代码作为文本来读取,而是将其作为结构化的树来读取。理解这棵树——抽象语法树(Abstract Syntax Tree, AST)——可以解释几乎所有现代开发工具的工作原理。

核心要点

  • AST 是代码结构的树形表示,通常会剔除表面层级的细节,如空格和分组括号。
  • Linter、格式化工具和编译器都通过将源代码解析为 AST、遍历它并在每个节点应用规则或转换来工作。
  • 访问者模式是主流方法:工具为特定节点类型注册处理器,并在遍历树时做出响应。
  • 具体语法树(Concrete Syntax Tree, CST)保留每个 token,更适合需要增量解析和完全保真表示的编辑器。
  • 基于 Rust 的工具如 Biome 和 Oxc 使用相同的解析-遍历-执行模型,但性能显著超越许多基于 JavaScript 的解析器。

什么是抽象语法树?

当工具解析你的源代码时,会经历两个阶段。

首先,词法分析器(lexer,或称 tokenizer)将原始文本分解为 token:关键字、标识符、运算符、标点符号。然后解析器(parser)获取这些 token 并构建一棵表示代码语法结构的树。

这棵树就是 AST。之所以称为”抽象”,是因为它通常会丢弃表面层级的细节——空格、大部分标点符号以及纯粹用于分组的括号——同时保留语义上有意义的内容。

以这行代码为例:

const x = 5 + 3

解析器会生成类似这样的结构:

{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [{
    "type": "VariableDeclarator",
    "id": { "type": "Identifier", "name": "x" },
    "init": {
      "type": "BinaryExpression",
      "operator": "+",
      "left":  { "type": "Literal", "value": 5 },
      "right": { "type": "Literal", "value": 3 }
    }
  }]
}

每个节点都有一个 type。子节点嵌套在父节点内部。整个程序成为一棵根节点为 Program 的树。

你可以使用 AST Explorer 直接探索这一过程,它允许你粘贴任何 JavaScript 或 TypeScript 代码并实时查看生成的树结构。

Linter 和格式化工具如何使用 AST

一旦工具获得了 AST,就可以做一些有用的事情:遍历它

大多数工具使用访问者模式(visitor pattern)。你为特定的节点类型注册一个函数,遍历引擎每次遇到匹配的节点时就会调用该函数。

ESLint 正是这样工作的。每个 lint 规则都是一个访问者对象。当 ESLint 遍历 AST 时,它会在每个节点调用相关的规则处理器。例如,一个不允许使用 == 而要求使用 === 的规则,只需监听 BinaryExpression 节点并检查 operator 属性即可。

Babel 使用相同的方法进行代码转换。它将源代码解析为 AST,应用基于访问者的转换来修改节点,然后将修改后的树打印回源代码。这就是它如何将现代 JavaScript 语法编译为旧版本等价代码的方式。

Prettier 采用了不同的角度。它将代码解析为 AST,丢弃所有原始格式,然后根据自己的布局规则重新打印树。AST 是真实来源——而不是原始文本。

AST vs. CST:当结构更重要时

AST 会省略语法上必需但语义上冗余的 token。具体语法树(Concrete Syntax Tree, CST) 保留源代码的完整语法结构,保留 token 及其位置。

Tree-sitter 生成针对增量解析优化的具体语法树。因为它只需更新受编辑影响的树的部分,所以非常适合现代编辑器(如 Neovim 和 Zed)中的语法高亮、代码折叠和结构化编辑。

向高性能解析的转变

JavaScript 生态系统中的新工具正在大幅提升解析性能。像 BiomeOxc 这样的项目使用 Rust 实现,并从头开始构建自己的解析器和 AST 表示。

它们以通常优于基于 JavaScript 的解析器的速度处理 linting 和格式化,同时支持现代语法,如 import attributes 和最新的 TypeScript 特性。

底层模型是相同的——解析为树、遍历它、应用分析或转换——但实现针对规模进行了优化。

结论

无论你是在编写自定义 ESLint 规则、使用 jscodeshift 构建 codemod,还是只是想理解工具为什么会有某种行为,AST 都是正确的切入点。它是每个严肃的开发工具所构建的结构化表示。一旦你能够阅读语法树,linter、格式化工具和编译器的行为就不再像魔法一样神秘了。

常见问题

日常使用不需要。这些工具开箱即用,具有合理的默认配置。但如果你想编写自定义 lint 规则、构建 codemod 或调试意外的工具行为,理解 AST 如何表示你的代码会让你对工具在每个步骤实际执行的操作有清晰的心智模型。

AST 会剔除语法上必需但语义上冗余的 token,如逗号、括号和空格。CST 保留源代码的完整语法结构。CST 在需要完全保真表示和增量解析的编辑器和 IDE 中更受青睐,而 AST 通常被 linter、编译器和格式化工具使用。

最简单的方法是访问 astexplorer.net 上的 AST Explorer。粘贴任何 JavaScript 或 TypeScript 代码片段,选择一个解析器如 acorn、babel 或 typescript,该工具会实时显示完整的树结构。它还支持其他语言,并允许你直接在浏览器中试验转换。

Rust 让这些工具能够直接控制内存分配和布局,避免垃圾回收暂停,并编译为高度优化的原生代码。这意味着解析、遍历和分析都运行得更快,在拥有数千个文件的大型代码库中尤其明显。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay