1use super::{Lint, LintKind, Linter, Suggestion};
2use crate::{Document, Span, TokenStringExt};
3
4#[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", "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 return false;
21 }
22
23 if let Some(space_token) = document.get_token(adj_idx - 1) {
25 if !space_token.kind.is_whitespace() {
26 return false;
27 }
28
29 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 if !is_good_adjective(&adj_str) && !has_context_word(document, i) {
73 continue;
74 }
75 if is_bad_adjective(&adj_str) {
77 continue;
78 }
79
80 if adj_str.ends_with("er") || adj_str.ends_with("st") {
92 continue;
93 }
94 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_lint_count("the sound of an approaching Krampus", AdjectiveOfA, 0);
368 }
369
370 #[test]
371 fn dont_flag_rid() {
372 assert_lint_count("I need to get rid of a problem", AdjectiveOfA, 0);
375 }
376
377 #[test]
378 fn dont_flag_precision() {
379 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 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 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 assert_lint_count("And now I only have half of a CyberTruck", AdjectiveOfA, 0);
411 }
412
413 #[test]
414 fn dont_flag_bit() {
415 assert_lint_count("we ran into a bit of an issue", AdjectiveOfA, 0);
417 }
418
419 #[test]
420 fn dont_flag_dream() {
421 assert_lint_count("When the dream of a united Europe began", AdjectiveOfA, 0);
423 }
424
425 #[test]
426 fn dont_flag_beginning() {
427 assert_lint_count("That's the beginning of a conversation.", AdjectiveOfA, 0);
429 }
430
431 #[test]
432 fn dont_flag_side() {
433 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 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 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}