1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use crate::{
// EDIT You won't need `TokenStringExt` unless you need a span covering multiple tokens.
Lint,
Token,
TokenStringExt,
// EDIT `SequenceExpr` is the most versatile `Expr` but you can use any `Expr` or `Pattern`
expr::{Expr, SequenceExpr},
// EDIT `expr_linter::Chunk` is a run of tokens between commas and is the default.
// EDIT But if you want to match a pattern containing commas, use `Sentence` instead.
linting::{ExprLinter, LintKind, Suggestion, debug::format_lint_match, expr_linter::Chunk},
};
// EDIT rename this struct for your new linter
pub struct ExprLinterSkeleton {
// EDIT `SequenceExpr` is the most versatile `Expr` but you can use any `Expr` or `Pattern`
expr: SequenceExpr,
}
// EDIT If your linter doesn't need access to the dictionary and doesn't depend on the dialect
// EDIT then just use `default()` as the only constructor.
// EDIT If you need dictionary or dialect access, use `impl ExprLinterSkeleton` and `fn new()`
// EDIT instead of `impl Default for ExprLinterSkeleton` and `fn default()`
impl Default for ExprLinterSkeleton {
fn default() -> Self {
Self {
// EDIT `SequenceExpr` is the most versatile `Expr` but you can use any `Expr` or `Pattern`.
// EDIT `SequenceExpr` has many many useful methods from which you can build fairly complex
// EDIT expressions
expr: SequenceExpr::any_capitalization_of("erorr"),
}
}
}
impl ExprLinter for ExprLinterSkeleton {
type Unit = Chunk;
// EDIT If you don't need the context before or after the matched tokens
// EDIT then use the simpler `fn match_to_lint()` instead.
// EDIT There are some methods in `expr_linter` to help checking for words
// EDIT and punctuation in the "before" or "after" contexts.
fn match_to_lint_with_context(
&self,
// NOTE Whitespace also uses a `Token`. What out for LLMs and agents that
// NOTE assume `Token`s are always words. "Hello World" is actually three
// NOTE tokens and "World" is `matched_tokens[2]`, not `[1]` as your LLM might assume.
matched_tokens: &[Token],
source: &[char],
context: Option<(&[Token], &[Token])>,
) -> Option<Lint> {
// EDIT A debug printf here while developing is very handy for verifying
// EDIT that your `Expr` above is matching what you expect
// EDIT make sure you remove this before committing
eprintln!("🚨 {}", format_lint_match(matched_tokens, context, source));
// EDIT Place your custom linter logic here.
// EDIT If your `Expr` can sometimes match
// EDIT `span` is the range of tokens that you will modify (or insert after).
// EDIT It is also the range that will be underlined etc.
// EDIT Each `Token` has a `.span` or you get get a `Span` of a slice of `Token`s
// EDIT using `.span()` from `TokenStringExt`.
// EDIT Note that every `Suggestion` for a single `Lint` must use the same span.
// EDIT This is important if you are correcting a phrase by suggesting one change
// EDIT to one word, or a different change to a different word. In this case
// EDIT your `span` will need to cover at least both of those words.
let span = matched_tokens.span()?;
// EDIT Look in `harper-core/src/linting/lint_kind.rs` for the lint kinds available
// EDIT with their descriptions to help you decide which is appropriate.
let lint_kind = LintKind::Miscellaneous;
// EDIT It may not be practical to suggest a correction. In such cases, use `vec![]`.
// EDIT Otherwise you can offer one or several suggestions. Each suggestion must
// EDIT operate on the same `Span`. You can replace the `Span`, remove the `Span`,
// EDIT or insert after the `Span`. When replacing text you will normally want to
// EDIT maintain how it used uppercase vs lowercase letter. So `replace_with_match_case`.
// EDIT In that case you need to pass the `template` as well as the new `value`.
// EDIT To get the template, you pass the original text as a `&[char]` slice.
// EDIT It's worth keeping in mind that though there are some helper functions that
// EDIT work with `String` or `&str` or string literals, most of this infrastructure
// EDIT natively works with `Vec<char>` or `&[char]`.
let suggestions = vec![Suggestion::replace_with_match_case_str(
"correction",
span.get_content(source),
)];
// EDIT You can return different messages depending on what the problem is and what
// EDIT the suggestions are.
// EDIT If it's not possible to be 100% sure that only real mistakes are flagged
// EDIT and there's a likelihood of some non-mistakes being flagged as false positives
// EDIT your message should allow for both possibilities.
// EDIT Likewise, if two or more suggestions are very different, as happens with
// EDIT confusable words, it's a good idea to guide the user with concise definitions.
let message = "Fix this erorr".to_string();
// EDIT You can return different `Lint`s from different places in your logic.
Some(Lint {
span,
lint_kind,
suggestions,
message,
// EDIT `priority` is not that well defined yet. Feel free to use `..Default::default`
// EDIT for now. Note that if you do, you mustn't use a trailing comma.
..Default::default()
})
}
fn expr(&self) -> &dyn Expr {
&self.expr
}
fn description(&self) -> &str {
"A linter skeleton for contributors to copy into `harper_core/src/linting/` and rename."
}
}
#[cfg(test)]
mod tests {
// EDIT There's a bunch more useful assertions for unit tests in `linting::tests`.
use crate::linting::tests::assert_suggestion_result;
use super::ExprLinterSkeleton;
#[test]
fn test_skeleton() {
assert_suggestion_result("erorr", ExprLinterSkeleton::default(), "correction");
}
}