harper_core/linting/
modal_of.rs

1use crate::{
2    Lrc, Token, TokenStringExt,
3    patterns::{EitherPattern, Pattern, SequencePattern, WordSet},
4};
5
6use super::{Lint, LintKind, PatternLinter, Suggestion};
7
8pub struct ModalOf {
9    pattern: Box<dyn Pattern>,
10}
11
12impl Default for ModalOf {
13    fn default() -> Self {
14        // Note 1. "shan't of" is plausible but very unlikely
15        // Note 2. "had of" has trickier false positives and is less common anyway
16        // "The only other report we've had of this kind of problem ..."
17        // "The code I had of this used to work fine ..."
18        let modals = ["could", "might", "must", "should", "would"];
19        let mut words = WordSet::new(&modals);
20        modals.iter().for_each(|word| {
21            words.add(&format!("{}n't", word));
22        });
23
24        let modal_of = Lrc::new(
25            SequencePattern::default()
26                .then(words)
27                .then_whitespace()
28                .t_aco("of"),
29        );
30
31        let ws_course = Lrc::new(SequencePattern::default().then_whitespace().t_aco("course"));
32
33        let modal_of_course = Lrc::new(
34            SequencePattern::default()
35                .then(modal_of.clone())
36                .then(ws_course.clone()),
37        );
38
39        let anyword_might_of = Lrc::new(
40            SequencePattern::default()
41                .then_any_word()
42                .then_whitespace()
43                .t_aco("might")
44                .then_whitespace()
45                .t_aco("of"),
46        );
47
48        let anyword_might_of_course = Lrc::new(
49            SequencePattern::default()
50                .then(anyword_might_of.clone())
51                .then(ws_course.clone()),
52        );
53
54        Self {
55            pattern: Box::new(EitherPattern::new(vec![
56                Box::new(anyword_might_of_course),
57                Box::new(modal_of_course),
58                Box::new(anyword_might_of),
59                Box::new(modal_of),
60            ])),
61        }
62    }
63}
64
65impl PatternLinter for ModalOf {
66    fn pattern(&self) -> &dyn Pattern {
67        self.pattern.as_ref()
68    }
69
70    fn match_to_lint(&self, matched_toks: &[Token], source_chars: &[char]) -> Option<Lint> {
71        let modal_index = match matched_toks.len() {
72            // Without context, always an error from the start
73            3 => 0,
74            5 => {
75                // False positives: modal _ of _ course / adj. _ might _ of / art. _ might _ of
76                let w3_text = matched_toks
77                    .last()
78                    .unwrap()
79                    .span
80                    .get_content(source_chars)
81                    .iter()
82                    .collect::<String>();
83                if w3_text.as_str() != "of" {
84                    return None;
85                }
86                let w1_kind = &matched_toks.first().unwrap().kind;
87                // the might of something, great might of something
88                if w1_kind.is_adjective() || w1_kind.is_determiner() {
89                    return None;
90                }
91                // not a false positive, skip context before
92                2
93            }
94            // False positive: <word> _ might _ of _ course
95            7 => return None,
96            _ => unreachable!(),
97        };
98
99        let span_modal_of = matched_toks[modal_index..modal_index + 3].span().unwrap();
100
101        let modal_have = format!(
102            "{} have",
103            matched_toks[modal_index]
104                .span
105                .get_content_string(source_chars)
106        )
107        .chars()
108        .collect();
109
110        Some(Lint {
111            span: span_modal_of,
112            lint_kind: LintKind::WordChoice,
113            suggestions: vec![Suggestion::replace_with_match_case(
114                modal_have,
115                span_modal_of.get_content(source_chars),
116            )],
117            message: "Use `have` rather than `of` here.".to_string(),
118            priority: 126,
119        })
120    }
121
122    fn description(&self) -> &'static str {
123        "Detects `of` mistakenly used with `would`, `could`, `should`, etc."
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::ModalOf;
130    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
131
132    // atomic unit tests
133
134    #[test]
135    fn test_lowercase() {
136        assert_suggestion_result("could of", ModalOf::default(), "could have");
137    }
138
139    #[test]
140    fn test_negative() {
141        assert_suggestion_result("mightn't of", ModalOf::default(), "mightn't have");
142    }
143
144    #[test]
145    fn test_uppercase_negative() {
146        assert_suggestion_result("Mustn't of", ModalOf::default(), "Mustn't have");
147    }
148
149    #[test]
150    fn test_false_positive_of_course() {
151        assert_lint_count("should of course", ModalOf::default(), 0);
152    }
153
154    #[test]
155    fn test_false_positive_the_might_of() {
156        assert_lint_count("the might of", ModalOf::default(), 0);
157    }
158
159    #[test]
160    fn test_false_positive_great_might_of() {
161        assert_lint_count("great might of", ModalOf::default(), 0);
162    }
163
164    #[test]
165    fn test_false_positive_capital_negative() {
166        assert_lint_count("Wouldn't of course", ModalOf::default(), 0);
167    }
168
169    // real-world tests
170
171    #[test]
172    fn test_buggy_implementation() {
173        assert_lint_count(
174            "... could of just been a buggy implementation",
175            ModalOf::default(),
176            1,
177        );
178    }
179
180    #[test]
181    fn test_missed_one() {
182        assert_lint_count(
183            "We already have a function ... that nedb can understand so we might of missed one.",
184            ModalOf::default(),
185            1,
186        );
187    }
188
189    #[test]
190    fn test_user_option() {
191        assert_lint_count(
192            "im more likely to believe you might of left in the 'user' option",
193            ModalOf::default(),
194            1,
195        );
196    }
197
198    #[test]
199    fn catches_must_of() {
200        assert_suggestion_result(
201            "Ah I must of missed that part.",
202            ModalOf::default(),
203            "Ah I must have missed that part.",
204        );
205    }
206
207    #[test]
208    fn catches_should_of() {
209        assert_lint_count(
210            "Yeah I should of just mentioned it should of been a for of.",
211            ModalOf::default(),
212            2,
213        );
214    }
215
216    #[test]
217    fn catches_would_of() {
218        assert_suggestion_result(
219            "now this issue would of caused hundreds of thousands of extra lines",
220            ModalOf::default(),
221            "now this issue would have caused hundreds of thousands of extra lines",
222        );
223    }
224
225    #[test]
226    fn doesnt_catch_you_could_of_course() {
227        assert_lint_count(
228            "You could of course explicit the else with each possibility",
229            ModalOf::default(),
230            0,
231        );
232    }
233
234    #[test]
235    fn doesnt_catch_compiler_could_of_course() {
236        assert_lint_count(
237            "The compiler could of course detect this too",
238            ModalOf::default(),
239            0,
240        );
241    }
242
243    #[test]
244    fn doesnt_catch_might_of_course_be() {
245        assert_lint_count(
246            "There might of course be other places where not implementing the IMemberSource might break ...",
247            ModalOf::default(),
248            0,
249        );
250    }
251
252    #[test]
253    fn doesnt_catch_not_a_must_of_course() {
254        assert_lint_count(
255            "Not a must of course if the convention should be .ts",
256            ModalOf::default(),
257            0,
258        );
259    }
260
261    #[test]
262    fn doesnt_catch_must_of_course_also() {
263        assert_lint_count(
264            "the schedular must of course also have run through",
265            ModalOf::default(),
266            0,
267        );
268    }
269
270    #[test]
271    fn doesnt_catch_should_of_course_not() {
272        assert_lint_count(
273            "not being local should of course not be supported",
274            ModalOf::default(),
275            0,
276        );
277    }
278
279    #[test]
280    fn doesnt_catch_would_of_course_just() {
281        assert_lint_count(
282            "I would of course just test this by compiling with MATX_MULTI_GPU=ON",
283            ModalOf::default(),
284            0,
285        );
286    }
287
288    #[test]
289    fn doesnt_catch_to_take_on_the_full_might_of_nato() {
290        assert_lint_count("To take on the full might of NATO.", ModalOf::default(), 0);
291    }
292
293    #[test]
294    fn doesnt_catch_mixed_case_of_course() {
295        assert_lint_count(
296            "... for now you could of Course put ...",
297            ModalOf::default(),
298            0,
299        );
300    }
301
302    #[test]
303    fn catches_mixed_case_could_of_put() {
304        assert_lint_count("... for now you could of Put ...", ModalOf::default(), 1);
305    }
306}