
语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载导读在 AST 变换类工具中注释comment的归属处理一直是最棘手的问题之一注释不像语句、表达式那样拥有明确的节点边界它可能散落在任意两行代码之间、括号之内甚至函数名之后而变换器在移动、删除、重写节点时必须知道每一条注释应该跟着哪个节点走。本文以 Hermes 仓库中 hermes-transform 的 prettier 注释挂接模块 为切入点深入讲解这套从 Prettier 移植而来的 comment attachment 算法它如何为每条注释定位precedingNode、enclosingNode、followingNode如何按“独行 / 行尾 / 其余”三种形态决定挂接方式以及 hermes-transform 如何在 AST 变换流水线中消费这些结果。读完本文你将掌握这一通用注释挂接模型的原理并能理解 Hermes 变换器在插入、克隆、移动注释时的底层机制。一、模块定位为什么 hermes-transform 需要一套“Prettier 的分叉”1.1 仓库中的物理位置与目录结构该模块位于 tools/hermes-parser/js/hermes-transform/src/transform/comments/prettier/其目录结构刻意保持了与 Prettier 上游源码一致的布局以便未来合入上游更新prettier/ ├── common/ │ └── util.js # 通用的字符跳过、换行检测、注释挂接辅助函数 ├── language-js/ │ ├── comments.js # 各类语法结构专用的注释处理器 │ ├── loc.js # locStart / locEnd 位置提取 │ ├── printer-estree.js# canAttachComment 与 handleComments 分发入口 │ └── utils.js # 节点类型判断、参数/实参缓存等工具 ├── main/ │ └── comments.js # 核心的 attach 算法从 Prettier 移植 ├── utils/ │ └── get-last.js # 取数组最后一个元素 └── README.md原 README 明确指出这是 Prettier 注释挂接算法的一个 fork只移植了src/main/comments.js中的attach方法以及运行它所需的最小代码集。Prettier 的原始许可证保存在包根目录的 PRETTIER_LICENCE 文件中许可证要求被完整保留。1.2 为什么只“挖”出attach一个方法Prettier 的注释处理横跨解析、打印两个阶段attach负责在打印之前把游离的注释与 AST 节点建立关联关系后续打印器再依据这些关系决定注释输出的位置。hermes-transform 是 Hermes 的AST 变换框架它借用 Prettier 的解析器来生成可打印的中间 AST因此只需要“注释关联”这一环不需要 Prettier 的整套打印器。从源码结构看这一取舍体现在两个层面文件级模块只保留了main/comments.js核心算法、language-js/*JavaScript/Flow/TypeScript 语法相关处理器和common/util.js基础工具未包含 Prettier 的 doc 打印、版面测量等庞大子系统。接口级整个模块对外只导出attach一个函数见 main/comments.js 末尾 的module.exports {attach}其余文件全部作为其内部依赖存在。这种“保持目录结构不变、只裁剪功能”的做法正是原 README 中“让未来合并上游更新更容易”这一设计意图的落地体现。二、核心算法attach如何决定注释归属attach位于 main/comments.js是整个模块的心脏。它的输入是注释数组、AST 根节点、源码文本、以及包含locStart/locEnd/printer的 options。整体流程分为三步定位decorate→ 分类placement→ 挂接attach。2.1 第一步用二分搜索为每条注释定位相邻节点decorateComment见 main/comments.js#L84-L158负责找出与注释位置相关的三个节点precedingNode注释之前、且离它最近的节点followingNode注释之后、且离它最近的节点enclosingNode完全包含该注释的最内层节点。其核心手段是getSortedChildNodes 二分搜索getSortedChildNodes递归地收集节点的所有子节点并按起始位置排序见 main/comments.js#L22-L79。这里有个值得注意的细节排序用的是“反向插入排序”因为子节点通常本来就按顺序遍历插入基本是常数时间。在有序子节点数组上做二分搜索若注释被某个子节点完全包含start commentStart commentEnd end则“下潜”到该子节点继续递归——这保证了最终找到的enclosingNode是最内层的若子节点整体在注释之前/之后则分别记录最近的precedingNode/followingNode见 main/comments.js#L93-L129。若位置重叠到无法判定直接throw new Error(Comment location overlaps with node location)——这是算法自检的兜底分支。decorateComment还处理了一个特殊边界TemplateLiteral模板字符串内部的注释。注释不能从模板的一个表达式漂移到另一个表达式因此当enclosingNode是TemplateLiteral时会通过findExpressionIndexForComment把注释与前后节点都限定在同一表达式索引内见 main/comments.js#L131-L155。2.2 第二步按形态将注释分成三类定位完成之后attach依据注释在源码中的物理形态将其归入三类之一见 main/comments.js#L227-L291形态判定依据默认挂接优先级ownLine独行isOwnLineComment注释之前存在换行向前跳过空白后能遇到换行符优先作为followingNode的 leading 注释无followingNode时作为precedingNode的 trailing再退化为enclosingNode/AST 根上的 danglingendOfLine行尾isEndOfLineComment注释之后存在换行向后跳过空白后遇到换行符优先作为precedingNode的 trailing 注释其次作为followingNode的 leading再退化到 danglingremaining其余如行中注释以上两者都不满足同时存在precedingNode和followingNode时进入 tie-breaking否则依次尝试 trailing / leading / dangling形态判定的实现依赖 common/util.js 中的hasNewline与skip*系列函数isOwnLineComment会先向前回找同一precedingNode下同一行的前一条注释再检查是否存在换行见 main/comments.js#L309-L330isEndOfLineComment则对称地向后扫描见 main/comments.js#L332-L357。hasNewline本身是“跳过空白 跳过换行”两步比较的复合判断见 common/util.js#L172-L176。2.3 第三步tie-breaking——同行多注释的“算账”逻辑当多条注释与同一对precedingNode/followingNode相邻典型如const x /* a */ /* b */ foo()归属存在歧义attach会把这些待定注释收集到tiesToBreak数组最后统一调用breakTies裁决见 main/comments.js#L359-L418。裁决规则是从后向前检查每条注释与followingNode之间的“缝隙”gap。gap 只能由空白或左括号(组成正则默认/^[\s(]*$/也可由printer.getGapRegex定制。若一条注释与followingNode之间被一串连续的合法 gap 连接则它属于 leading一旦遇到包含其他字符的 gap则前面的注释都判定为 trailing。最终按“前段 trailing、后段 leading”切分整组注释并重新按位置排序节点上的注释数组。另外attach在breakTies之后会清理注释上的precedingNode/enclosingNode/followingNode引用见 main/comments.js#L296-L305因为这些引用会形成 AST 中的循环引用若不删除可能导致后续遍历无限递归。2.4 JSON 等“叶子解析器”的特殊路径attach对json、json5、__js_expression、__vue_expression这几种解析器走简化逻辑注释若在 AST 起点之前则直接作为根节点 leading若在终点之后则作为 trailing不再做节点级定位见 main/comments.js#L201-L215。三、语言层分发printer-estree.js与comments.jsattach是通用的真正体现“JavaScript/Flow/TypeScript 语法特殊性”的是printer-estree.js与language-js/comments.js。3.1 从attach到语言处理器的桥接attach从 options 中取出printer.handleComments见 main/comments.js#L167-L178其中三个回调分别接管三类注释的处理printer-estree.js 把这些钩子组装起来module.exports { canAttachComment, handleComments: { avoidAstMutation: true, // 用 context 对象而非直接改 AST 传递节点引用 ownLine: handleComments.handleOwnLineComment, endOfLine: handleComments.handleEndOfLineComment, remaining: handleComments.handleRemainingComment, }, getCommentChildNodes: handleComments.getCommentChildNodes, };canAttachComment决定哪些节点可以作为注释的附着点注释节点本身、EmptyStatement、TemplateElement、Import、TSEmptyBodyFunctionExpression等都被排除见 printer-estree.js#L15-L26。3.2 语法特化处理器清单language-js/comments.js 按三类形态各维护了一个处理器列表通过.some()依次尝试命中即返回ownLine 处理器handleOwnLineCommentcomments.js#L56-L75包含prettier-ignore处理、最后一个函数参数、成员表达式、if/while/try、class、import 说明符、for、联合类型、match 模式、仅注释文件、import 声明、赋值模式、方法名、标签语句等 14 项特化。endOfLine 处理器handleEndOfLineCommentcomments.js#L81-L98覆盖闭包类型转换注释type、条件表达式、调用表达式、属性、类型别名、变量声明符等。remaining 处理器handleRemainingCommentcomments.js#L104-L119覆盖空括号内注释、箭头函数参数后注释、函数名后注释、TS 映射类型、break/continue、TS 函数尾随注释等。这些处理器解决的都是“仅凭位置关系无法得出正确归属”的语法场景。举两个典型例子if-else 前的注释if (1) {...} // comment \n else {...}中注释若按默认规则会挂到else对应的块表达式上输出时错位。handleIfStatementCommentscomments.js#L173-L239会把注释移入块内成为块首语句的 leading或退化为块的 dangling对于if (a /* comment */) {}这种写在条件括号内的注释则通过getNextNonSpaceNonCommentCharacter探测下一个非空白字符是否为)来判定并挂为前一个节点的 trailing。空函数参数括号内的注释foo(/* comment */)中handleCommentInEmptyParenscomments.js#L517-L544只在函数参数或调用实参为空时才把它作为enclosingNode的 dangling 注释避免误挂到非空参数列表上。prettier-ignore特殊注释isPrettierIgnoreComment判断comment.value.trim() prettier-ignore相关处理器会为后续节点设置prettierIgnore标记如联合类型与 match 模式见 comments.js#L657-L720。3.3 位置函数与节点工具language-js/loc.js 提供locStart/locEnd优先取node.range退化为node.start/node.end且locStart会把装饰器decorators纳入起点。language-js/utils.js 提供跨解析器兼容的节点类型判断isBlockComment兼容Block/CommentBlock/MultiLine等命名差异并用WeakMap缓存getFunctionParameters/getCallArguments的结果见 utils.js#L64-L103。common/util.js 还实现了addLeadingComment/addTrailingComment/addDanglingComment三个挂接原语它们设置注释的leading/trailing/marker标记、初始化printed false并写入node.comments数组见 common/util.js#L280-L306。四、在 hermes-transform 流水线中的集成4.1 从解析到打印的完整调用链hermes-transform 的注释处理横跨解析与打印两个阶段transform/comments/comments.js 是连接 Prettier fork 与变换框架的适配层解析阶段transform/parse.js 在得到 AST 后调用attachComments(comments, ast, code)后者把参数打包成{locStart, locEnd, printer}传给 fork 的attach见 comments.js#L39-L49为后续变换提供“注释属于哪个节点”的定位信息。变换阶段用户在 AST 上做增删改。需要保留/转移注释时使用 comments.js 导出的辅助函数——moveCommentsToNewNode把旧节点的注释整体搬移到新节点cloneCommentsToNewNode连同leading/trailing标记一起克隆cloneJSDocCommentsToNewNode只克隆/** ... */形式的 JSDoc 注释判断条件是块注释且value以*开头见 comments.js#L129-L144。打印阶段transform/print.js 调用mutateESTreeASTCommentsForPrettier(program, originalCode)生成交给 Prettier 的源码文本再调用prettier.format完成最终输出。4.2mutateESTreeASTCommentsForPrettier的两个关键副作用该函数见 comments.js#L51-L107在打印前做了两件重要的事删除program.comments若不删除Prettier 在打印时会基于 AST 再跑一遍自己的注释挂接导致注释在每个节点上重复出现、输出损坏见 comments.js#L57-L61 的注释说明。处理 docblockHermes AST 把文件头注释放在program.docblock上而非任何节点。该函数把它取出来若程序体非空则挂到第一条语句的 leading 位置并调用makeCommentOwnLine保证其独占一行否则直接挂在 program 上最后删除program.docblock见 comments.js#L63-L106。4.3 变换中新增注释appendCommentToSourceAddComments变换通过 MutationContext.appendCommentToSource 把新注释“写”进源码文本AddComments.js#L45。appendCommentToSourcecomments.js#L259-L330针对两种注释类型采用不同策略块注释Block通过设置伪 range如[firstNewline 1, firstNewline]“骗过”Prettier——Prettier 打印时根据注释与节点之间源码文本中是否存在换行来决定是否空行因此LEADING_OWN_LINE/TRAILING_OWN_LINE用makeCommentOwnLine让 range 两侧必然存在换行LEADING_INLINE/TRAILING_INLINE则在空文件中追加$FORCE_INLINE_ON_EMPTY_FILE_TOKEN$;占位语句来保证找到非空白字符。行注释LinePrettier 打印行注释时会直接从源码切片comments.js#L299-L302 的注释说明了这一点因此新增的行注释必须真实地写进源码文本注释本体为//${comment.value}行注释只能 trailing inline此时追加$FORCE_END_OF_LINE_COMMENT_TOKEN$;占位符帮助 Prettier 将其识别为行尾注释。五、attach与 hermes-transform 的适配细节5.1avoidAstMutation模式fork 的attach支持printer.handleComments.avoidAstMutation默认关闭printer-estree.js中置为true。两种模式的区别在于传给语言处理器的参数默认模式把precedingNode/enclosingNode/followingNode直接写到注释对象上处理器接收[comment, text, options, ast, isLastComment]见 main/comments.js#L217-L225。avoidAstMutation模式不写注释对象而是把整个context含comment、三个相邻节点、text、options、ast、isLastComment作为单一参数传入。hermes-transform 启用该模式避免污染 AST 对象符合其“只读 AST 显式变换”的设计。此外isLastComment标记被handleOnlyComments用来处理“文件中只有注释”的边界最后一根注释作为 AST 的 dangling其余作为 leading见 comments.js#L730-L773。5.2 与 Hermes 解析器生态的协作这套注释挂接模块位于hermes-transform包中其输入注释与 AST 来自同目录的hermes-parser与hermes-estree如 comments.js#L11 中import type {Comment, ESNode, Program} from hermes-estree。同一 JS 目录下还有 prettier-plugin-hermes-parser供 Prettier 直接使用 Hermes 解析器两者都随包携带 PRETTIER_LICENCE体现了 Meta 在复用 Prettier 代码时对许可证义务的严格遵循。六、总结与后续阅读Prettier 的 comment attachment 算法解决了一个本质困难注释没有语法位置只有文本位置。attach用“定位 → 分类 → 挂接 → 平票裁决”四步把文本位置映射为 AST 归属关系而language-js/comments.js中的几十个特化处理器则覆盖了if/else、函数参数、类装饰器、联合类型、TS 映射类型等语法结构的特殊形态。hermes-transform 在这个 fork 之上进一步封装了attachComments、mutateESTreeASTCommentsForPrettier、appendCommentToSource等面向变换场景的 API使变换器可以在不触碰 Prettier 内部细节的前提下安全地移动、克隆、新增注释。如果想继续深入可以从以下路径着手阅读算法主体 main/comments.js重点跟踪decorateComment的二分搜索与breakTies的缝隙判定阅读语法特化 language-js/comments.js对照典型 JS 写法逐一验证各处理器的触发条件阅读适配层 transform/comments/comments.js 与打印入口 transform/print.js理解注释如何在“解析—变换—打印”全链路中保持稳定对比 prettier-plugin-hermes-parser 的使用方式观察同一套注释机制在“直接格式化”与“AST 变换”两种场景下的差异。赞分享语言运行时编译器移动开发【免费下载链接】hermesA JavaScript engine optimized for running React Native.项目地址https://gitcode.com/gh_mirrors/hermes/hermes点击查看免费下载相关推荐使用 prettier/plugin-hermes 为 Prettier 接入 Hermes 解析器使用 prettier/plugin hermes 为 Prettier 接入 Hermes 解析器 本篇技术指南围绕 Prettier 官方仓库中的 pr开发工具格式化CLIflow-transform 中的注释附着算法从 Prettier fork 到 AST 变换的深度剖析flow transform 中的注释附着算法从 Prettier fork 到 AST 变换的深度剖析 导读 在基于 AST 的代码变换中注释comme开发工具静态分析代码质量Babel Parser 注释附加机制Comment Attachment全解析从 Comment Whitespace 到 leading/trailing/inner CommentsBabel Parser 注释附加机制Comment Attachment全解析从 Comment Whitespace 到 leading/traili编译器开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考