2.2解析阶段与抽象语法树


2.2 解析阶段与抽象语法树

解析器的角色

Prettier 的第一个动作不是分析代码"格式哪里不对",而是把代码理解成结构化的数据。这个理解过程就是解析(Parsing),它依赖特定语言的解析器把代码字符串转换成抽象语法树(AST)。

AST 是计算机科学中一个经典概念。简单说,它用树状结构表示代码的语法关系。每段代码都能被唯一地表示为一棵 AST(假设代码语法正确),这棵树描述了代码的每一个语法元素是什么、它们之间的嵌套关系是什么。

举个例子,const x = 1 + 2; 这行代码在 AST 中的表示(简化后)是一个"变量声明"节点,包含:

  • 声明类型:const
  • 变量名标识符:x
  • 初始值:一个二元加法表达式,左操作数是 1,右操作数是 2

注意 AST 中没有任何关于空格、换行、分号的信息。const x=1+2;const x = 1 + 2 ;const\nx\n=\n1\n+\n2\n; 这三种写法产生完全相同的 AST。这正是解析阶段的关键价值——它把代码从"文本"抽象成"结构",把格式差异彻底消除了。

Prettier 使用的解析器

Prettier 本身不是解析器,它是一个格式化框架,解析工作委托给外部解析器完成。不同语言使用不同的解析器:

语言 解析器 说明
JavaScript @babel/parser Babel 的解析器,支持最新的 ECMAScript 语法
TypeScript @typescript-eslint/typescript-estree TypeScript 解析器,处理类型注解
CSS postcss PostCSS 的解析器,处理各种 CSS 语法
HTML angular-html-parser Angular 团队的 HTML 解析器
JSON 内置 JSON.parse 利用 JavaScript 原生 JSON 解析
Markdown remark-unified Unified 生态的 Markdown 解析器
GraphQL graphql GraphQL 官方解析器
YAML yaml / yaml-unist-parser YAML 解析器
Vue Single-File vue-eslint-parser 处理 .vue 文件中的 template/script/style 块

Prettier 通过解析器注册机制来管理这些语言支持。核心包本身只包含 JavaScript/TypeScript 的支持,其他语言的解析器作为内置或外部插件加载。这也是 Prettier 能通过插件扩展新语言的原因——你只需要写一个解析器,把它注册到 Prettier 的解析器表里就行。

解析失败处理

当 Prettier 遇到语法错误的代码时,解析器会报错,Prettier 会终止格式化并输出错误信息。这看起来是限制,但其实是合理的设计——如果代码连语法都不正确,Prettier 无法保证格式化的结果是否保持了原始语义。

实践中,这种情况通常出现在开发过程中——你正在写一段代码,还没写完就想保存看看效果。Prettier 报错是正常的,你只需要补全代码后重新保存即可。

对于已经存在语法错误的遗留代码,Prettier 的建议是先手动修复语法错误,或者把相关文件加入 .prettierignore。这听起来不近人情,但考虑到 Prettier 的设计前提——"语法正确的代码,格式化后仍然正确"——这个限制是必要的。

如果解析器报告了错误但代码实际上是合法的(比如使用了非常新的语法提案),可能需要更新 Prettier 或相关解析器版本来获得支持。这在第五章会讨论。

解析阶段的技术细节

解析器的核心工作包含两个子步骤:词法分析(Lexing)和语法分析(Parsing)。

词法分析把代码字符串拆分成"词法单元"(token)序列。以 const x = 10; 为例,词法分析产生:

[Keyword: const, Identifier: x, Punctuator: =, Numeric: 10, Punctuator: ;]

语法分析把词法单元序列组织成 AST 树。根据语言的文法规则(grammar),词法单元被分配到树的不同位置:

Program └─ VariableDeclaration ├─ kind: const └─ declarations[0] └─ VariableDeclarator ├─ id: Identifier(x) └─ init: NumericLiteral(10)

Prettier 接手的是这棵最终的 AST 树,不需要关心词法分析的过程。它只需要知道"这个节点的类型是什么"、"它有哪些子节点",就能开始 Doc IR 的构建。

图 2-2:从源码到 AST 的两步解析过程

注释的处理

代码中的注释在解析阶段是一个特殊的处理对象。注释不属于 AST 的节点层次结构——它们不嵌套在函数声明、变量声明等语法节点内,而是作为"附属信息"(attachment)挂载在最近的语法节点上。

Prettier 的解析器会记录每个注释的位置信息(起始行号、列号),然后在 Doc 构建阶段把这些注释插入到 Doc IR 中适当的位置。插入的逻辑是:把注释放在它"最合理"的上下文中——行尾注释保持在行尾,独立注释保持独立一行,前置注释保持在对应代码之前。

注释处理是格式化中最棘手的部分之一。因为注释不像代码有明确的语法结构,它们可以出现在代码的几乎任何位置。Prettier 的策略是尽量保持注释的原始位置和格式(不改变注释内部的内容),只调整注释相对于代码的位置。

这就是为什么你偶尔会看到 Prettier 格式化后,注释的位置不太理想——它已经做了最好的判断,但"最合理"和"你最想要的"可能不完全一致。在这种情况下,你可以手动调整注释位置,只要代码本身符合 Prettier 的格式就行。

源映射(Source Map)的生成

Prettier 在格式化过程中会生成源映射(source map),记录输出文件的每个位置对应输入文件的哪个位置。这个源映射主要被编辑器集成使用——当你开启"保存时格式化"后,光标位置能正确保持在格式化后的对应位置,而不是跳到文件开头。这看起来是一个小细节,但对编码体验的影响很大。

如果没有源映射,每次保存时光标跳来跳去,开发者会本能地感到不安——"格式化是不是改掉了什么"。有了源映射,光标只是微微移动(因为格式化改变了行数和位置),这种平滑的体验让"保存时格式化"真正变成一种无感的自动化操作。

解析阶段虽然不像 Doc 构建和打印那样有复杂的算法决策,但它是整个流程的基石。解析器提供的 AST 质量直接影响后续阶段的效果——如果 AST 不准确或不完整,Doc 构建就无从谈起。这也是为什么 Prettier 对解析器的选择和版本管理非常谨慎,对不支持的语言不做格式化(而不是用粗糙的解析器凑合)。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U