Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: rewrote instructions on how to author a rule #482

Merged
merged 2 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 154 additions & 15 deletions packages/web/src/routes/docs/contributors/author-a-rule/+page.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,162 @@ title: Author a Rule
Before we get into how to write a rule, it is important that we get some of the language cleared up.

When we refer to a Harper rule, we are talking about [an implementation of the Linter trait](https://docs.rs/harper-core/latest/harper_core/linting/trait.Linter.html).
As you can see, there is an enormous amount of flexibility in this trait and a wide variety of strategies for querying the provided document.
As you can see, there is an enormous amount of flexibility in this trait and a wide variety of potential strategies for querying the provided document to locate errors.

## Patterns
This guide will go through one easy way to add a rule to Harper.
The lofty goal is for this to be doable by someone with little to no Rust experience.
You should, however, be able to figure out how to use Git.

For new contributors, defining a pattern and [implementing the PatternLinter trait](https://docs.rs/harper-core/latest/harper_core/linting/trait.PatternLinter.html) is the easiest way to get started.
If you like to learn by example, you can make a copy of the `ThatWhich` rule and modify it to look for something else.
Here's the general playbook:
## Fork the Harper Monorepo

- Start by forking the [Harper monorepo](https://github.com/Automattic/harper/fork) and create a new file under `harper-core/src/linting` called `my_rule.rs`.
- Follow our [guide to get your environment set up.](./environment)
- Copy in a template rule (like from `that_which.rs`).
- Modify the constructor to create a pattern to look for the problematic text you have in mind.
- Export your rule from the `linting module` ([which you can find here](https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs))
- Add your rule to the `LintGroup` [macro call](https://github.com/Automattic/harper/blob/master/harper-core/src/linting/lint_group.rs), which will aggregate its results with the other linters in Harper.
- Open a PR.
Before you can open a pull request or modify any code, you need a mutable copy of our monorepo.
The best way to do that is to [fork it in GitHub](https://github.com/Automattic/harper/fork).

## Querying the Document Directly
Next, you'll want to copy this fork onto your computer and create a new branch.
GitHub has an [excellent page explaining how clone repositories](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository).

If you don't wish to use a Harper [Pattern](https://docs.rs/harper-core/latest/harper_core/patterns/trait.Pattern.html), you may query the `Document` directly.
Make sure you read the [available methods](https://docs.rs/harper-core/latest/harper_core/struct.Document.html) available for `Document`s and for [TokenStrings](https://docs.rs/harper-core/latest/harper_core/struct.Document.html#impl-TokenStringExt-for-Document).
## Get Your Environment Set Up

Please read our [guide for getting your environment set up](./environment).

## Open a Draft Pull Request

Next, you'll want to open a draft pull request.
This gives us (the Harper maintainers) a better view of what is actively being worked on.
It also makes it much easier to ask questions about how Harper works while you're working on your rule.

GitHub has some [good documentation on how to create a draft PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) if this is your first time.

## Create Your Rule's Module

We separate each rule into its own file inside the `harper-core/src/linting` [directory.](https://github.com/Automattic/harper/tree/master/harper-core/src/linting)
Create a new file under that directory with the name of your rule in `snake_case`.
If you can't decide yet, just call it `my_rule.rs`.

Don't put anything in this file yet, there's some bookkeeping we have to do first.

## Register Your Rule

Before we start describing to Harper what grammatical errors to look for, we need to register your rule within the system.

First, add your rule's module to the tree by adding it to [the top of the mod file](https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might I suggest something like this:

You need to add it in two places. First add a "module declaration":

mod an_a;
mod avoid_curses;
mod boring_words;
// [svp! df:+]mod my_rule;

Then a little further down add a "public use declaration":

pub use an_a::AnA;
pub use avoid_curses::AvoidCurses;
pub use boring_words::BoringWords;
// [svp! df:+]pub use my_rule::MyRule;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that the single code block with both edits is simpler. The correct placement of the lines will be handled by rustfmt before we merge the PR.

It should look something like this:

```rust title="harper-core/src/linting/mod.rs"
mod an_a;
mod avoid_curses;
mod boring_words;
mod capitalize_personal_pronouns;
// [svp! df:+]mod my_rule;
// [svp! df:+]pub use my_rule::MyRule;
```

Next, we need to configure whether your rule will be enabled by default.
While you're working on it, we **highly suggest** you enable it to avoid confusion.

To do that, import your rule at the top of the `lint_group` [file](https://github.com/Automattic/harper/blob/master/harper-core/src/linting/mod.rs).

```rust title="harper-core/src/linting/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;
// [svp! df:+]use super::my_rule::MyRule;
```

Finally, enable it in the macro invocation near the bottom:

```rust title="harper-core/src/linting/lint_group.rs"
create_lint_group_config!(
SpelledNumbers => false,
AnA => true,
SentenceCapitalization => true,
UnclosedQuotes => true,
// [svp! df:+] MyRule => true
);
```

That's it!

## Write Your Rule

Defining a pattern and [implementing the PatternLinter trait](https://docs.rs/harper-core/latest/harper_core/linting/trait.PatternLinter.html) is the easiest way to define a new rule for Harper.
Here's a template to get you started:

```rust title="my_rule.rs"
use crate::{
Lrc, Token
};

use super::{Lint, PatternLinter};

pub struct MyRule {
pattern: Box<dyn Pattern>,
}

impl Default for MyRule {
fn default() -> Self {
let mut pattern = todo!();

Self {
pattern: Box::new(pattern),
}
}
}

impl PatternLinter for ThatWhich {
fn pattern(&self) -> &dyn Pattern {
self.pattern.as_ref()
}

/// Any series of tokens that match the pattern 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]) -> Lint {
unimplemented!();
}

fn description(&self) -> &'static str {
"Replace this text with a description of what your rule looks for."
}
}
```

## Test Your Changes

To test your rule, write out an example of the error it looks for in a test file at the root of the Harper monorepo.

```markdown title="test.md"
This is an test of the `an_a` rule.
Your test should look different.
```

### Using the Command Line

From there, you can run `just lint <test filename>`.
It should emit a readable report of the grammatical errors in the document.
If the error your rule looks for does _not_ appear in this list, something is wrong.

If you need any help writing or debugging rules, don't be afraid to contact the Harper team in your draft pull request.

> **Note:** if two lints (or suggestions) overlap or address the same problem, this command will only display the first one.
> In that case, you might want to use another method of debugging.

### Using Visual Studio Code

First make sure you have [the extension installed from the marketplace](https://marketplace.visualstudio.com/items?itemName=elijah-potter.harper).
Then, can configure the path of the `harper-ls` binary the Visual Studio Code extension uses in settings.
Set it to `<harper repo>/target/release/harper-ls`.

![How to change the `harper-ls` path](/images/vscode_harper_path.webp)

Now every time you want to test a change, you'll have to recompile `harper-ls` and reload Visual Studio Code using `Developer: Reload Window`.

```bash
cargo build --release # Run in the monorepo to compile `harper-ls`.
```

## Elevate Your Pull Request

Once you're satisfied with your rule, you can go ahead and elevate your pull requests to mark it as "ready for review."
At that point, a maintainer on the Harper team take a look at it and (hopefully) merge it.
Binary file not shown.
Loading