ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Harper 语法规则编写实战指南:从短语纠正到 ExprLinter 完整实现

Harper 语法规则编写实战指南:从短语纠正到 ExprLinter 完整实现 NLP开发工具【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址https://gitcode.com/GitHub_Trending/har/harper点击查看免费下载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与justfileJust 命令运行器环境就绪后即可使用just系列命令。尽早提交草稿 Pull RequestDraft 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.weirexpr 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 级ExprLinteradd_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), ], Dont 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 WorkspaceHarper 提供了声明式的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是笔误实现时须替换为你自己的规则名如MyRuleuse crate::{ Lrc, Token }; use super::{Lint, ExprLinter}; pub struct MyRule { expr: Boxdyn 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]) - OptionLint { 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]) - OptionLint { 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/patternsLint 构造span决定下划线与修改范围每个Lint的所有Suggestion必须共享同一spansuggestions可提供零到多个常配合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 实际是三个 tokenWorld是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_mappingsharper-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开发工具【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址https://gitcode.com/GitHub_Trending/har/harper点击查看免费下载相关推荐Legado 源规则正则表达式完全指南从基础语法到书源实战Legado 源规则正则表达式完全指南从基础语法到书源实战 正则表达式是 Legado阅读 3.0书源规则中最重要的文本匹配工具。本指南以 docs/de移动开发前端应用HaE规则编写完全指南从基础语法到高级技巧HaE规则编写完全指南从基础语法到高级技巧 你是否还在为复杂的渗透测试数据提取而烦恼是否希望通过自定义规则快速定位关键信息本文将系统讲解HaEHighlYARA 规则编写完全指南从基础语法到高级条件表达式的实战手册YARA 规则编写完全指南从基础语法到高级条件表达式的实战手册 本篇技术指南以 YARA 项目官方文档 docs/writingrules.rst https网络安全模式匹配创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进