harper_core/linting/
expr_linter.rs

1use crate::expr::{Expr, ExprExt};
2use blanket::blanket;
3
4use crate::{Document, LSend, Token, TokenStringExt};
5
6use super::{Lint, Linter};
7
8/// A trait that searches for tokens that fulfil [`Expr`]s in a [`Document`].
9///
10/// Makes use of [`TokenStringExt::iter_chunks`] to avoid matching across sentence or clause
11/// boundaries.
12#[blanket(derive(Box))]
13pub trait ExprLinter: LSend {
14    /// A simple getter for the expression you want Harper to search for.
15    fn expr(&self) -> &dyn Expr;
16    /// If any portions of a [`Document`] match [`Self::expr`], they are passed through [`ExprLinter::match_to_lint`] to be
17    /// transformed into a [`Lint`] for editor consumption.
18    ///
19    /// This function may return `None` to elect _not_ to produce a lint.
20    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint>;
21    /// A user-facing description of what kinds of grammatical errors this rule looks for.
22    /// It is usually shown in settings menus.
23    fn description(&self) -> &str;
24}
25
26/// Helper function to find the only occurrence of a token matching a predicate
27///
28/// Returns `Some(token)` if exactly one token matches the predicate, `None` otherwise.
29/// TODO: This can be used in the [`ThenThan`] linter when #1819 is merged.
30pub fn find_the_only_token_matching<'a, F>(
31    tokens: &'a [Token],
32    source: &[char],
33    predicate: F,
34) -> Option<&'a Token>
35where
36    F: Fn(&Token, &[char]) -> bool,
37{
38    let mut matches = tokens.iter().filter(|&tok| predicate(tok, source));
39    match (matches.next(), matches.next()) {
40        (Some(tok), None) => Some(tok),
41        _ => None,
42    }
43}
44
45impl<L> Linter for L
46where
47    L: ExprLinter,
48{
49    fn lint(&mut self, document: &Document) -> Vec<Lint> {
50        let mut lints = Vec::new();
51        let source = document.get_source();
52
53        for chunk in document.iter_chunks() {
54            lints.extend(run_on_chunk(self, chunk, source));
55        }
56
57        lints
58    }
59
60    fn description(&self) -> &str {
61        self.description()
62    }
63}
64
65pub fn run_on_chunk<'a>(
66    linter: &'a impl ExprLinter,
67    chunk: &'a [Token],
68    source: &'a [char],
69) -> impl Iterator<Item = Lint> + 'a {
70    linter
71        .expr()
72        .iter_matches(chunk, source)
73        .filter_map(|match_span| {
74            linter.match_to_lint(&chunk[match_span.start..match_span.end], source)
75        })
76}