harper_core/linting/
map_phrase_linter.rs

1use super::{ExprLinter, Lint, LintKind};
2use crate::expr::Expr;
3use crate::expr::FixedPhrase;
4use crate::expr::LongestMatchOf;
5use crate::expr::SimilarToPhrase;
6use crate::linting::Suggestion;
7use crate::linting::expr_linter::Chunk;
8use crate::{Token, TokenStringExt};
9
10pub struct MapPhraseLinter {
11    description: String,
12    expr: Box<dyn Expr>,
13    correct_forms: Vec<String>,
14    message: String,
15    lint_kind: LintKind,
16}
17
18impl MapPhraseLinter {
19    pub fn new(
20        expr: Box<dyn Expr>,
21        correct_forms: impl IntoIterator<Item = impl ToString>,
22        message: impl ToString,
23        description: impl ToString,
24        lint_kind: Option<LintKind>,
25    ) -> Self {
26        Self {
27            description: description.to_string(),
28            expr,
29            correct_forms: correct_forms.into_iter().map(|f| f.to_string()).collect(),
30            message: message.to_string(),
31            lint_kind: lint_kind.unwrap_or(LintKind::Miscellaneous),
32        }
33    }
34
35    pub fn new_similar_to_phrase(phrase: &'static str, detectable_distance: u8) -> Self {
36        Self::new(
37            Box::new(SimilarToPhrase::from_phrase(phrase, detectable_distance)),
38            [phrase],
39            format!("Did you mean the phrase `{phrase}`?"),
40            format!("Looks for slight improper modifications to the phrase `{phrase}`."),
41            None,
42        )
43    }
44
45    pub fn new_fixed_phrases(
46        phrase: impl IntoIterator<Item = impl AsRef<str>>,
47        correct_forms: impl IntoIterator<Item = impl ToString>,
48        message: impl ToString,
49        description: impl ToString,
50        lint_kind: Option<LintKind>,
51    ) -> Self {
52        let patterns = LongestMatchOf::new(
53            phrase
54                .into_iter()
55                .map(|p| {
56                    let expr: Box<dyn Expr> = Box::new(FixedPhrase::from_phrase(p.as_ref()));
57                    expr
58                })
59                .collect(),
60        );
61
62        Self::new(
63            Box::new(patterns),
64            correct_forms,
65            message,
66            description,
67            lint_kind,
68        )
69    }
70
71    pub fn new_fixed_phrase(
72        phrase: impl AsRef<str>,
73        correct_forms: impl IntoIterator<Item = impl ToString>,
74        message: impl ToString,
75        description: impl ToString,
76        lint_kind: Option<LintKind>,
77    ) -> Self {
78        Self::new(
79            Box::new(FixedPhrase::from_phrase(phrase.as_ref())),
80            correct_forms,
81            message,
82            description,
83            lint_kind,
84        )
85    }
86
87    pub fn new_closed_compound(phrase: impl AsRef<str>, correct_form: impl ToString) -> Self {
88        let message = format!(
89            "Did you mean the closed compound `{}`?",
90            correct_form.to_string()
91        );
92
93        let description = format!(
94            "Looks for incorrect spacing inside the closed compound `{}`.",
95            correct_form.to_string()
96        );
97
98        Self::new_fixed_phrase(
99            phrase,
100            [correct_form],
101            message,
102            description,
103            Some(LintKind::Miscellaneous),
104        )
105    }
106}
107
108impl ExprLinter for MapPhraseLinter {
109    type Unit = Chunk;
110
111    fn expr(&self) -> &dyn Expr {
112        self.expr.as_ref()
113    }
114
115    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
116        let span = matched_tokens.span()?;
117        let matched_text = span.get_content(source);
118
119        Some(Lint {
120            span,
121            lint_kind: self.lint_kind,
122            suggestions: self
123                .correct_forms
124                .iter()
125                .map(|correct_form| {
126                    Suggestion::replace_with_match_case(
127                        correct_form.chars().collect(),
128                        matched_text,
129                    )
130                })
131                .collect(),
132            message: self.message.to_string(),
133            priority: 31,
134        })
135    }
136
137    fn description(&self) -> &str {
138        self.description.as_str()
139    }
140}