Prettier 序列表达式首个元素前注释的稳定性修复:从两次格式化漂移到一次定稿
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
序列表达式((a, b))是 JavaScript 中少数需要依赖括号才能清晰表达语义的语法结构,而注释一旦出现在它内部,其归属与打印位置就极易受格式化流程影响。本文基于 Prettier 仓库中尚未发布的修复记录(changelog_unreleased/javascript/19894.md),完整还原“序列表达式首元素前注释不稳定”这一 Bug 的复现过程、根因分析与修复机制,并结合 注释挂载源码 与 序列表达式打印源码 深入讲解 Prettier 是如何保证“一次格式化即稳定”的。读完本文,你将理解 Prettier 注释处理的三段式管线(ownLine / endOfLine / remaining),并掌握如何在本地验证此类稳定性修复。
修复背景:一个会让格式化结果“漂移”的注释
Prettier 作为 opinionated 的代码格式化器,其核心承诺之一是幂等性(idempotency):同一份代码无论格式化一次还是两次,结果应当完全一致。一旦出现“第二次格式化后结果又变了”的情况,用户就会在编辑器中反复看到 diff 抖动,这是格式化器最令人头疼的体验问题之一。
本次修复针对的正是这种不稳定场景。问题代码形如:
((/* c */ a), b);当注释/* c */出现在序列表达式首个元素之前、且被最外层括号包裹时,Prettier 稳定版的行为表现为:
- 第一次格式化:先移除冗余括号、再打印注释,得到
(/* c */ a, b); - 第二次格式化:注释被移动到整个序列表达式之前,得到
/* c */ (a, b);
第二次结果与第一次不同,说明格式化结果并未收敛——这正是稳定性缺陷的典型症状。修复后的 Prettier main 分支(即当前未发布代码)在第一次格式化时就稳定输出/* c */ (a, b);。
复现问题:两种格式化结果为何会不同
为便于理解,先把整个输入到输出的过程拆成两个阶段:
- 括号归一化:
((/* c */ a), b)中,/* c */ a被一对冗余括号包裹。Prettier 在打印阶段会判断括号是否必要,冗余括号会被移除,序列表达式最终以(a, b)的形式打印(最外层括号为 ExpressionStatement 所必需,予以保留)。 - 注释挂载(comment attachment):注释必须先被挂到 AST 的某个节点上,打印阶段才能知道它在哪儿输出。注释的最终位置完全取决于它被挂载到了哪个节点。
稳定版中,第一次格式化时注释被当作首元素a的leading comment(前导注释)处理,打印在元素前;第二次格式化时注释又被识别为整个序列表达式的前导注释,打印在(之前。同一条注释两次被挂到了不同的节点上,于是产生了两种输出。
修复机制:为序列表达式前导注释提供专用处理器
修复的核心是给 JavaScript 语言的注释处理管线新增了一个专用处理器handleSequenceExpressionLeadingComment,定义于 src/language-js/comments/handle-comments.js#L1222-L1238:
function handleSequenceExpressionLeadingComment({ comment, enclosingNode, precedingNode, followingNode, }) { if ( !precedingNode && enclosingNode?.type === "SequenceExpression" && followingNode === enclosingNode.expressions[0] ) { addLeadingComment(enclosingNode, comment); return true; } return false; }该处理器的判定条件非常精确:
!precedingNode:注释之前没有其他节点,即它位于序列表达式的最前面;enclosingNode?.type === "SequenceExpression":注释的直接包裹节点是序列表达式;followingNode === enclosingNode.expressions[0]:注释后紧跟的元素就是序列表达式的第一个元素。
三者同时满足时,注释会被明确挂载为整个序列表达式的 leading comment(通过addLeadingComment,实现见 src/main/comments/utilities.js),并立即返回true表示已处理。这样注释的归属从第一次格式化起就被固定下来,不再随括号归一化而漂移到首元素上。
深入理解:注释处理管线的三个挂载入口
Prettier 在打印前会遍历所有注释,按注释与代码的相对位置把它们分成三类,分别交给三条处理链。从 src/language-js/comments/handle-comments.js#L72-L164 可以看到,handleSequenceExpressionLeadingComment被同时注册进了全部三条链:
handleOwnLineComment(注释独占一行,L98);handleEndOfLineComment(注释位于行尾,L134);handleRemainingComment(其余位置,L162)。
这意味着无论/* c */是内联块注释、独占一行,还是夹在括号之间,只要满足上述结构条件,都会被统一挂到序列表达式节点上。这一“三入口全覆盖”的注册方式,保证了修复在不同注释书写风格下行为一致。
从源码结构还可以推断:这个处理器被放在handleOwnLineComment链的最后一位,属于兜底性质的专门规则——前面的通用规则(如handleConditionalExpressionComments、handleAssignmentLikeComments等)都未命中时,才会轮到这条针对序列表达式的精确匹配规则。
打印侧配合:序列表达式如何排版
注释挂载到位后,打印行为由 src/language-js/print/sequence-expression.js 中的printSequenceExpression决定。该函数区分了两种典型上下文:
- 当序列表达式位于
ExpressionStatement或ForStatement头部(少数允许不带括号出现的位置)时,第一个表达式之后的内容会被缩进,例如:
(a, b, c);- 其他上下文(如
return、throw、箭头函数体等)则通过shouldIndentSequenceExpression(同文件 L12-L27)判断是否需要在换行时缩进,并配合ifBreak/softline实现“短行不换行、长行换行缩进”的排版。
注释作为序列表达式的 leading comment 挂载后,会随整个序列表达式一起打印在括号之前,即修复目标/* c */ (a, b);。由于handleParenthesizedExpressionTrailingComment(handle-comments.js#L1240-L1304)还会处理括号表达式尾随注释的收敛问题,序列表达式相关的注释打印在本次修复中实现了前后一致的确定性。
测试佐证:序列表达式注释的既有覆盖
仓库中已有针对序列表达式注释行为的测试,例如 tests/format/js/comments/return-statement.js#L104-L114 覆盖了return/throw语句中序列表达式内的注释:
function sequenceExpressionInside() { return ( // Reason for a a, b ); throw ( // Reason for a a, b ); }对应的快照断言位于 tests/format/js/comments/snapshots/format.test.js.snap,其中有多个sequenceExpressionInside相关条目(分别对应不同解析器如 babel、typescript、flow 的输出)。这类快照测试正是 Prettier 保证注释处理稳定性的主要防线——新增的handleSequenceExpressionLeadingComment处理器需要与此类既有行为保持一致,并通过快照比对确认不会引入回归。
如何在本地验证该修复
如果你想在当前仓库实际验证这一稳定性修复,可以按以下步骤操作:
- 运行格式化测试:仓库使用 Jest 快照测试,格式化测试的入口配置在 jest.config.js,运行指定目录的测试:
yarn jest tests/format/js/comments --updateSnapshot说明:
--updateSnapshot会更新快照文件,仅用于本地调试验证;在未确认输出符合预期前,不要提交快照改动。
- 手动复现输入:将
((/* c */ a), b);写入一个.js文件,通过 CLI 连续格式化两次并比对结果:
yarn prettier input.js | yarn prettier --stdin-filepath input.js若两次输出一致,即满足幂等性要求。
- 查阅变更记录:本次修复对应未发布变更记录 changelog_unreleased/javascript/19894.md(PR #19894,贡献者 @Kjubikstronk)。仓库的
changelog_unreleased/目录按语言分门别类收集待发布变更(如 javascript、typescript、css 等),正式发版时这些条目会被合并进 CHANGELOG.md,因此该修复将在下一个 Prettier 版本中随正式变更日志一起公布。
小结
本次修复虽然只涉及几行代码,却完整展示了 Prettier 保证格式化稳定性的方法论:
- 注释归属必须先于打印确定:所有注释在打印前都要经过
handleComments管线挂载到 AST 节点(src/language-js/comments/handle-comments.js),归属一旦确定,打印就完全确定; - 结构条件要精确匹配:
handleSequenceExpressionLeadingComment通过!precedingNode、enclosingNode.type === "SequenceExpression"、followingNode === expressions[0]三重判定,把“首元素前的注释”这一特殊情况从通用规则中精确识别出来; - 三类注释位置全覆盖:处理器同时注册进 ownLine / endOfLine / remaining 三条挂载链,确保不同书写风格行为一致;
- 幂等性是硬性要求:格式化结果必须在一次处理后收敛,这也是所有修复合入前必须通过快照测试验证的原因。
对于 Prettier 用户而言,这一修复意味着:包含“序列表达式首元素前注释”的代码在升级后将不再出现格式化结果漂移;对于想要为 Prettier 贡献修复的开发者而言,本文梳理的“注释挂载处理器 + 打印函数 + 快照测试”三件套,正是解决此类稳定性问题的最小完整路径。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考