ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

Harper 的 harper-comments:基于 tree-sitter 为 30 种编程语言精准提取并检查注释的实现剖析 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 的两层职责通用层作为 tree-sitter 的封装帮助 Harper 定位大量编程语言中的注释专用层为若干语言的结构化文档注释如 Go 的//go:指令提供目的明确的解析器这些解析器统一通过CommentParser自动启用。从 harper-comments/Cargo.toml 可以确认其依赖面核心依赖是harper-core提供Parser、Token、Masker等抽象、harper-tree-sittertree-sitter 封装、harper-html供 JavaDoc 使用以及 26 个具体语言的 tree-sitter 语法包tree-sitter-c、tree-sitter-rust、tree-sitter-go、tree-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::MaskCommentMasker, Boxdyn 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-typescriptcsharptree-sitter-c-sharpkotlintree-sitter-kotlin-ngclojuretree-sitter-clojureluatree-sitter-luacmaketree-sitter-cmakenixtree-sitter-nixdartharper-tree-sitter-dartphptree-sitter-phpelixirtree-sitter-elixirpowershelltree-sitter-powershellgotree-sitter-gorubytree-sitter-rubygleamtree-sitter-gleamrusttree-sitter-rustgroovytree-sitter-groovyscalatree-sitter-scalahaskell/damltree-sitter-haskellshellscripttree-sitter-bashjavatree-sitter-javasoliditytree-sitter-solidityswift/toml/zig对应语法包2.2 文件名推断与 LSP 文件类型对齐除了按语言 ID 构建CommentParser还提供new_from_filename(path: Path, ...)它通过内部函数filename_to_filetype把文件扩展名转换为语言 ID。这份映射刻意与 LSPLanguage Server Protocol的文件类型命名保持一致例如cpp、h→cppex、exs→elixirgroovy、gradle→groovykt、kts→kotlinsbt、sc、scala、mill→scalabash、sh→shellscriptps1、psd1、psm1→powershell。源码注释特别叮嘱贡献者try to keep this in sync withnew_from_language_id即两张映射表必须同步维护。这解释了为何 harper-comments/tests/language_support.rs 的测试集里会出现common.mill、complex_gradle_build.gradle这类非标准文件。2.3 注释节点的识别条件遮罩阶段如何判断一个 tree-sitter 节点是注释条件函数非常简单fn node_condition(n: Node) - bool { n.kind().contains(comment) }即节点类型字符串包含comment子串。这一宽松匹配覆盖了comment、line_comment、block_comment、multiline_comment等 tree-sitter 各语法中的常见命名也是该 crate 能以极低成本接入新语言的诀窍所在。三、专用解析器为结构化注释定制规则README 中提到的 purpose-built parsers 位于 harper-comments/src/comment_parsers/共有六个Go、JavaDoc、JsDoc、Lua、Solidity、Unit。它们在new_from_language_id中的选择逻辑为let comment_parser: Boxdyn 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 规则来检查——这意味着注释中写dont、their这类常见错误会被正常捕获而代码块等 Markdown 结构也能被正确理解。3.1 公共基础剥离注释定界符所有解析器共享 mod.rs 中的without_initiators工具函数它从注释文本的首尾各去掉连续的注释定界字符#、-、/、*、!及空白得到净内容区间。例如/// 这是一条注释→ 净内容这是一条注释/** ... */的开头/**与行尾装饰星号被剥离空注释///得到空区间由单测cleans_empty_comment直接验证。随后各解析器把净内容交给内层解析器并将结果 token 的span用push_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 是复杂度最高的解析器采用逐行解析策略按换行切分对每行执行without_initiators剥离*、//等定界符后交给内层 Markdown 解析器块标签若行内出现后紧跟单词如class、param则从该标签起至行尾的所有 token 标记为TokenKind::Unlintable不检查因为param name the name中的name是标识符而非自然语言内联标签mark_inline_tags函数扫描{tag ...}形式的内联标签如{link MyClass}将其整体标记为不可检查。该函数通过定位OpenCurlyWord的模式、再向后寻找CloseCurly来确定标签边界。源码中附带了针对边界情况的回归测试/** { */这种未闭合标签曾导致解析死循环escapes_loop测试以及{link MyClass#foo}这种 JSDoc 自定义链接语法handles_inline_link测试的完整 token 断言。3.4 JavaDocHTML 解析 装饰星号清洗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 在构造时把内层解析器设为JsDocSolidity 的 Natspec 注释语法与 JSDoc 同源并额外增加一条规则净内容以SPDX-开头的行即// SPDX-License-Identifier: MIT这类许可证标识整体跳过避免检查MIT、GPL-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.cpp、multiline_comments.ts、multiline_comments.sol各期望 4 lintdirty/clean 对照clean.lua0 lint vsdirty.lua1 lintclean.zig0 vsdirty.zig5clean.exs0 vsdirty.exs4JSDoc/JavaDocjsdoc.ts期望 4javadoc_complex.java期望 5忽略机制ignore_comments.rs/.c/.sol/.ps1各期望 1被忽略的注释贡献 0未被忽略的保留 1历史 issue 回归issue_96.lua、issue_132.rs、issue_229.js、issue_962.sh、issue_1097.lua等以 issue 命名的用例锚定曾经出过 bug 的具体输入。此外单元层面还有针对性回归comment_parser.rs 中的hang测试用 10 秒超时守护//{j这类 Java 注释输入不挂死与 jsdoc 的escapes_loop一起构成防死循环防线。六、小结这套设计值得借鉴的三个点遮罩 专用解析器两级架构通用路径只依赖 tree-sitter 节点名包含comment这一约定新语言接入成本极低真正需要理解文档注释语法的少数语言再挂专用Parser且全部对上层透明CommentParser是唯一公共 API。净文本解析 span 回填的统一坐标系所有专用解析器都在剥离定界符后的净文本上做词法/语法分析再用push_by平移 token 位置使 Harper 的诊断行号、列号与原文严格对齐——测试中每个 token span 都能取回真实内容的断言持续守护这一点。豁免规则贴近真实工程场景//go:build指令、SPDX-License-Identifier头、param标签、shebang、Markdown 代码围栏、spellchecker:ignore显式忽略——每一条豁免都对应开发者写注释时会遇到的真实误报源而不是理想化的假设输入。如果你要为 Harper 生态新增语言支持路径也清晰可见在 harper-comments/src/comment_parser.rs 的new_from_language_id与filename_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),仅供参考
返回列表