Prettier 的第一个动作不是分析代码"格式哪里不对",而是把代码理解成结构化的数据。这个理解过程就是解析(Parsing),它依赖特定语言的解析器把代码字符串转换成抽象语法树(AST)。
AST 是计算机科学中一个经典概念。简单说,它用树状结构表示代码的语法关系。每段代码都能被唯一地表示为一棵 AST(假设代码语法正确),这棵树描述了代码的每一个语法元素是什么、它们之间的嵌套关系是什么。
举个例子,const x = 1 + 2; 这行代码在 AST 中的表示(简化后)是一个"变量声明"节点,包含:
constx1,右操作数是 2注意 AST 中没有任何关于空格、换行、分号的信息。const x=1+2;、const x = 1 + 2 ;、const\nx\n=\n1\n+\n2\n; 这三种写法产生完全相同的 AST。这正是解析阶段的关键价值——它把代码从"文本"抽象成"结构",把格式差异彻底消除了。
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 的构建。
代码中的注释在解析阶段是一个特殊的处理对象。注释不属于 AST 的节点层次结构——它们不嵌套在函数声明、变量声明等语法节点内,而是作为"附属信息"(attachment)挂载在最近的语法节点上。
Prettier 的解析器会记录每个注释的位置信息(起始行号、列号),然后在 Doc 构建阶段把这些注释插入到 Doc IR 中适当的位置。插入的逻辑是:把注释放在它"最合理"的上下文中——行尾注释保持在行尾,独立注释保持独立一行,前置注释保持在对应代码之前。
注释处理是格式化中最棘手的部分之一。因为注释不像代码有明确的语法结构,它们可以出现在代码的几乎任何位置。Prettier 的策略是尽量保持注释的原始位置和格式(不改变注释内部的内容),只调整注释相对于代码的位置。
这就是为什么你偶尔会看到 Prettier 格式化后,注释的位置不太理想——它已经做了最好的判断,但"最合理"和"你最想要的"可能不完全一致。在这种情况下,你可以手动调整注释位置,只要代码本身符合 Prettier 的格式就行。
Prettier 在格式化过程中会生成源映射(source map),记录输出文件的每个位置对应输入文件的哪个位置。这个源映射主要被编辑器集成使用——当你开启"保存时格式化"后,光标位置能正确保持在格式化后的对应位置,而不是跳到文件开头。这看起来是一个小细节,但对编码体验的影响很大。
如果没有源映射,每次保存时光标跳来跳去,开发者会本能地感到不安——"格式化是不是改掉了什么"。有了源映射,光标只是微微移动(因为格式化改变了行数和位置),这种平滑的体验让"保存时格式化"真正变成一种无感的自动化操作。
解析阶段虽然不像 Doc 构建和打印那样有复杂的算法决策,但它是整个流程的基石。解析器提供的 AST 质量直接影响后续阶段的效果——如果 AST 不准确或不完整,Doc 构建就无从谈起。这也是为什么 Prettier 对解析器的选择和版本管理非常谨慎,对不支持的语言不做格式化(而不是用粗糙的解析器凑合)。