Harper 的 harper-comments:基于 tree-sitter 为 30 种编程语言精准提取并检查注释的实现剖析
2026/9/14 17:12:28 网站建设 项目流程

Harper 的 harper-comments:基于 tree-sitter 为 30 种编程语言精准提取并检查注释的实现剖析

【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址: https://gitcode.com/GitHub_Trending/har/harper

本文围绕harper-comments这个 Rust crate 展开。它是 Harper(一个离线、隐私优先的 Rust 语法/拼写检查器)中负责"只检查代码注释"的核心组件:通过封装 tree-sitter 语法树定位各语言的注释节点,并为 Go、JSDoc、JavaDoc 等结构化文档注释提供专门的解析器。读完本文,你将理解 Harper 是如何把"从源代码中摘出注释文本"变成一条可靠的解析流水线,以及如何通过spellchecker:ignore等机制精细控制检查范围。

一、crate 定位:一个"注释提取器"而非完整语法引擎

harper-comments的官方说明(harper-comments/README.md)只有寥寥数句,但它概括了该 crate 的两层职责:

  1. 通用层:作为 tree-sitter 的封装,帮助 Harper 定位"大量编程语言"中的注释;
  2. 专用层:为若干语言的结构化文档注释(如 Go 的//go:指令)提供目的明确的解析器,这些解析器统一通过CommentParser自动启用。

从 harper-comments/Cargo.toml 可以确认其依赖面:核心依赖是harper-core(提供ParserTokenMasker等抽象)、harper-tree-sitter(tree-sitter 封装)、harper-html(供 JavaDoc 使用),以及 26 个具体语言的 tree-sitter 语法包(tree-sitter-ctree-sitter-rusttree-sitter-gotree-sitter-typescript等)。crate 入口 harper-comments/src/lib.rs 只导出一个公共类型:

mod comment_parser; mod comment_parsers; mod masker; pub use comment_parser::CommentParser;

也就是说,外部使用者只需面向CommentParser一个 API 即可工作,内部的专用解析器是自动选择的——这正是 README 所说 "enabled automatically" 的含义。

二、CommentParser:按语言 ID 或文件名构建解析器

harper-comments/src/comment_parser.rs 是整个 crate 的中枢。CommentParser内部结构为:

pub struct CommentParser { inner: parsers::Mask<CommentMasker, Box<dyn Parser>>, }

它由两部分组合而成:

  • CommentMasker:基于 tree-sitter 的遮罩器,负责在语法树中找出所有注释节点;
  • 内层dyn Parser:真正"读懂"注释文本的解析器(按语言不同而不同)。

Parsertrait 的实现只有一行转发(parse方法委托给self.inner.parse(source)),但构造函数完成了全部语言路由逻辑。

2.1 支持的语言列表

new_from_language_id(language_id: &str, markdown_options: MarkdownOptions)将 30 个语言 ID 映射到对应的 tree-sitter 语法,节选自源码:

语言 IDtree-sitter 语法语言 IDtree-sitter 语法
c/cpptree-sitter-c/tree-sitter-cppjavascript/typescript/*reacttree-sitter-javascript/tree-sitter-typescript
csharptree-sitter-c-sharpkotlintree-sitter-kotlin-ng
clojuretree-sitter-clojureluatree-sitter-lua
cmaketree-sitter-cmakenixtree-sitter-nix
dartharper-tree-sitter-dartphptree-sitter-php
elixirtree-sitter-elixirpowershelltree-sitter-powershell
gotree-sitter-gorubytree-sitter-ruby
gleamtree-sitter-gleamrusttree-sitter-rust
groovytree-sitter-groovyscalatree-sitter-scala
haskell/damltree-sitter-haskellshellscripttree-sitter-bash
javatree-sitter-javasoliditytree-sitter-solidity
swift/toml/zig对应语法包

2.2 文件名推断:与 LSP 文件类型对齐

除了按语言 ID 构建,CommentParser还提供new_from_filename(path: &Path, ...),它通过内部函数filename_to_filetype把文件扩展名转换为语言 ID。这份映射刻意与 LSP(Language Server Protocol)的文件类型命名保持一致,例如:

  • cpphcpp
  • exexselixir
  • groovygradlegroovy
  • ktktskotlin
  • sbtscscalamillscala
  • bashshshellscript
  • ps1psd1psm1powershell

源码注释特别叮嘱贡献者"try to keep this in sync withnew_from_language_id",即两张映射表必须同步维护。这解释了为何 harper-comments/tests/language_support.rs 的测试集里会出现common.millcomplex_gradle_build.gradle这类"非标准"文件。

2.3 注释节点的识别条件

遮罩阶段如何判断一个 tree-sitter 节点是注释?条件函数非常简单:

fn node_condition(n: &Node) -> bool { n.kind().contains("comment") }

即节点类型字符串包含comment子串。这一宽松匹配覆盖了commentline_commentblock_commentmultiline_comment等 tree-sitter 各语法中的常见命名,也是该 crate 能以极低成本接入新语言的诀窍所在。

三、专用解析器:为"结构化注释"定制规则

README 中提到的 "purpose-built parsers" 位于 harper-comments/src/comment_parsers/,共有六个:GoJavaDocJsDocLuaSolidityUnit。它们在new_from_language_id中的选择逻辑为:

let comment_parser: Box<dyn Parser> = match language_id { "go" => Box::new(Go::new_markdown(markdown_options)), "java" => Box::new(JavaDoc::default()), "javascript" | "javascriptreact" | "typescript" | "typescriptreact" => { Box::new(JsDoc::new_markdown(markdown_options)) } "lua" => Box::new(Lua::new_markdown(markdown_options)), "solidity" => Box::new(Solidity::new_markdown(markdown_options)), _ => Box::new(Unit::new_markdown(markdown_options)), };

除 Java 外,其余专用解析器默认以Markdown 解析器作为内层解析器(new_markdown),即"注释里的正文按 Markdown 规则来检查"——这意味着注释中写don'ttheir这类常见错误会被正常捕获,而代码块等 Markdown 结构也能被正确理解。

3.1 公共基础:剥离注释定界符

所有解析器共享 mod.rs 中的without_initiators工具函数:它从注释文本的首尾各去掉连续的注释定界字符(#-/*!)及空白,得到"净内容"区间。例如:

  • /// 这是一条注释→ 净内容这是一条注释
  • /** ... */的开头/**与行尾装饰星号被剥离;
  • 空注释///得到空区间,由单测cleans_empty_comment直接验证。

随后各解析器把净内容交给内层解析器,并将结果 token 的spanpush_by(actual.start)平移回原文坐标——这个"解析净文本 + 坐标回填"的模式保证了 Harper 诊断能精确指回源文件中的字符位置。

3.2 Go:跳过//go:构建指令

go.rs 针对 Go 特有的构建指令注释。若净内容以go:开头(匹配['g', 'o', ':', ..]),说明这是//go:build//go:generate一类指令,Harper 找到该行第一个换行符并整体跳过,直接返回空 token 序列——避免把指令参数误当散文来检查。

3.3 JSDoc:块级标签与内联标签的双重豁免

jsdoc.rs 是复杂度最高的解析器,采用"逐行解析"策略:

  1. 按换行切分,对每行执行without_initiators剥离*//等定界符后交给内层 Markdown 解析器;
  2. 块标签:若行内出现@后紧跟单词(如@class@param),则从该标签起至行尾的所有 token 标记为TokenKind::Unlintable(不检查),因为@param name the name中的name是标识符而非自然语言;
  3. 内联标签mark_inline_tags函数扫描{@tag ...}形式的内联标签(如{@link MyClass}),将其整体标记为不可检查。该函数通过定位OpenCurly+@+Word的模式、再向后寻找CloseCurly来确定标签边界。

源码中附带了针对边界情况的回归测试:/** {@ */这种未闭合标签曾导致解析死循环(escapes_loop测试),以及{@link MyClass#foo}这种 JSDoc 自定义链接语法(handles_inline_link测试)的完整 token 断言。

3.4 JavaDoc:HTML 解析 + 装饰星号清洗

javadoc.rs 选择了与 JSDoc 不同的路线:JavaDoc 正文允许内嵌 HTML,因此它直接复用 harper-html 的HtmlParser解析净文本,而非 Markdown。其后续处理包括:

  • 遍历 token 流,在每个换行之后删除连续的*(装饰星号)与Spacetoken,还原对齐星号缩进带来的"假空格";
  • 复用 JSDoc 的mark_inline_tags处理内联标签;
  • 将形如@tag word ...的四元组 token(@、标签名、空格、下一个单词)标记为Unlintable

测试文件 javadoc_clean_simple.java 与 javadoc_complex.java 分别断言 0 个和 5 个 lint,覆盖了两类典型 JavaDoc。

3.5 Lua:整行@标签豁免

lua.rs 处理 LuaDoc 风格注释。其starts_with_prefix判断:若一行注释的净内容以@开头(如@param x number),则整行不做检查、仅产出换行 token;其余行照常按 Markdown 检查。这与 JSDoc 的"从标签起豁免"不同,是"整行豁免"策略,契合 LuaDoc 标签通常独占一行的习惯。

3.6 Solidity:复用 JSDoc 逻辑并豁免 SPDX 头

solidity.rs 在构造时把内层解析器设为JsDoc(Solidity 的 Natspec 注释语法与 JSDoc 同源),并额外增加一条规则:净内容以SPDX-开头的行(即// SPDX-License-Identifier: MIT这类许可证标识)整体跳过,避免检查MITGPL-3.0-or-later等许可证 ID 文本。

3.7 Unit:覆盖"大多数语言"的兜底解析器

unit.rs 是其余全部语言(C、Rust、Python 无关——本 crate 不含 Python,以及 Zig、Clojure、Elixir 等)的默认解析器。其文档注释自我定位为 "meant to covermostcases inmostprogramming languages"。它的逐行逻辑与 Lua 类似,另有一个关键机制:代码围栏(code fence)开关。当某行净内容以 ``` 开头时翻转in_code_fence标志,围栏内的行直接跳过——因为在注释块里用 Markdown 代码围栏粘贴示例代码时,围栏内容不应被当作英文散文来检查。

四、CommentMasker:遮罩、shebang 与spellchecker:ignore

"找到注释"这一步由 harper-comments/src/masker.rs 完成。CommentMasker包裹harper-tree-sitter提供的TreeSitterMasker,在其生成遮罩后做两件事:

1. shebang 处理。如果注释 span 从文件偏移 0 开始且以#!开头(真正的 shebang 只可能出现在首行),trim_leading_shebang会裁掉#!...第一行,但保留被 tree-sitter 合并进同一注释块的后续行继续检查。这解决了"shebang 后的注释块整体被屏蔽"的问题,对应测试ignore_shebang_1.sh~ignore_shebang_4.sh(前三个断言 0 lint,第四个因后续注释含错误断言 1 lint)。

2. 显式忽略指令。遮罩器内置默认忽略条件:注释文本若包含下列任一写法,则该注释整体不检查:

spellchecker:ignore / spellchecker: ignore spell-checker:ignore / spell-checker: ignore spellcheck:ignore / spellcheck: ignore harper:ignore / harper: ignore

同时它暴露了new_with_ignore_condition构造器,允许嵌入方自定义忽略条件。CommentParser还通过create_ident_dict透传标识符字典能力,可以把代码中的自定义标识符并入词典,避免误报。

五、测试体系:以"期望 lint 数量"验证每种语言

harper-comments/tests/language_support.rs 是整个 crate 的语言支持验收层。它用create_test!宏批量生成测试:读取 harper-comments/tests/language_support_sources/ 下的源码文件,按文件名构建CommentParser,跑LintGroup::new_curated(dict, Dialect::American)全量 lint,断言 lint 数量等于预期值,并逐 token 校验 span 能取回真实文本(防止坐标回填错误)。

覆盖维度举例:

  • 多语言多行注释multiline_comments.cppmultiline_comments.tsmultiline_comments.sol各期望 4 lint;
  • dirty/clean 对照clean.lua0 lint vsdirty.lua1 lint,clean.zig0 vsdirty.zig5,clean.exs0 vsdirty.exs4;
  • JSDoc/JavaDocjsdoc.ts期望 4,javadoc_complex.java期望 5;
  • 忽略机制ignore_comments.rs/.c/.sol/.ps1各期望 1(被忽略的注释贡献 0,未被忽略的保留 1);
  • 历史 issue 回归issue_96.luaissue_132.rsissue_229.jsissue_962.shissue_1097.lua等以 issue 命名的用例,锚定曾经出过 bug 的具体输入。

此外,单元层面还有针对性回归:comment_parser.rs 中的hang测试用 10 秒超时守护//{@j这类 Java 注释输入不挂死,与 jsdoc 的escapes_loop一起构成防死循环防线。

六、小结:这套设计值得借鉴的三个点

  1. "遮罩 + 专用解析器"两级架构:通用路径只依赖 tree-sitter 节点名包含comment这一约定,新语言接入成本极低;真正需要理解文档注释语法的少数语言再挂专用Parser,且全部对上层透明(CommentParser是唯一公共 API)。
  2. "净文本解析 + span 回填"的统一坐标系:所有专用解析器都在剥离定界符后的净文本上做词法/语法分析,再用push_by平移 token 位置,使 Harper 的诊断行号、列号与原文严格对齐——测试中"每个 token span 都能取回真实内容"的断言持续守护这一点。
  3. 豁免规则贴近真实工程场景//go:build指令、SPDX-License-Identifier头、@param标签、shebang、Markdown 代码围栏、spellchecker:ignore显式忽略——每一条豁免都对应开发者写注释时会遇到的真实误报源,而不是理想化的假设输入。

如果你要为 Harper 生态新增语言支持,路径也清晰可见:在 harper-comments/src/comment_parser.rs 的new_from_language_idfilename_to_filetype两处加入映射、在 harper-comments/Cargo.toml 添加对应 tree-sitter 依赖、按需在 harper-comments/src/comment_parsers/ 增加专用解析器,最后向 harper-comments/tests/language_support_sources/ 补充测试源码即可。

【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址: https://gitcode.com/GitHub_Trending/har/harper

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询