Prettier YAML 格式化:保留 anchor 与 tag 的原始顺序(含源码实现解析)
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
本篇基于 Prettier 仓库中未发布的 YAML 变更记录 changelog_unreleased/yaml/19599.md(PR #19599,作者 @fisker),讲解一个针对 YAMLyamlparser 的格式化行为改进:Prettier 现在会保留源文件中 anchor(&anchor)与 tag(!!str等)的书写顺序,而不是把它们强制归一化为固定顺序。读完本文,你可以理解该改动的动机、验证其前后行为差异,并定位到 printer-yaml.js 中实现该行为的具体代码与对应的测试用例。
背景:YAML 的节点属性允许任意顺序
在 YAML 语法中,tag(如!!str、!mytype)和 anchor(如&myanchor)都属于节点的“属性”(node property),规范允许两者以任意先后顺序出现,例如下面两种写法都是合法的:
foo: &anchor1 !!str Value1 # anchor 在前 bar: !!str &anchor2 Value2 # tag 在前但 Prettier 的稳定版(stable)在输出时采用的是固定顺序:无论源文件里写的是哪个在前,输出都会被归一化为!!str &anchor(tag 在前)。对于第二个示例,稳定版的输出结果如下:
# Input foo: &anchor1 !!str Value1 bar: !!str &anchor2 Value2 # Prettier stable(固定顺序,改变了源文件的书写顺序) foo: !!str &anchor1 Value1 bar: !!str &anchor2 Value2注意第一行:输入是 anchor 在前、tag 在后,稳定版却把顺序“反转”成了 tag 在前。虽然两种顺序在语义上等价,但对锚点/标签的使用者来说,格式化结果与手写代码不一致会造成无意义的 diff,也违背了格式化器“保持源码风格”的原则。
改动后的行为:顺序与源文件保持一致
PR #19599 之后的 Prettier main 分支会完整保留原始书写顺序,同一份输入的输出变为:
# Prettier main(保留源文件顺序) foo: &anchor1 !!str Value1 bar: !!str &anchor2 Value2两行输出的属性顺序均与输入完全一致,格式化不再引入任何属性重排。变更记录中的完整对比示例(含<!-- prettier-ignore -->防止 diff 被重排)可以在 changelog_unreleased/yaml/19599.md 中原样查看。
源码实现:按源位置排序 tag 与 anchor
这个改动的核心在 src/language-yaml/printer-yaml.js 的genericPrint函数中:
const tagAndAnchor = ["tag", "anchor"].filter((property) => node[property]); if (tagAndAnchor.length > 1) { tagAndAnchor.sort( (propertyA, propertyB) => locStart(node[propertyA]) - locStart(node[propertyB]), ); } for (const [index, property] of tagAndAnchor.entries()) { if (index > 0) { parts.push(" "); } parts.push(print(property)); }实现思路可以拆解为三点:
- 收集属性:从当前节点上取出存在的
tag和anchor子节点,过滤掉不存在的属性。 - 按源位置排序:当两者同时存在时,用
locStart(来自 src/language-yaml/loc.js)比较两个属性节点在源文件中的起始偏移量,升序排列——也就是“谁在源文件中出现得早,谁就先被打印”。这正是“保留原始顺序”的关键,取代了此前固定的tag, anchor顺序。 - 用空格连接打印:按排序后的顺序依次打印,中间插入一个空格分隔。
配合后续的属性打印逻辑,顺序得以端到端地保持:
case "tag": return options.originalText.slice( node.position.start.offset, node.position.end.offset, ); case "anchor": return ["&", node.value];见 printer-yaml.js。其中tag节点直接以原文切片输出(保留自定义 tag 如!mytype的原始拼写),anchor则以&加值的方式输出。AST 本身由第三方解析器yaml-unist-parser生成,见 src/language-yaml/parser-yaml.js,解析时启用了uniqueKeys: false选项以容忍重复键。
此外,从源码结构看,tagAndAnchor排序后的部分还会影响后续分隔符的打印:对于sequence/mapping类型的节点,属性后接hardline(换行),其他节点接空格,见 printer-yaml.js。这意味着即使源文件中 anchor 与 tag 分别写在两行(YAML 允许属性各占一行),输出也会合并到同一行,但先后顺序依然遵循源文件。
测试用例:多组合的 tag/anchor 场景
仓库中的格式化测试覆盖了大量 tag 与 anchor 的组合场景,可以直接运行tests/format下的测试来验证本改动的效果。
同一段落内多种组合
tests/format/yaml/spec/various-combinations-of-tags-and-anchors.yml 包含了 8 个文档片段,涵盖了 tag/anchor 同行、分行、属性作用于 key 或整个 value 等各种情况:
--- &a1 !!str scalar1 --- !!str &a2 scalar2 --- &a3 !!str scalar3 --- &a4 !!map &a5 !!str key5: value4 --- a6: 1 &anchor6 b6: 2 --- !!map &a8 !!str key8: value7 --- !!map !!str &a10 key10: value9 --- !!str &a11 value11对应的快照输出(见 format.test.js.snap)显示:分行书写的属性被合并到同一行,但顺序与源文件一致——&a1\n!!str输出为&a1 !!str scalar1(anchor 在前),!!str\n&a2输出为!!str &a2 scalar2(tag 在前)。这组用例是验证“保留顺序”行为最全面的回归测试。
其他相关场景
- node-anchor-and-tag-on-seperate-lines.yml:anchor 与 tag 分行且作用于块映射的场景;
- tag-key.yml:tag 作用在显式映射的 key 上(
? !!tag key形式); - anchor-with-unicode-character.yml:anchor 值包含 Unicode 字符时的处理;
- 规范示例类用例如 spec-example-6-29-node-anchors.yml、spec-example-2-23-various-explicit-tags.yml 等,用于确保改动不与 YAML 规范示例的既有行为冲突。
适用前提与小结
- 适用范围:该行为仅作用于 Prettier 的
yamlparser(语言模块位于 src/language-yaml/),对应.yml/.yaml文件;对 YAML front matter 嵌入在其他格式(如 Markdown)中的场景,同样经由该 printer 输出。 - 前提:anchor 与 tag 同时存在于同一节点时才会触发排序逻辑;只有其中一个属性时不受影响。
- 本质:这是一次“格式化保真度”改进——Prettier 稳定版会把属性顺序归一化为固定形态,main 分支改为以源文件的实际顺序为准,从而减少无意义的 diff。
小结:从 changelog_unreleased/yaml/19599.md 的变更记录、printer-yaml.js 中基于locStart的排序实现,到tests/format/yaml/spec/下成组的回归测试,可以看到这是一个改动面小、验证充分的 YAML 格式化细节修复。如果你使用 anchor 与 tag 混合书写的 YAML 配置(如带类型标注的 schema、带别名引用的配置片段),升级到包含该改动的版本后,格式化结果将与手写顺序保持一致。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考