harper_core/linting/
inflected_verb_after_to.rs

1use super::{Lint, LintKind, Linter, Suggestion};
2use crate::char_string::CharStringExt;
3use crate::spell::Dictionary;
4use crate::{Document, Span, TokenStringExt};
5
6pub struct InflectedVerbAfterTo<T>
7where
8    T: Dictionary,
9{
10    dictionary: T,
11}
12
13impl<T: Dictionary> InflectedVerbAfterTo<T> {
14    pub fn new(dictionary: T) -> Self {
15        Self { dictionary }
16    }
17}
18
19impl<T: Dictionary> Linter for InflectedVerbAfterTo<T> {
20    fn lint(&mut self, document: &Document) -> Vec<Lint> {
21        let mut lints = Vec::new();
22        for pi in document.iter_preposition_indices() {
23            let prep = document.get_token(pi).unwrap();
24            let Some(space) = document.get_token(pi + 1) else {
25                continue;
26            };
27            let Some(word) = document.get_token(pi + 2) else {
28                continue;
29            };
30            if !space.kind.is_whitespace() || !word.kind.is_word() {
31                continue;
32            }
33            let prep_to = document.get_span_content(&prep.span);
34            if !prep_to.eq_ignore_ascii_case_chars(&['t', 'o']) {
35                continue;
36            }
37
38            let chars = document.get_span_content(&word.span);
39
40            if chars.len() < 4 {
41                continue;
42            }
43
44            let check_stem = |stem: &[char]| {
45                if let Some(metadata) = self.dictionary.get_word_metadata(stem)
46                    && metadata.is_verb()
47                    && !metadata.is_noun()
48                {
49                    return true;
50                }
51                false
52            };
53
54            let mut lint_from_stem = |stem: &[char]| {
55                lints.push(Lint {
56                    span: Span::new(prep.span.start, word.span.end),
57                    lint_kind: LintKind::WordChoice,
58                    message: "The base form of the verb is needed here.".to_string(),
59                    suggestions: vec![Suggestion::ReplaceWith(
60                        prep_to
61                            .iter()
62                            .chain([' '].iter())
63                            .chain(stem.iter())
64                            .copied()
65                            .collect(),
66                    )],
67                    ..Default::default()
68                });
69            };
70
71            #[derive(PartialEq)]
72            enum ToVerbExpects {
73                ExpectsInfinitive,
74                ExpectsNominal,
75            }
76
77            use ToVerbExpects::*;
78
79            let ed_specific_heuristics = || {
80                if let Some(prev) = document.get_next_word_from_offset(pi, -1) {
81                    let prev_chars = document.get_span_content(&prev.span);
82                    if let Some(metadata) = self.dictionary.get_word_metadata(prev_chars) {
83                        // adj: "able to" expects an infinitive verb
84                        // verb: "have/had/has/having to" expect an infinitive verb
85                        if metadata.is_adjective() || metadata.is_verb() {
86                            return ToVerbExpects::ExpectsInfinitive;
87                        }
88                    }
89                } else {
90                    // Assume a chunk beginning with "to" and a verb in -ed should expect an infinitive verb
91                    return ToVerbExpects::ExpectsInfinitive;
92                }
93                // Default assumption is that "to" is a preposition so a noun etc. should come after it
94                ToVerbExpects::ExpectsNominal
95            };
96
97            if chars.ends_with(&['e', 'd']) {
98                let ed = check_stem(&chars[..chars.len() - 2]);
99                if ed && ed_specific_heuristics() == ExpectsInfinitive {
100                    lint_from_stem(&chars[..chars.len() - 2]);
101                };
102                let d = check_stem(&chars[..chars.len() - 1]);
103                // Add -d specific heuristics when needed
104                if d {
105                    lint_from_stem(&chars[..chars.len() - 1]);
106                };
107            }
108            if chars.ends_with(&['e', 's']) {
109                let es = check_stem(&chars[..chars.len() - 2]);
110                // Add -es specific heuristics when needed
111                if es {
112                    lint_from_stem(&chars[..chars.len() - 2]);
113                };
114            }
115            if chars.ends_with(&['s']) {
116                let s = check_stem(&chars[..chars.len() - 1]);
117                // Add -s specific heuristics when needed
118                if s {
119                    lint_from_stem(&chars[..chars.len() - 1]);
120                };
121            }
122        }
123        lints
124    }
125
126    fn description(&self) -> &str {
127        "This rule looks for `to verb` where `verb` is not in the infinitive form."
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::InflectedVerbAfterTo;
134    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
135    use crate::spell::FstDictionary;
136
137    #[test]
138    fn dont_flag_to_check_both_verb_and_noun() {
139        assert_lint_count(
140            "to check",
141            InflectedVerbAfterTo::new(FstDictionary::curated()),
142            0,
143        );
144    }
145
146    #[test]
147    fn dont_flag_to_checks_both_verb_and_noun() {
148        assert_lint_count(
149            "to checks",
150            InflectedVerbAfterTo::new(FstDictionary::curated()),
151            0,
152        );
153    }
154
155    #[test]
156    fn dont_flag_to_cheques_not_a_verb() {
157        assert_lint_count(
158            "to cheques",
159            InflectedVerbAfterTo::new(FstDictionary::curated()),
160            0,
161        );
162    }
163
164    #[test]
165    #[ignore = "-ing forms can act as nouns, current heuristics cannot distinguish"]
166    fn flag_to_checking() {
167        assert_lint_count(
168            "to checking",
169            InflectedVerbAfterTo::new(FstDictionary::curated()),
170            1,
171        );
172    }
173
174    #[test]
175    fn dont_flag_check_ed() {
176        assert_lint_count(
177            "to checked",
178            InflectedVerbAfterTo::new(FstDictionary::curated()),
179            0,
180        );
181    }
182
183    #[test]
184    fn dont_flag_noun_belief_s() {
185        assert_lint_count(
186            "to beliefs",
187            InflectedVerbAfterTo::new(FstDictionary::curated()),
188            0,
189        );
190    }
191
192    #[test]
193    fn dont_flag_noun_meat_s() {
194        assert_lint_count(
195            "to meats",
196            InflectedVerbAfterTo::new(FstDictionary::curated()),
197            0,
198        );
199    }
200
201    #[test]
202    #[ignore = "can't check yet. 'capture' is noun as well as verb. \"to nouns\" is good English. we can't disambiguate verbs from nouns."]
203    fn check_993_suggestions() {
204        assert_suggestion_result(
205            "A location-agnostic structure that attempts to captures the context and content that a Lint occurred.",
206            InflectedVerbAfterTo::new(FstDictionary::curated()),
207            "A location-agnostic structure that attempts to capture the context and content that a Lint occurred.",
208        );
209    }
210
211    #[test]
212    fn dont_flag_embarrass_not_in_dictionary() {
213        assert_lint_count(
214            "Second I'm going to embarrass you for a.",
215            InflectedVerbAfterTo::new(FstDictionary::curated()),
216            0,
217        );
218    }
219
220    #[test]
221    fn corrects_exist_s() {
222        assert_suggestion_result(
223            "A valid solution is expected to exists.",
224            InflectedVerbAfterTo::new(FstDictionary::curated()),
225            "A valid solution is expected to exist.",
226        );
227    }
228
229    #[test]
230    #[ignore = "can't check yet. 'catch' is noun as well as verb. 'to nouns' is good English. we can't disambiguate verbs from nouns."]
231    fn corrects_es_ending() {
232        assert_suggestion_result(
233            "I need it to catches every exception.",
234            InflectedVerbAfterTo::new(FstDictionary::curated()),
235            "I need it to catch every exception.",
236        );
237    }
238
239    #[test]
240    fn corrects_ed_ending() {
241        assert_suggestion_result(
242            "I had to expanded my horizon.",
243            InflectedVerbAfterTo::new(FstDictionary::curated()),
244            "I had to expand my horizon.",
245        );
246    }
247
248    #[test]
249    fn flags_expire_d() {
250        assert_lint_count(
251            "I didn't know it was going to expired.",
252            InflectedVerbAfterTo::new(FstDictionary::curated()),
253            1,
254        );
255    }
256
257    #[test]
258    fn corrects_explain_ed() {
259        assert_suggestion_result(
260            "To explained the rules to the team.",
261            InflectedVerbAfterTo::new(FstDictionary::curated()),
262            "To explain the rules to the team.",
263        );
264    }
265
266    #[test]
267    #[ignore = "can't check yet. surprisingly, 'explore' is noun as well as verb. 'to nouns' is good English. we can't disambiguate verbs from nouns."]
268    fn corrects_explor_ed() {
269        assert_suggestion_result(
270            "I went to explored distant galaxies.",
271            InflectedVerbAfterTo::new(FstDictionary::curated()),
272            "I went to explore distant galaxies.",
273        );
274    }
275
276    #[test]
277    fn cant_flag_express_ed_also_noun() {
278        assert_lint_count(
279            "I failed to clearly expressed my point.",
280            InflectedVerbAfterTo::new(FstDictionary::curated()),
281            0,
282        );
283    }
284
285    #[test]
286    fn correct_feign_ed() {
287        // adj "able" before "to" works with "to", making "to" part of an infinitive verb
288        assert_suggestion_result(
289            "I was able to feigned ignorance.",
290            InflectedVerbAfterTo::new(FstDictionary::curated()),
291            "I was able to feign ignorance.",
292        );
293    }
294
295    #[test]
296    fn issue_241() {
297        // Hypothesis: when before "to" is not an adj, assume "to" is a preposition
298        assert_lint_count(
299            "Comparison to Expected Results",
300            InflectedVerbAfterTo::new(FstDictionary::curated()),
301            0,
302        );
303    }
304}