harper_core/linting/
adjective_of_a.rs

1use super::{Lint, LintKind, Linter, Suggestion};
2use crate::{Document, Span, TokenStringExt};
3
4/// Detect sequences of words of the form "adjective of a".
5#[derive(Debug, Clone, Copy, Default)]
6pub struct AdjectiveOfA;
7
8const ADJECTIVE_WHITELIST: &[&str] = &["bad", "big", "good", "large", "long", "vague"];
9
10const CONTEXT_WORDS: &[&str] = &[
11    "as", "how", // but "how much of a"
12    "that", "this", "too",
13];
14
15const ADJECTIVE_BLACKLIST: &[&str] = &["much", "part"];
16
17fn has_context_word(document: &Document, adj_idx: usize) -> bool {
18    if adj_idx < 2 {
19        // Need at least 2 tokens before the adjective (word + space)
20        return false;
21    }
22
23    // Get the token before the adjective (should be a space)
24    if let Some(space_token) = document.get_token(adj_idx - 1) {
25        if !space_token.kind.is_whitespace() {
26            return false;
27        }
28
29        // Get the token before the space (should be our context word)
30        if let Some(word_token) = document.get_token(adj_idx - 2) {
31            if !word_token.kind.is_word() {
32                return false;
33            }
34
35            let word = document.get_span_content_str(&word_token.span);
36
37            return CONTEXT_WORDS.iter().any(|&w| w.eq_ignore_ascii_case(&word));
38        }
39    }
40
41    false
42}
43
44fn is_good_adjective(word: &str) -> bool {
45    ADJECTIVE_WHITELIST
46        .iter()
47        .any(|&adj| word.eq_ignore_ascii_case(adj))
48}
49
50fn is_bad_adjective(word: &str) -> bool {
51    ADJECTIVE_BLACKLIST
52        .iter()
53        .any(|&adj| word.eq_ignore_ascii_case(adj))
54}
55
56impl Linter for AdjectiveOfA {
57    fn lint(&mut self, document: &Document) -> Vec<Lint> {
58        let mut lints = Vec::new();
59
60        for i in document.iter_adjective_indices() {
61            let adjective = document.get_token(i).unwrap();
62            let space_1 = document.get_token(i + 1);
63            let word_of = document.get_token(i + 2);
64            let space_2 = document.get_token(i + 3);
65            let a_or_an = document.get_token(i + 4);
66            let adj_str = document
67                .get_span_content_str(&adjective.span)
68                .to_lowercase();
69
70            // Only flag adjectives known to use this construction
71            // Unless we have a clearer context
72            if !is_good_adjective(&adj_str) && !has_context_word(document, i) {
73                continue;
74            }
75            // Some adjectives still create false positives even with the extra context
76            if is_bad_adjective(&adj_str) {
77                continue;
78            }
79
80            // Rule out comparatives and superlatives.
81
82            // Pros:
83            // "for the better of a day"
84            // "might not be the best of a given run"
85            // "Which brings me to my best of a bad situation."
86            //
87            // Cons:
88            // "see if you can give us a little better of an answer"
89            // "hopefully it won't be too much worse of a problem"
90            // "seems far worse of a result to me"
91            if adj_str.ends_with("er") || adj_str.ends_with("st") {
92                continue;
93            }
94            // Rule out present participles (e.g. "beginning of a")
95            // The -ing form of a verb acts as an adjective called a present participle
96            // and also acts as a noun called a gerund.
97            if adj_str.ends_with("ing") && (adjective.kind.is_noun() || adjective.kind.is_verb()) {
98                continue;
99            }
100
101            if space_1.is_none() || word_of.is_none() || space_2.is_none() || a_or_an.is_none() {
102                continue;
103            }
104            let space_1 = space_1.unwrap();
105            if !space_1.kind.is_whitespace() {
106                continue;
107            }
108            let word_of = word_of.unwrap();
109            if !word_of.kind.is_word() {
110                continue;
111            }
112            let word_of = document.get_span_content_str(&word_of.span).to_lowercase();
113            if word_of != "of" {
114                continue;
115            }
116            let space_2 = space_2.unwrap();
117            if !space_2.kind.is_whitespace() {
118                continue;
119            }
120            let a_or_an = a_or_an.unwrap();
121            if !a_or_an.kind.is_word() {
122                continue;
123            }
124            let a_or_an_str = document.get_span_content_str(&a_or_an.span).to_lowercase();
125            if a_or_an_str != "a" && a_or_an_str != "an" {
126                continue;
127            }
128
129            // Whitespace may differ, add the other replacement if so
130            let mut sugg_1 = Vec::new();
131            sugg_1.extend_from_slice(document.get_span_content(&adjective.span));
132            sugg_1.extend_from_slice(document.get_span_content(&space_1.span));
133            sugg_1.extend_from_slice(document.get_span_content(&a_or_an.span));
134
135            let mut sugg_2 = Vec::new();
136            sugg_2.extend_from_slice(document.get_span_content(&adjective.span));
137            sugg_2.extend_from_slice(document.get_span_content(&space_2.span));
138            sugg_2.extend_from_slice(document.get_span_content(&a_or_an.span));
139
140            let mut suggestions = vec![Suggestion::ReplaceWith(sugg_1.clone())];
141            if sugg_1 != sugg_2 {
142                suggestions.push(Suggestion::ReplaceWith(sugg_2));
143            }
144
145            lints.push(Lint {
146                span: Span::new(adjective.span.start, a_or_an.span.end),
147                lint_kind: LintKind::Style,
148                suggestions,
149                message: "The word `of` is not needed here.".to_string(),
150                priority: 63,
151            });
152        }
153
154        lints
155    }
156
157    fn description(&self) -> &str {
158        "This rule looks for sequences of words of the form `adjective of a`."
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::AdjectiveOfA;
165    use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
166
167    #[test]
168    fn correct_large_of_a() {
169        assert_suggestion_result(
170            "Yeah I'm using as large of a batch size as I can on this machine",
171            AdjectiveOfA,
172            "Yeah I'm using as large a batch size as I can on this machine",
173        )
174    }
175
176    #[test]
177    fn correct_bad_of_an() {
178        assert_suggestion_result(
179            "- If forking is really that bad of an option, let's first decide where to put this.",
180            AdjectiveOfA,
181            "- If forking is really that bad an option, let's first decide where to put this.",
182        );
183    }
184
185    #[test]
186    fn dont_flag_comparative() {
187        assert_lint_count(
188            "I only worked with custom composer installers for the better of a day, so please excuse me if I missed a thing.",
189            AdjectiveOfA,
190            0,
191        );
192    }
193
194    #[test]
195    fn dont_flag_superlative() {
196        assert_lint_count(
197            "I am trying to use composites to visualize the worst of a set of metrics.",
198            AdjectiveOfA,
199            0,
200        );
201    }
202
203    #[test]
204    fn dont_flag_kind() {
205        // Adjective as in "a kind person" vs noun as in "A kind of person"
206        assert_lint_count(
207            "Log.txt file automatic creation in PWD is kind of an anti-feature",
208            AdjectiveOfA,
209            0,
210        );
211    }
212
213    #[test]
214    fn dont_flag_part() {
215        // Can be an adjective in e.g. "He is just part owner"
216        assert_lint_count(
217            "cannot delete a food that is no longer part of a recipe",
218            AdjectiveOfA,
219            0,
220        );
221    }
222
223    #[test]
224    fn dont_flag_much() {
225        // "much of" is correct idiomatic usage
226        assert_lint_count(
227            "How much of a performance impact when switching from rails to rails-api ?",
228            AdjectiveOfA,
229            0,
230        );
231    }
232
233    #[test]
234    fn dont_flag_part_uppercase() {
235        // Can be an adjective in e.g. "Part man, part machine"
236        assert_lint_count(
237            "Quarkus Extension as Part of a Project inside a Monorepo?",
238            AdjectiveOfA,
239            0,
240        );
241    }
242
243    #[test]
244    fn dont_flag_all_of() {
245        // "all of" is correct idiomatic usage
246        assert_lint_count(
247            "This repository is deprecated. All of its content and history has been moved.",
248            AdjectiveOfA,
249            0,
250        );
251    }
252
253    #[test]
254    fn dont_flag_inside() {
255        // "inside of" is idiomatic usage
256        assert_lint_count(
257            "Michael and Brock sat inside of a diner in Brandon",
258            AdjectiveOfA,
259            0,
260        );
261    }
262
263    #[test]
264    fn dont_flag_out() {
265        // "out of" is correct idiomatic usage
266        assert_lint_count(
267            "not only would he potentially be out of a job and back to sort of poverty",
268            AdjectiveOfA,
269            0,
270        );
271    }
272
273    #[test]
274    fn dont_flag_full() {
275        // "full of" is correct idiomatic usage
276        assert_lint_count(
277            "fortunately I happen to have this Tupperware full of an unceremoniously disassembled LED Mac Mini",
278            AdjectiveOfA,
279            0,
280        );
281    }
282
283    #[test]
284    fn dont_flag_something() {
285        // Can be a noun in e.g. "a certain something"
286        assert_lint_count(
287            "Well its popularity seems to be taking something of a dip right now.",
288            AdjectiveOfA,
289            0,
290        );
291    }
292
293    #[test]
294    fn dont_flag_short() {
295        // Can be a noun in e.g. "use a multimeter to find the short"
296        assert_lint_count(
297            "I found one Youtube short of an indonesian girl.",
298            AdjectiveOfA,
299            0,
300        )
301    }
302
303    #[test]
304    fn dont_flag_bottom() {
305        // Can be an adjective in e.g. "bottom bunk"
306        assert_lint_count(
307            "When leaves are just like coming out individually from the bottom of a fruit.",
308            AdjectiveOfA,
309            0,
310        )
311    }
312
313    #[test]
314    fn dont_flag_left() {
315        // Can be an adjective in e.g. "left hand"
316        assert_lint_count("and what is left of a 12vt coil", AdjectiveOfA, 0)
317    }
318
319    #[test]
320    fn dont_flag_full_uppercase() {
321        assert_lint_count("Full of a bunch varnish like we get.", AdjectiveOfA, 0);
322    }
323
324    #[test]
325    fn dont_flag_head() {
326        // Can be an adjective in e.g. "the head cook"
327        assert_lint_count(
328            "You need to get out if you're the head of an education department and you're not using AI",
329            AdjectiveOfA,
330            0,
331        );
332    }
333
334    #[test]
335    fn dont_flag_middle() {
336        // Can be an adjective in e.g. "middle child"
337        assert_lint_count(
338            "just to get to that part in the middle of a blizzard",
339            AdjectiveOfA,
340            0,
341        );
342    }
343
344    #[test]
345    fn dont_flag_chance() {
346        // Can be an adjective in e.g. "a chance encounter"
347        assert_lint_count(
348            "products that you overpay for because there are subtle details in the terms and conditions that reduce the size or chance of a payout.",
349            AdjectiveOfA,
350            0,
351        );
352    }
353
354    #[test]
355    fn dont_flag_potential() {
356        // Can be an adjective in e.g. "a potential candidate"
357        assert_lint_count(
358            "People that are happy to accept it for the potential of a reward.",
359            AdjectiveOfA,
360            0,
361        );
362    }
363
364    #[test]
365    fn dont_flag_sound() {
366        // Can be an adjective in e.g. "sound advice"
367        assert_lint_count("the sound of an approaching Krampus", AdjectiveOfA, 0);
368    }
369
370    #[test]
371    fn dont_flag_rid() {
372        // I removed the `5` flag from `rid` in `dictionary.dict`
373        // because dictionaries say the sense is archaic.
374        assert_lint_count("I need to get rid of a problem", AdjectiveOfA, 0);
375    }
376
377    #[test]
378    fn dont_flag_precision() {
379        // Can be an adjective in e.g. "a precision instrument"
380        assert_lint_count(
381            "a man whose crew cut has the precision of a targeted drone strike",
382            AdjectiveOfA,
383            0,
384        );
385    }
386
387    #[test]
388    fn dont_flag_back() {
389        // Can be an adjective in e.g. "back door"
390        assert_lint_count(
391            "a man whose crew cut has the back of a targeted drone strike",
392            AdjectiveOfA,
393            0,
394        );
395    }
396
397    #[test]
398    fn dont_flag_emblematic() {
399        // "emblematic of" is correct idiomatic usage
400        assert_lint_count(
401            "... situation was emblematic of a publication that ...",
402            AdjectiveOfA,
403            0,
404        );
405    }
406
407    #[test]
408    fn dont_flag_half() {
409        // Can be an adjective in e.g. "half man, half machine"
410        assert_lint_count("And now I only have half of a CyberTruck", AdjectiveOfA, 0);
411    }
412
413    #[test]
414    fn dont_flag_bit() {
415        // Technically also an adj as in "that guy's bit - he'll turn into a zombie"
416        assert_lint_count("we ran into a bit of an issue", AdjectiveOfA, 0);
417    }
418
419    #[test]
420    fn dont_flag_dream() {
421        // Can be an adjective in e.g. "we built our dream house"
422        assert_lint_count("When the dream of a united Europe began", AdjectiveOfA, 0);
423    }
424
425    #[test]
426    fn dont_flag_beginning() {
427        // Present participles have properties of adjectives, nouns, and verbs
428        assert_lint_count("That's the beginning of a conversation.", AdjectiveOfA, 0);
429    }
430
431    #[test]
432    fn dont_flag_side() {
433        // Can be an adjective in e.g. "via a side door"
434        assert_lint_count(
435            "it hit the barrier on the side of a highway",
436            AdjectiveOfA,
437            0,
438        );
439    }
440
441    #[test]
442    fn dont_flag_derivative() {
443        // Adj: "a derivative story", Noun: "stocks and derivatives"
444        assert_lint_count(
445            "Techniques for evaluating the *partial derivative of a function",
446            AdjectiveOfA,
447            0,
448        )
449    }
450
451    #[test]
452    fn dont_flag_equivalent() {
453        assert_lint_count(
454            "Rust's equivalent of a switch statement is a match expression",
455            AdjectiveOfA,
456            0,
457        );
458    }
459
460    #[test]
461    fn dont_flag_up() {
462        assert_lint_count(
463            "Yeah gas is made up of a bunch of teenytiny particles all moving around.",
464            AdjectiveOfA,
465            0,
466        );
467    }
468
469    #[test]
470    fn dont_flag_eighth() {
471        assert_lint_count(
472            "It's about an eighth of an inch or whatever",
473            AdjectiveOfA,
474            0,
475        );
476    }
477
478    #[test]
479    fn dont_flag_shy() {
480        assert_lint_count(
481            "... or just shy of a third of the country's total trade deficit.",
482            AdjectiveOfA,
483            0,
484        );
485    }
486
487    #[test]
488    fn dont_flag_fun() {
489        assert_lint_count(
490            "Remember that $4,000 Hermes horse bag I was making fun of a little while ago.",
491            AdjectiveOfA,
492            0,
493        );
494    }
495
496    #[test]
497    fn dont_flag_off() {
498        // Can be an adjective in e.g. "The TV is off".
499        // This should be in a different lint that handles based on/off/off of.
500        assert_lint_count(
501            "can't identify a person based off of an IP from 10 years ago",
502            AdjectiveOfA,
503            0,
504        );
505    }
506
507    #[test]
508    fn dont_flag_borderline_of() {
509        assert_lint_count(
510            "it's very very on the borderline of a rock pop ballad",
511            AdjectiveOfA,
512            0,
513        );
514    }
515
516    #[test]
517    fn dont_flag_light() {
518        assert_lint_count("The light of a star.", AdjectiveOfA, 0);
519    }
520
521    #[test]
522    fn dont_flag_multiple() {
523        assert_lint_count(
524            "The image needs to be a multiple of a certain size.",
525            AdjectiveOfA,
526            0,
527        );
528    }
529
530    #[test]
531    fn dont_flag_red() {
532        assert_lint_count("The red of a drop of blood.", AdjectiveOfA, 0);
533    }
534
535    #[test]
536    fn dont_flag_top() {
537        assert_lint_count("The top of a hill.", AdjectiveOfA, 0);
538    }
539
540    #[test]
541    fn dont_flag_slack() {
542        assert_lint_count(
543            "They've been picking up the slack of a federal government mostly dominated by whatever this is.",
544            AdjectiveOfA,
545            0,
546        );
547    }
548
549    #[test]
550    fn dont_flag_illustrative() {
551        assert_lint_count(
552            "Yet, the fact that they clearly give a one-sided account of most of their case studies is illustrative of a bias.",
553            AdjectiveOfA,
554            0,
555        );
556    }
557
558    #[test]
559    fn dont_flag_perspective() {
560        assert_lint_count(
561            "I always assess software by looking at it from the perspective of a new user.",
562            AdjectiveOfA,
563            0,
564        );
565    }
566
567    #[test]
568    fn correct_too_large_of_a() {
569        assert_suggestion_result(
570            "Warn users if setting too large of a session object",
571            AdjectiveOfA,
572            "Warn users if setting too large a session object",
573        )
574    }
575
576    #[test]
577    fn correct_too_long_of_a() {
578        assert_suggestion_result(
579            "An Org Role with Too Long of a Name Hides Delete Option",
580            AdjectiveOfA,
581            "An Org Role with Too Long a Name Hides Delete Option",
582        )
583    }
584
585    #[test]
586    fn correct_too_big_of_a() {
587        assert_suggestion_result(
588            "StepButton has too big of a space to click",
589            AdjectiveOfA,
590            "StepButton has too big a space to click",
591        )
592    }
593
594    #[test]
595    fn correct_too_vague_of_a() {
596        assert_suggestion_result(
597            "\"No Speech provider is registered.\" is too vague of an error",
598            AdjectiveOfA,
599            "\"No Speech provider is registered.\" is too vague an error",
600        )
601    }
602
603    #[test]
604    fn correct_too_dumb_of_a() {
605        assert_suggestion_result(
606            "Hopefully this isn't too dumb of a question.",
607            AdjectiveOfA,
608            "Hopefully this isn't too dumb a question.",
609        )
610    }
611
612    #[test]
613    fn correct_how_important_of_a() {
614        assert_suggestion_result(
615            "This should tell us how important of a use case that is and how often writing a type literal in a case is deliberate.",
616            AdjectiveOfA,
617            "This should tell us how important a use case that is and how often writing a type literal in a case is deliberate.",
618        )
619    }
620
621    #[test]
622    fn correct_that_rare_of_an() {
623        assert_suggestion_result(
624            "so making changes isn't that rare of an occurrence for me.",
625            AdjectiveOfA,
626            "so making changes isn't that rare an occurrence for me.",
627        )
628    }
629
630    #[test]
631    fn correct_as_important_of_a() {
632        assert_suggestion_result(
633            "Might be nice to have it draggable from other places as well, but not as important of a bug anymore.",
634            AdjectiveOfA,
635            "Might be nice to have it draggable from other places as well, but not as important a bug anymore.",
636        )
637    }
638
639    #[test]
640    fn correct_too_short_of_a() {
641        assert_suggestion_result(
642            "I login infrequently as well and 6 months is too short of a time.",
643            AdjectiveOfA,
644            "I login infrequently as well and 6 months is too short a time.",
645        )
646    }
647
648    #[test]
649    fn correct_that_common_of_a() {
650        assert_suggestion_result(
651            "that common of a name for a cluster role its hard to rule out",
652            AdjectiveOfA,
653            "that common a name for a cluster role its hard to rule out",
654        )
655    }
656
657    #[test]
658    fn correct_as_great_of_an() {
659        assert_suggestion_result(
660            "the w factor into the u factor to as great of an extent as possible.",
661            AdjectiveOfA,
662            "the w factor into the u factor to as great an extent as possible.",
663        )
664    }
665
666    #[test]
667    fn correct_too_uncommon_of_a() {
668        assert_suggestion_result(
669            "but this is probably too uncommon of a practice to be the default",
670            AdjectiveOfA,
671            "but this is probably too uncommon a practice to be the default",
672        )
673    }
674}