关于 formatter-development
面向 Biome 格式化器贡献者的开发技能,涵盖结构化格式化、令牌与注释处理、布局、测试和审查。
主要功能
- 01格式化器工作流与审查清单
- 02结构化 IR 布局指南
- 03令牌和注释保留规则
- 04抑制注释处理实践
- 05定向测试与幂等性检查
适用场景
- 01实现或调整格式化规则
- 02格式化时保留注释和令牌
- 03测试布局变更及格式化稳定性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ biomejs/ biome --skill formatter-development安装后在 Claude Code 中输入/formatter-development确认已加载。
详细介绍
格式化程序开发
将 crates/biome_formatter/CONTRIBUTING.md 和对应语言的格式化程序指南作为权威架构参考。选择 IR 原语前,请检查相邻节点的实现。
工作流
- 使用聚焦的内部格式化程序规范或
quick_test重现行为。 - 检查节点字段、注释和附近的格式化规则。
- 使用格式化程序 IR 实现最小的布局改动。
- 运行聚焦的格式化程序测试并检查快照。
- 在需要兼容时,与 Prettier 进行比较。
- 提交前进行格式化和 lint 检查。
打印规范
格式化程序的输出是对源代码树的结构化重写,而不是重新进行美化打印。每个未处理的节点、未处理的令牌、未检查的抑制项和未跟踪的替换,都应视为格式化程序缺陷,而非风格反馈。
- 格式化范围内的每个源令牌都必须且只能被处理一次。只有通过格式化、使用
format_removed移除、使用format_replaced替换,或通过执行上述任一操作的语言专用辅助函数处理令牌,才算处理了令牌。 - 每次都必须处理格式化范围内的每个源节点。优先使用
node.format()或node.format().with_options(...),这样节点自身的规则就会格式化节点、检查抑制项,并通过格式化程序基础设施处理注释。 - 父格式化程序不得仅为获得方便的布局而内联子节点的字段。应将布局决策移至子节点规则中,或将选项传入子节点格式化程序。
- 如果特定架构的格式化程序绕过了节点规则,就不得悄然跳过该节点。它必须检查
f.context().comments().is_suppressed(node.syntax());如果节点被抑制,则必须改为调用该语言的format_suppressed_node(...)辅助函数,而不是格式化节点主体。若不进行此检查,调试构建会因抑制覆盖检查失败,用户的抑制设置也可能被忽略。 - 必须通过类型化访问器打印源令牌。只有在格式化程序插入语法且不存在对应源令牌时,才使用
token("...")。 - 从输出中移除源令牌时,必须使用
format_removed(&token)。不得直接丢弃字段、将其绑定到_,或在未通过format_removed处理的情况下从write!中省略它;跳过的空白等附属文本仍属于该令牌。 - 替换源令牌的文本时,必须使用
format_replaced(&token, &replacement)。不得直接打印替换文本,也不得将token("...")用于替换文本,因为原令牌仍带有附属文本,必须标记为已处理。 - 自定义格式化必须由实现
Format<Context>的小型结构体承载。不得使用自由函数或存储的闭包值来承载格式化程序状态或布局约束。仅在一次性的局部衔接逻辑中,且该逻辑会立即写出时,才使用format_with。
节点规则
生成的节点规则实现 FormatNodeRule。在 fmt_fields 中:
- 显式解构生成的
*Fields类型; - 通过类型化访问器格式化源令牌;
- 只有在同一格式化路径的其他位置处理了字段,或有意通过
format_removed/format_replaced处理字段时,才使用_而非..;否则,节点或令牌字段上的_会导致树被丢弃; - 保留每个令牌和注释,除非格式化程序约定明确要求移除或替换它们;
- 将布局决策放在拥有这些决策的类型附近。
format_verbatim_* 方法会保留节点的源文本。只有在测试覆盖了该节点的有效、格式错误和带注释形式后,才可将原样格式化替换为结构化格式化。
令牌规则
格式化、替换或移除每个令牌。若令牌未得到处理,格式化程序测试会触发 panic,以防止源代码意外丢失。
替换令牌时使用 format_replaced,移除令牌时使用 format_removed。
临时格式化
尽可能通过 node.format() 格式化节点。常规规则检查会在正常格式化过程中处理格式化抑制注释。
如果辅助函数在 FormatNodeRule 之外格式化节点或其标记,请运行格式化器测试。如果抑制检查断言报告了某个节点,请对该节点调用 f.context().comments().mark_suppression_checked(node.syntax())。该断言表明辅助函数绕过了节点的常规抑制检查。
IR 组合
使用语义 IR,而不是将空白写成任意文本:
- 用
space()表示必需的空格; - 用软换行表示可选换行;
- 用硬换行表示强制换行;
- 用组选择紧凑或展开的布局;
- 使用与外围结构匹配的缩进原语;
- 将条件内容绑定到其适配决策控制该内容的组。
对于独立的格式化关注点,使用实现 Format 的具名类型。一组传递 &mut Formatter 的自由函数会掩盖已经写入的内容,以及适用的布局不变量。
用只选择一次的枚举表示多路布局。在多个写入位置重新计算布局可能导致输出不一致和幂等性失败。
注释
前置和后置注释通常由格式化器基础设施处理。当节点拥有某个位置,且没有子节点可以附加注释时,请显式格式化悬空注释。
请在更改影响的每个结构边界测试注释:第一个子节点之前、子节点之间、最后一个子节点之后,以及空节点周围。丢弃或移动源注释会造成数据丢失。
测试与幂等性
加载 testing-codegen,以使用快照命令并进行审核。
内部规范应包含用于确定规范输出所需的、有针对性的源代码形态。在适用时,既应包含已格式化的输入,也应包含有意未格式化但应收敛到相同结果的输入。
格式化器测试基础设施会在测试调用期间重新格式化无错误且非范围输出,并在第二次结果不同时失败。输出不同时,IR 可作为诊断依据;它不是独立的相等性约定。
即使 Prettier 快照发生变化或消失,行为变更也应添加内部规范。与某个外部语料库输入一致,并不能覆盖变更影响的边缘情况。
与 Prettier 比较
当兼容性属于要求的一部分时,使用仓库工具:
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l js 'const value={a:1}'
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -f path/to/file.js
--rebuild 会重建 Biome 的 WASM 包并写入构建产物。它适用于实现期间,不适用于只读审核期间。
将差异视为设计参考,而不是自动视为错误。当文档说明的行为或架构要求如此时,Biome 可能会有意产生不同结果。
生成与验证
更改语言格式化器 crate 中的源代码后:
- 运行最精简的格式化器 crate 测试或规范测试,并在接受快照前进行审核。
- 当兼容性相关时,与 Prettier 进行比较;保留经过审核的有意差异。
- 提交前运行
just f和just l。
审核清单
- 每个生成字段都经过显式处理。
- 每个子节点都通过其自身规则进行格式化;或者,架构特定的绕过逻辑会在手动格式化节点主体前检查
f.context().comments().is_suppressed(node.syntax())。 - 每个源标记都经过格式化、通过
format_removed移除,或通过format_replaced替换;不会静默跳过源标记、将其绑定到_,或将其重新创建为静态文本。 - 注释保留在预期位置。
- 自定义格式化由实现
Format<Context>的结构体表示,而不是由自由函数或闭包值表示。 - 布局只选择一次,并通过语义 IR 组合。
- 错误和错误语法仍可表示,不会导致格式化器 panic。
- 内部规范覆盖变更的行为。
- 在一次测试调用中,重新格式化即可收敛。
参考资料
- 格式化器指南:
crates/biome_formatter/CONTRIBUTING.md - JavaScript 格式化器指南:
crates/biome_js_formatter/CONTRIBUTING.md - 格式化器测试基础设施:
crates/biome_formatter_test/src/spec.rs - Prettier 对比:
packages/prettier-compare/README.md