全解析:从 Comment Whitespace 到 leading/trailing/inner Comments)
编译器开发工具【免费下载链接】babel Babel is a compiler for writing next generation JavaScript.项目地址https://gitcode.com/gh_mirrors/ba/babel点击查看免费下载本文以 Babel 官方设计文档 comment-attachment.md 为核心骨架结合 Babel 仓库中 Babel Parserbabel/parser的真实源码与测试用例系统讲解 Babel 解析 JavaScript 时如何把注释挂接到 AST 节点。你将掌握Comment Whitespace注释空白块的形式化定义与四条不变式、节点与注释空白块的四种关系leading / trailing / containing / inner、注释附加的三阶段实现构造 → 挂接 → 收尾以及 trailing comma 场景下的补偿性调整逻辑。阅读完本文你既能理解 Babel AST 中leadingComments/trailingComments/innerComments的语义与产生规则也能在二次开发 Babel 插件或分析 AST 时准确判断任意注释最终挂到哪个节点。引言注释为什么需要一个独立的附加机制Babel 在解析 JavaScript 文件时会为 AST 节点挂接注释。基本的直觉是注释总是附加到它“相邻”的 AST 节点——如果存在前驱节点则作为前驱的 trailing comment如果存在后继节点则作为后继的 leading comment如果两侧都不存在相邻节点则回退到“最内层的包含节点”innermost containing node作为 inner comment。直接实现这个直觉并不容易递归下降解析器在某一时刻只知道“当前正在解析的节点”而注释可能出现在节点之前、之后甚至内部位置关系需要精确的区间比较。因此当前实现对应上游 PR babel#13521本文只引用仓库内文档不展开外部链接采取了一个“反向思路”不把注释直接挂到 AST 节点上而是把 AST 节点挂到一组“适用于该注释空白块的节点栈”上。当某个 Comment Whitespace 建立好它与节点的关系含 leading、trailing、containing之后再把其中的注释转发给 AST 节点并执行一些补偿调整——例如把 trailing comma 之后、列表结构内部的 inner comment 合并到最后一个元素的 trailing comments。下面我们先精确定义 Comment Whitespace再逐步展开三个阶段的具体实现。Comment Whitespace注释空白块的定义与示例什么是 Comment Whitespace一个Comment Whitespace表示一段“由空白字符与注释组成的连续区间”其中注释包括四类源码见 packages/babel-parser/src/tokenizer/index.ts 中skipSpace对skipLineComment/skipBlockComment的调用//行注释CommentLine/* */块注释CommentBlock!--HTML 开放注释仅非 module 且启用 AnnexB 时按行注释解析--HTML 关闭注释同上在仓库源码 packages/babel-parser/src/parser/comments.ts 中CommentWhitespace类型被定义为export type CommentWhitespace { /** 空白块的起始位置 */ start: number; /** 空白块的结束位置 */ end: number; /** 该空白块包含的注释 */ comments: Comment[]; /** 紧邻该空白块之前的 AST 节点 */ leadingNode: Node | null; /** 紧邻该空白块之后的 AST 节点 */ trailingNode: Node | null; /** 包含该空白块的最小 AST 节点 */ containingNode: Node | null; };文档中的标准示例设计文档给出了如下代码片段每行注释对应不同的空白块a// 1 /* 2 */ !-- 3 -- 2;解析时会产生两个 Comment Whitespace第一个对应// 1\n/* 2 */从第 1 个字符的/到前{ start: 1, // / 的位置 end: 15, // 的位置 comments: [ CommentLine { start: 1, end: 5 }, CommentBlock { start: 6, end: 13 } ], leadingNode: Identifier(a), trailingNode: null, containerNode: BinaryExpression, // 即 a 2 这棵子树 }第二个对应!-- 3\n--\n从之后的空格到2前{ start: 16, // 后空格的位置 end: 28, // 2 的位置 comments: [ CommentLine { start: 17, end: 23 }, // !-- 3 CommentLine { start: 24, end: 27 } // -- ], leadingNode: null, trailingNode: NumericLiteral(2), containerNode: BinaryExpression, }值得注意!-- 3与--在 AnnexB 语义下都被解析成CommentLine行注释而非块注释。四条关键性质P1–P3 及推论给定一段程序源码全体 Comment Whitespace 满足以下形式化性质非空性Nonemptiness, P1对任意空白块w有w.start w.end即区间长度恒大于 0。隔离性Isolation, P2不存在两个空白块w1、w2满足w1.start ≤ w2.start ≤ w1.end即相邻注释空白块区间互不重叠。完备性Completeness, P3对任意注释 AST 节点c都存在一个空白块w满足w.start ≤ c.start c.end ≤ w.end称w包络encompassesc。也就是说任何注释都会被某个注释空白块完整覆盖。单调性MonotonicityP1 与 P2 的推论把按start排序的空白块记为{ w1, w2, ..., w_n }则必然有w1.start w1.end w2.start w2.end ... w_n.start w_n.end单调性在实现中极为重要它保证了commentStack注释空白块栈可以按顺序进出注释区间天然有序从而让processComment中“从栈顶逆序扫描、遇到commentEnd nodeStart即可break”的剪枝成立见后文源码分析。节点与 Comment Whitespace 的关系leading / trailing / containing / inner三种基础关系的形式化定义对任意注释空白块w与 AST 节点n可以定义Leading noden是w的前导节点当且仅当n.end w.start。它紧贴空白块左侧。Trailing noden是w的后继节点当且仅当n.start w.end。它紧贴空白块右侧。Containing noden是w的包含节点当且仅当对所有满足N.start w.start w.end N.end的节点N下列命题成立N.start ≤ n.start w.start w.end n.end ≤ N.end即n是“完整包裹住该空白块”的候选节点中位置最内层、尺寸最小的那一个。需要强调从w到n的关系不是单射的——一个注释空白块可能有多个 leading node、多个 trailing node、多个 containing node。这是因为嵌套节点会共享同一边界例如a 2的外层BinaryExpression与内层的Identifier(a)都可能满足end w.start。为此文档定义了极值extrema最外层 leading/trailing noden是w的最外层前导/后继节点当且仅当对w的每一个其他 leading/trailing 节点NN都是n的后代。最内层 containing noden是w的最内层包含节点当且仅当对w的每一个其他 containing 节点Nn都是N的后代。三类注释的非形式化定义有了极值概念就可以精确区分三类注释Leading Comment前导注释c是n的 leading comment当且仅当存在空白块w使得n是w的最外层 trailing node且w包络c。语义上即“紧贴在n之前的注释”存入节点的leadingComments。Trailing Comment后继注释c是n的 trailing comment当且仅当存在空白块w使得n是w的最外层 leading node且w包络c。语义上即“紧贴在n之后的注释”存入节点的trailingComments。Inner Comment内部注释c是n的 inner comment当且仅当同时满足存在空白块w使得n是w的最内层 containing node且w包络c不存在空白块w使得n是w的最外层 leading 或 trailing node 且w包络c。换言之先看注释两侧有没有相邻节点有则归为 leading/trailing两侧都没有直接相邻节点时才归入包含它的最内层节点的innerComments。这正好呼应文档开篇那句“如果没有相邻节点则回退到最内层包含节点”。隔离性P2还带来一个实用简化如果两个注释c1、c2都属于n的 leading/trailing comments那么它们必然被同一个空白块w包络。因此分类时可以“成组标记”而非逐个判断这正是finalizeComment一次性把整个空白块的comments数组赋给节点的原因。trailing comma 场景的补偿说明文档特别指出对应上游 PR babel#10369Babel Parser 会把列表结构如数组、对象、函数参数中 trailing comma 之后的某些 inner comment 标记为列表中最后一个元素的 trailing comment。这是对 AST 中不存在TrailingCommaElement这种可挂注释节点的一种补偿行为。设计文档声明不深入讨论该行为的实现细节但我们会在“收尾阶段”结合源码把这一补偿逻辑讲清楚。实现详解一构造 Comment WhitespaceTokenizer#skipSpace第一阶段发生在词法层。nextToken()在读取每个 token 之前都会调用skipSpace()packages/babel-parser/src/tokenizer/index.ts。skipSpace的核心流程如下对应 skipSpace 实现记录空白起点spaceStart state.pos若启用了AttachComment选项初始化一个临时comments数组。进入loop循环按字符推进普通空白空格、Tab、换行、CRLF、行/段分隔符直接累加state.pos并维护行号遇到/则分别尝试/* */skipBlockComment与//skipLineComment在非 module、启用 AnnexB 的前提下遇到--或!--也按行注释解析skipLineComment(3)/skipLineComment(4)。解析出的注释同时调用addComment写入parser.comments列表并压入本地comments数组。一旦遇到非空白、非注释字符default分支break loop循环结束。此时若comments.length 0就构造一个CommentWhitespace把start/end转换为源码偏移初始leadingNode/trailingNode/containingNode均为null然后state.commentStack.push(commentWhitespace)。从源码可以确认两点事实文档中提到的“把HTMLOpenComment与HTMLCloseComment的解析合并进skipSpace”已落地且受inModule与OptionFlags.AnnexB双重条件约束tokenizer/index.ts。commentStack存放在 tokenizer 的State中packages/babel-parser/src/tokenizer/state.ts顶部引用了CommentWhitespace类型与解析器共享。实现详解二把节点挂接到 Comment WhitespaceprocessComment第二阶段发生在语法层。每个 AST 节点完成时都会经过finishNode→finishNodeAt/finishNodeAtNode并在设置好type、end、loc.end之后调用processComment(node)packages/babel-parser/src/parser/node.ts且受OptionFlags.AttachComment控制——这解释了attachComment: false时 AST 不携带注释字段的行为。processComment的逻辑comments.ts分两步第一步处理栈顶空白块。取commentStack栈顶元素若lastCommentWS.start node.end说明该节点紧贴空白块左侧把它设为leadingNode。这里隐含一个关键细节节点在 finish 之前其后的空白已被nextToken()读取完毕因此“若此节点有 trailing comments它必定是commentStack栈顶那个空白块的leadingNode”。第二步逆序扫描整条栈。对commentStack从栈顶向下迭代若commentWS.end node.start由空白块定义可推出commentWS.start node.start于是node是 containing 候选直接设置commentWS.containingNode node随后立即finalizeComment(commentWS)并把该空白块从栈中splice移除。之所以此刻可以收尾是因为该空白块的 leading/trailing node 若存在不会再改变containing node 已赋为当前节点而当前节点就是“最小尺寸”的最内层包含者也不会再改变。否则commentEnd nodeStart若commentEnd nodeStart设置commentWS.trailingNode node随后直接break终止循环。这个剪枝正是由注释空白块的单调性P1P2 推论保证的栈中更靠前的空白块区间必然更靠左不可能再与当前节点发生关系。两个“获胜者”规则processComment里存在两个方向相反的竞争规则与文档中的极值定义一一对应leadingNode / trailingNode 的“后到者胜”在同一位置连续调用多次finishNode()例如外层与内层节点共享边界时后一次调用会覆盖前一次设置最后一次调用的节点胜出得到的正是最外层leading/trailing node。对应lastCommentWS.leadingNode node的覆盖式赋值。containingNode 的“先到者胜”逆序迭代时对commentEnd nodeStart的空白块只做一次containingNode node赋值仅在“未定义”时设置第一个完成 finish 的节点胜出得到的正是最内层containing node。由于递归下降解析器的自然属性当 containing node 完成 finish 时其内部的 leading/trailing node 必然已经解析完毕因此相关节点不再会被processComment更新——这就是“收尾时机”成立的依据。文档脚注补充了一个例外estree插件会在不同的 tokenizer 位置调用finishNodeAt理论上可能破坏这一前提但因为绝大多数estree用户走的是babel/eslint-parser它会移除挂接的注释所以实际无碍。实现详解三收尾阶段finalizeComment 与 trailing comma 调整finalizeComment把注释分发给相关节点当空白块的三类节点关系都已确定后finalizeComment(commentWS)comments.ts执行真正的分发若leadingNode或trailingNode存在把整个comments数组setTrailingComments(leadingNode)/setLeadingComments(trailingNode)。注意setTrailingComments与setLeadingComments都会对已有注释使用unshift把新注释放在旧注释之前因为commentStack是逆序枚举的comments.ts。若两者皆为null走 containing 分支。先检查空白块前一字符是否为逗号charCodeAt(commentStart - 1) comma不是逗号直接setInnerComments(containingNode, comments)。是逗号进入 trailing comma 补偿逻辑按 containing node 的type分发到adjustInnerComments(node, elements, commentWS)覆盖的列表节点类型包括ObjectExpression/ObjectPattern→node.propertiesCallExpression/NewExpression/OptionalCallExpression→node.argumentsImportExpression→[node.source, node.options ?? null]FunctionDeclaration/FunctionExpression/ArrowFunctionExpression/ObjectMethod/ClassMethod/ClassPrivateMethod/TSTypeParameterDeclaration→node.paramsArrayExpression/ArrayPattern→node.elementsExportNamedDeclaration/ImportDeclaration→node.specifiersTSEnumBody→node.membersTSInterfaceBody→node.body其余类型 → 兜底setInnerCommentsadjustInnerCommentscomments.ts从末尾向前找到最后一个非null元素若找不到列表为空或该元素的start大于空白块起点即逗号前没有实际元素可挂则回退为setInnerComments(node, comments)否则把注释setTrailingComments(lastElement, comments)——这正是文档所述“把 trailing comma 之后的注释合并到最后一个元素的 trailing comments”的源码实现。finalizeRemainingComments为 parseExpression 兜底此外还有一个专用例程finalizeRemainingComments()comments.ts它会倒序把commentStack中残留的每个空白块都执行一遍finalizeComment然后清空栈。它只在getExpression即parseExpression的顶层入口末尾被调用packages/babel-parser/src/parser/expression.ts因为此时顶层节点不是Program而是某个表达式节点正常解析流程没有机会去 finalize 那些挂在顶层表达式前/后的注释所以需要显式排空。用测试用例验证行为仓库的解析器测试直接反映了上述机制的可观察行为。以 packages/babel-parser/test/fixtures/comments/basic/array-expression-trailing-comma 为例输入片段const trailingAfterComma [ One, // One // Two Two, // Three // Four ]对应的output.json中Two这个元素被同时挂上了trailingComments: [ { type: CommentLine, value: Three, start: 142 }, { type: CommentLine, value: Four, start: 153 } ], leadingComments: [ { type: CommentLine, value: One, start: 116 }, { type: CommentLine, value: Two, start: 126 } ]可以看到// Three与// Four都位于Two,的 trailing comma之后按照“inner comment 归属 containing node”的默认规则本应成为数组的 innerComments但通过adjustInnerComments补偿它们被合并到了最后一个元素Two的trailingComments且// Three排在// Four之前unshift逆序插入的净效果。测试目录下还有大量同类用例如arrow-function、async-arrow-function、call-expression-trailing-comma等位于 comments/basic以及attachComment-false/目录用于验证关闭注释附加时的行为差异。关联实现位置速查设计文档packages/babel-parser/ast/comment-attachment.mdCommentWhitespace类型定义与三类注释的分发逻辑packages/babel-parser/src/parser/comments.ts空白扫描与注释收集skipSpace、skipLineComment、skipBlockComment、HTML 注释packages/babel-parser/src/tokenizer/index.ts节点完成钩子finishNode/finishNodeAt/finishNodeAtNode调用processCommentpackages/babel-parser/src/parser/node.ts顶层表达式收尾getExpression调用finalizeRemainingCommentspackages/babel-parser/src/parser/expression.ts行为验证测试packages/babel-parser/test/fixtures/comments/basic总结Babel Parser 的注释附加机制可以概括为一条清晰的主线词法层把连续的“空白 注释”折叠成有序的 Comment WhitespaceP1–P3 保证其不重叠、完备、单调语法层在finishNode时把节点按“最外层 leading/trailing、最内层 containing”两个极值规则挂接到空白块上最后由finalizeComment一次性地把整组注释分发给相关节点并对 trailing comma 场景做adjustInnerComments补偿将逗号后的注释合并到列表末元素的 trailingComments。这一设计把“注释挂到哪个节点”这个看似朴素的语义问题落实为可形式化、可验证的区间算法理解它之后无论是阅读 Babel 生成的 ASTleadingComments/trailingComments/innerComments、编写处理注释的 Babel 插件还是研究babel/eslint-parser的行为你都能准确预测每一个注释的最终归宿。赞分享编译器开发工具【免费下载链接】babel Babel is a compiler for writing next generation JavaScript.项目地址https://gitcode.com/gh_mirrors/ba/babel点击查看免费下载相关推荐pandoc DOCX 注释范围Comment Range往返转换解析从 --track-changesall 到 comment-start/comment-end Spanpandoc DOCX 注释范围Comment Range往返转换解析从 track changesall 到 comment start/commen文档开发工具CLI用注释生成文档Meteor doctool.js 的 Doc Comment 解析与 Markdown 生成机制用注释生成文档Meteor doctool.js 的 Doc Comment 解析与 Markdown 生成机制 Meteor 仓库中的 scripts/do后端前端开发工具移动开发Gutenberg 中 core/post-comment 块已弃用解析块元数据、Context 机制与迁移到 Comments 块Gutenberg 中 core/post comment 块已弃用解析块元数据、Context 机制与迁移到 Comments 块 本文基于 Guten后端前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考