harper_core/linting/
inflected_verb_after_to.rs1use 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 if metadata.is_adjective() || metadata.is_verb() {
86 return ToVerbExpects::ExpectsInfinitive;
87 }
88 }
89 } else {
90 return ToVerbExpects::ExpectsInfinitive;
92 }
93 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 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 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 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 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 assert_lint_count(
299 "Comparison to Expected Results",
300 InflectedVerbAfterTo::new(FstDictionary::curated()),
301 0,
302 );
303 }
304}