- NLP
- 开发工具
【免费下载链接】harper
Offline, privacy-first grammar checker. Fast, open-source, Rust-powered
Harper 是一款离线、隐私优先、由 Rust 驱动的开源语法检查器,其绝大多数语法规则都以harper-corecrate 内的Lintertrait 实现形式存在。本文基于官方贡献者指南,系统讲解如何为 Harper 添加一条全新语法规则:从选择实现复杂度的不同路径(短语纠正、多映射纠正、专有名词、Weir 规则),到创建并注册一个ExprLinter模块,再到本地测试与提交 Pull Request 的全过程。读完本文,你将掌握"零 Rust 基础也能上手"的简单规则添加方式,以及面向复杂规则的源码级实现与调试方法。
规则是什么:Lintertrait 与灵活的匹配策略
在动手之前,先厘清一个核心概念:Harper 语境下的"规则"(rule),指的是 Linter trait 的一个实现。该 trait 提供了极大的灵活性,允许采用多种策略查询给定的文档并定位错误。
在实际仓库中,harper-core/src/linting/mod.rs的文件头注释也直接声明了这一点:"Frameworks and rules that locate errors in text",并附上了官方编写规则的文档链接。整条规则体系分布在harper-core/src/linting/目录下,包含数百个具体规则文件、weir_rules子目录(Weir 语言编写的规则)、以及phrase_set_corrections等批量规则组。理解了"规则 = Linter 实现"这个心智模型后,剩下的问题就是:你的规则需要多复杂?根据复杂度不同,有四条递增的实现路径可供选择。
准备工作:Fork、环境与草稿 PR
在写任何代码之前,按顺序完成三件事:
- Fork Harper 单仓库(monorepo):在 GitHub 上 fork Automattic/harper,获得可修改的副本;随后将 fork 克隆到本地并新建一个分支(克隆方法可参考 GitHub 官方文档)。
- 配置开发环境:参考官方的 环境搭建指南,安装 Rust 工具链等依赖。仓库根目录还提供了
rust-toolchain.toml、rustfmt.toml、clippy.toml与justfile(Just 命令运行器),环境就绪后即可使用just系列命令。 - 尽早提交草稿 Pull Request(Draft PR):这能让 Harper 维护者提前看到正在进行的改动,也便于在开发过程中直接在 PR 里提问、获得反馈,避免方向走偏。
路径一:最简单的短语替换——按行添加映射
绝大多数语法规则其实非常朴素:识别某个错误短语(如 "all of the sudden")并替换为正确短语(如 "all of a sudden"),完全不需要任何复杂编程。
值得说明的是:原指南中给出的添加位置是harper-core/src/linting/phrase_corrections/mod.rs,而在当前仓库中,这类纯短语替换规则已经主要由 Weir 规则体系承载(见路径四)。例如 "all of the sudden" 的完整规则实现位于 harper-core/src/linting/weir_rules/AllOfASudden.weir:
expr main [(all of the sudden), (all of sudden), (all the sudden)] let message "Prefer the standard phrasing `all of a sudden`." let description "Guides this expression toward the standard `all of a sudden`." let kind "Nonstandard" let becomes "all of a sudden" test "On an app that has been released since December, all of the sudden around February 5th ANRs started going up." "On an app that has been released since December, all of a sudden around February 5th ANRs started going up."从源码结构可以推断,这类规则的优势在于:内置了大小写变体与跨行断行处理。原指南中同一规则的 Rust 写法示意如下(结构与字段含义保持不变):
"AllOfASudden" => ( // The offending phrase ["all of the sudden"], // The correct phrase ["all of a sudden"], // The message to notify the user of the error "The phrase is `all of a sudden`, meaning `unexpectedly`.", // A description of the rule. "Corrects `all of the sudden` to `all of a sudden`." ),同一规则还可以挂载多个错误短语或多个正确短语:
"EnMasse" => ( // Multiple offending phrases ["on mass", "on masse", "in mass"], ["en masse"],"InOfItself" => ( ["in of itself"], // Multiple correct phrases ["in itself", "in and of itself"],路径二:多变体纠正——phrase_set_corrections
当规则涉及单复数、多种动词时态、多个变体等更复杂的纠正关系时,应使用 harper-core/src/linting/phrase_set_corrections/mod.rs。该文件包含两个 section,均由宏驱动,最终通过MapPhraseSetLinter注册为 chunk 级ExprLinter:
add_1_to_1_mappings:一组名对应多对"单个错误短语 → 单个正确短语"
"Ado" => ( &[ // Multiple variants but only one offending phrase // and one correct phrase ("further adieu", "further ado"), ("much adieu", "much ado"), ], "Use `ado` (meaning 'fuss') not `adieu` (meaning 'farewell').", "Corrects `adieu` to `ado` in common phrases." ),仓库中真实条目还带有第四个可选参数LintKind(如LintKind::Eggcorn、LintKind::Usage、LintKind::Punctuation、LintKind::Spelling),用于给错误分类:
"Ado" => ( &[ ("further adieu", "further ado"), ("much adieu", "much ado"), ], "Don't confuse the French/German `adieu`, meaning `farewell`, with the English `ado`, meaning `fuss`.", "Corrects `adieu` to `ado`.", LintKind::Eggcorn ),add_many_to_many_mappings:多对"多错误短语 → 多正确短语"
"ChangeTack" => ( &[ // Both multiple variants and also multiple offending // phrases and/or correct phrases (&["change tact", "change tacks"], &["change tack"]), (&["changed tact", "changed tacks"], &["changed tack"]), ], "A change in direction is a change of `tack` (not `tact`).", "Corrects the idiom `change tack`." ),从 map_phrase_set_linter.rs 与 map_phrase_linter.rs 的源码可以看到,MapPhraseLinter内部将Expr、correct_forms、message、description、LintKind封装为一条可直接注册的 linter,这解释了为什么在mod.rs中只需要"一行宏 + 一个元组"就能生成完整规则。
路径三:专有名词大写——proper_noun_rules.json
如果你只是想强制一个多 token 专有名词的正确大写(例如 "Tumblr Blaze"),无需写任何 Rust 代码,只需在 harper-core/proper_noun_rules.json 中添加一条记录:
// The name of the rule "TumblrNames": { // The canonical capitalization of the proper noun. "canonical": [ "Tumblr Blaze", "Tumblr Pro", "Tumblr Live", "Tumblr Ads", "Tumblr Communities", "Tumblr Shop", "Tumblr Dashboard" ], // A description to be shown to the user when they make a mistake. "description": "Ensure proper capitalization of Tumblr-related terms." },该 JSON 文件目前已包含数百条专有名词规则(如美洲、大洋洲、海洋与海域、加拿大、老挝、马来西亚、各国国名等),由proper_noun_capitalization_linters在 harper-core/src/linting/lint_group/mod.rs 中以out.merge_from(...)方式并入规则组。与之配套,harper-core/default_config.json中也会出现对应的布尔开关条目(例如"name": "TumblrNames", "label": "Tumblr Names"),供用户在配置界面启停。
路径四:Weir 规则——为风格指南而生
如果你的规则属于组织风格指南中的特殊约定(例如"禁止写G Suite,应写Google Workspace"),Harper 提供了声明式的Weir 语言:把.weir文件放进harper-core/src/linting/weir_rules目录即可。
放置规则时遵循两条约定(见原指南):
- 顶层文件:如
RuleName.weir,被加载为一个名为RuleName的公开规则; - 目录聚合:如
RuleName/Singular.weir与RuleName/Plural.weir,它们作为独立子规则运行,但 Harper 对外只暴露一个名为RuleName的公开规则(共享同一设置项与规则目录条目)。
Weir 的表达式语言在 docs/weir 有完整介绍,其核心是expr main定义模式、let message/description/kind/becomes定义行为、test定义测试样例,匹配默认不区分大小写。本文开头展示的AllOfASudden.weir即是一个典型实例。
路径五:完整自定义——创建ExprLinter模块
如果上述四种途径都无法满足你的规则(例如需要访问上下文、字典、方言),才需要进入完整的 Rust 实现流程。
1. 创建规则模块文件
每个规则在harper-core/src/linting/目录下拥有独立文件,文件名为规则的snake_case形式。拿不定主意时先叫my_rule.rs,先不要写入任何内容,完成注册登记后再实现。
2. 注册规则
注册分三步,全部完成后规则才进入系统:
第一步:在 harper-core/src/linting/mod.rs 顶部把模块挂入模块树(当前文件已有数百个mod声明,保持字母序插入):
mod an_a; mod avoid_curses; mod boring_words; mod capitalize_personal_pronouns; // +mod my_rule;第二步:在 harper-core/src/linting/lint_group/mod.rs 顶部导入你的规则类型(注意:当前仓库中该文件实为lint_group/mod.rs目录结构,原指南中的lint_group.rs已演进为目录):
use super::an_a::AnA; use super::avoid_curses::AvoidCurses; use super::boring_words::BoringWords; use super::capitalize_personal_pronouns::CapitalizePersonalPronouns; use super::correct_number_suffix::CorrectNumberSuffix; // +use super::my_rule::MyRule;第三步:在new_curated函数末尾的宏调用区添加注册(lint_group/mod.rs中同时定义了insert_struct_rule、insert_struct_rule_with_dict、insert_struct_rule_with_dialect、insert_expr_rule、insert_expr_rule_with_dict、insert_expr_rule_with_dialect六套宏):
insert_struct_rule!(AdjectiveOfA); insert_expr_rule!(BackInTheDay); insert_struct_rule!(WordPressDotcom); insert_expr_rule!(OutOfDate); // +insert_expr_rule!(MyRule);选择原则:如果实现的是ExprLinter,请使用insert_expr_rule,以便利用 Harper 更激进的缓存策略(宏内部调用add_chunk_expr_linter,按 chunk——即逗号分隔的子句——进行缓存匹配);普通Linter则使用insert_struct_rule。若规则需要Dictionary或Dialect,分别使用带_with_dict/_with_dialect后缀的版本。
3. 更新default_config.json
新增规则必须同步更新 harper-core/default_config.json。该文件定义了 Harper 全平台展示的默认配置——新规则若不加入其中,就不会出现在默认设置里。其结构为Group → child → settings的嵌套布尔开关树(如"name": "TumblrNames", "state": true, "label": "Tumblr Names"),对应StructuredConfig的类型定义。工作期间建议把开关设为启用(state: true),避免调试时混淆"规则没生效"与"规则被默认关闭"。
4. 编写规则本体:ExprLinter模板
定义表达式并实现ExprLintertrait,是给 Harper 添加新规则最便捷的方式。官方模板如下(注意:原模板中impl ExprLinter for ThatWhich是笔误,实现时须替换为你自己的规则名,如MyRule):
use crate::{ Lrc, Token }; use super::{Lint, ExprLinter}; pub struct MyRule { expr: Box<dyn Expr>, } impl Default for MyRule { fn default() -> Self { // Define the grammatical expr the rule should look for in user text. let mut expr = todo!(); Self { expr: Box::new(expr), } } } impl ExprLinter for MyRule { /// Pass the expr to the ExprLinter framework. fn expr(&self) -> &dyn Expr { self.expr.as_ref() } /// Any series of tokens that match the expr provided in the `default()` method above will /// be provided to this function, which you are required to map into a [`Lint`] object. fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> { unimplemented!(); } fn description(&self) -> &'static str { "Replace this text with a description of what your rule looks for." } }从 harper-core/src/linting/expr_linter.rs 的实现可以看到底层机制:ExprLinter是带关联类型Unit: DocumentIterator的 trait,默认按Chunk(逗号间的子句)遍历文档、也可换用Sentence(整句)遍历;expr()提供匹配表达式,命中片段会交给match_to_lint/match_to_lint_with_context转成Lint,返回None则跳过本次匹配不产生提示。
以仓库中的真实规则 change_tack.rs 为参照,一个生产级ExprLinter通常这样组织:
pub struct ChangeTack { expr: FirstMatchOf, // 多分支表达式组合 } impl Default for ChangeTack { fn default() -> Self { let verb_forms = &["change", "changes", "changing", "changed"]; let noun_forms = &verb_forms[..3]; let eggcorns = &["tact", "tacks", "tacts"]; Self { expr: FirstMatchOf::new(vec![ Box::new(SequenceExpr::longest_of(vec![ Box::new(SequenceExpr::word_set(verb_forms).then_optional(...)), Box::new(SequenceExpr::word_set(noun_forms).t_ws().t_aco("of")), ]).t_ws().then_word_set(eggcorns)), Box::new(SequenceExpr::word_seq(&["different", "tact"])), ]), } } } impl ExprLinter for ChangeTack { type Unit = Chunk; fn expr(&self) -> &dyn Expr { &self.expr } fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option<Lint> { let tact_tok = toks.last()?; let tact_span = tact_tok.span; let tact_chars = tact_span.get_content(src); Some(Lint { span: tact_span, lint_kind: LintKind::Eggcorn, suggestions: vec![Suggestion::replace_with_match_case( ['t', 'a', 'c', 'k'].to_vec(), tact_chars, )], message: "A change in direction or approach is a change of `tack`. Not `tact` (or `tacks` or `tacts`).".to_owned(), priority: 32, }) } fn description(&self) -> &'static str { "Locates errors in the idioms `to change tack` and `change of tack` ..." } }要点:
- 表达式构建:
SequenceExpr是最通用的Expr,提供word_set、word_seq、then_optional、then_any_of、t_ws(可选空白)、t_aco(可选 "of")等大量组合方法;此外还有FirstMatchOf、LongestMatchOf、FixedPhrase、SimilarToPhrase、Word等模式,详见 harper-core/src/expr 与 harper-core/src/patterns; - Lint 构造:
span决定下划线与修改范围,每个Lint的所有Suggestion必须共享同一span;suggestions可提供零到多个,常配合Suggestion::replace_with_match_case保持原词大小写;lint_kind从 lint_kind.rs 的枚举中选择,包含Agreement、Capitalization、Eggcorn、Grammar、Punctuation、Repetition、Spelling、Nonstandard、Redundancy、Readability、Regionalism、Miscellaneous(默认值)等类别,文档明确说明"没有理由不新增类别"; - 上下文感知:需要查看匹配前/后 token 时,实现
match_to_lint_with_context而非简单版match_to_lint。
5. 直接使用官方骨架模板
为降低上手门槛,仓库根目录提供了两个可直接复制的骨架文件:
- harper-core/expr_linter_skeleton.rs —— 最小化的未注释版本;
- harper-core/expr_linter_skeleton_commented.rs —— 带详尽注释的版本,逐行解释
Expr、Unit、Lint、Suggestion、LintKind等组件的可选方案。
使用步骤:
- 复制任一文件到
harper-core/src/linting/my_rule.rs; - 将结构体
ExprLinterSkeleton改名为你的规则名; - 按
// EDIT注释逐个定制表达式与逻辑; - 提交前务必删除调试用的
eprintln!("🚨 {}", format_lint_match(...))语句。
注释版还给出了一条关键警告:空白也是Token。"Hello World" 实际是三个 token,World是matched_tokens[2]而非[1]——这是新手(以及 LLM/Agent 自动生成代码)最容易踩的坑。骨架自带单元测试test_skeleton,断言"erorr"被纠正为"correction"。
测试你的规则
先在仓库根目录写一个包含目标错误的测试文档:
This is an test of the `an_a` rule. Your test should look different.命令行方式
运行just lint <test filename>,会输出文档中语法错误的可读报告。若你规则关注的错误没有出现在列表中,说明有问题需要排查。
规则有了单元测试之后,从harper-core目录只运行名字匹配某模式的部分测试,可显著缩短"编辑-测试"循环(跳过其他 workspace crate 的测试):
cd harper-core cargo test -- <REGEX>注意:如果两条 lint(或建议)重叠或针对同一问题,
just lint只显示第一条;此时应考虑换用其他调试手段(如单元测试断言)。
调试过程中可参考仓库大量现成的单元测试写法,例如change_tack.rs内的assert_suggestion_result("change tact", ChangeTack::default(), "change tack")系列断言,以及 harper-core/src/linting 下各规则文件自带的#[cfg(test)] mod tests。
Visual Studio Code 方式
- 安装 Harper 扩展后,在设置页配置
harper-ls二进制路径,指向<harper repo>/target/release/harper-ls:
- 每次改动后需重新编译
harper-ls并在命令面板执行Developer: Reload Window重载窗口:
cargo build --release # Run in the monorepo to compile `harper-ls`.限制说明:该工作流仅适用于只改 Rust 代码的情况。若你的改动涉及 VS Code 扩展本身(例如把新规则的设置项加入扩展的
package.json以便在 VS Code 中测试),则需要以扩展开发宿主(Extension Development Host)方式打开扩展,详见官方 Visual Studio Code 贡献指南。
完成并提交:Elevate Your Pull Request
当你对规则的效果满意后,将草稿 PR 提升为 "ready for review" 状态,维护者会审查并(大概率)合并。审查阶段请同步自查:规则描述是否清晰、LintKind分类是否恰当、是否更新了default_config.json、// EDIT调试输出是否已清除、单元测试是否覆盖了大小写/标点/上下文等边界情况。
小结:五条路径的选择速查
| 规则复杂度 | 推荐路径 | 涉及文件 |
|---|---|---|
| 单个/多个固定短语替换 | Weir 规则(或短语映射) | harper-core/src/linting/weir_rules、phrase_set_corrections/mod.rs |
| 单复数、多时态的多变体纠正 | add_1_to_1_mappings/add_many_to_many_mappings | harper-core/src/linting/phrase_set_corrections/mod.rs |
| 多 token 专有名词大写 | JSON 条目 | harper-core/proper_noun_rules.json |
| 组织风格指南自定义约定 | Weir 声明式规则 | harper-core/src/linting/weir_rules |
| 需要上下文/字典/方言的复杂规则 | 完整ExprLinter实现 | harper-core/src/linting/expr_linter.rs |
无论选择哪条路径,最终规则都会被统一注册进LintGroup(见 harper-core/src/linting/lint_group/mod.rs),通过default_config.json控制默认启停,并随 Harper 各前端(CLI、VS Code 扩展、编辑器插件、Web 端)自动生效。掌握了本文的完整流程,你就能为 Harper 贡献从一行短语映射到复杂上下文规则在内的任意新规则。
- NLP
- 开发工具
【免费下载链接】harper
Offline, privacy-first grammar checker. Fast, open-source, Rust-powered
相关推荐
Legado 源规则正则表达式完全指南:从基础语法到书源实战
Legado 源规则正则表达式完全指南:从基础语法到书源实战 正则表达式是 Legado(阅读 3.0)书源规则中最重要的文本匹配工具。本指南以 docs/de
移动开发前端应用HaE规则编写完全指南:从基础语法到高级技巧
HaE规则编写完全指南:从基础语法到高级技巧 你是否还在为复杂的渗透测试数据提取而烦恼?是否希望通过自定义规则快速定位关键信息?本文将系统讲解HaE(Highl
YARA 规则编写完全指南:从基础语法到高级条件表达式的实战手册
YARA 规则编写完全指南:从基础语法到高级条件表达式的实战手册 本篇技术指南以 YARA 项目官方文档 docs/writingrules.rst https
网络安全模式匹配
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考