wowlab_tidy/languages/rust/rules/style/
duplicate_words.rs1use winnow::token::take_until;
2
3use crate::{Example, FileCtx, Fix, Violation, infra::parse, violation};
4
5const ADJACENT_WORD_COUNT: usize = 2;
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "the the in doc comment",
11 code: "/// the the value",
12 pass: false,
13 },
14 Example {
15 label: "normal doc comment",
16 code: "/// the value",
17 pass: true,
18 },
19 Example {
20 label: "not a comment",
21 code: "let the_the = 1;",
22 pass: true,
23 },
24 Example {
25 label: "is is in comment",
26 code: "// is is",
27 pass: false,
28 },
29 Example {
30 label: "case insensitive duplicate",
31 code: "/// The the value",
32 pass: false,
33 },
34];
35
36crate::line_rule!(
37 duplicate_words,
38 "Flag repeated words in comments like `the the` or `is is`.",
39 "Repeated words like 'the the' are typos that slip past spell checkers and make documentation look sloppy.",
40 Low,
41 fix_duplicate_words,
42);
43
44fn check_duplicate_words(ctx: &FileCtx<'_>) -> Vec<Violation> {
45 let mut out = Vec::new();
46
47 for (i, line) in ctx.lines.iter().enumerate() {
48 let lineno = i + 1;
49 let trimmed = line.trim();
50 let Some(text) = parse::comment_content(trimmed) else {
51 continue;
52 };
53
54 let words: Vec<&str> = text.split_whitespace().collect();
55
56 for pair in words.windows(ADJACENT_WORD_COUNT) {
57 let &[first, second] = pair else { continue };
58
59 if first.eq_ignore_ascii_case(second) {
60 out.push(violation(
61 ctx.rel,
62 lineno,
63 format!("duplicate word \"{}\" in comment", first.to_lowercase()),
64 ));
65 break;
66 }
67 }
68 }
69
70 out
71}
72
73fn fix_duplicate_words(ctx: &FileCtx<'_>, v: &Violation) -> Option<Fix> {
74 let line = ctx.line(v.line)?;
75 let trimmed = line.trim();
76 let text = parse::comment_content(trimmed)?;
77
78 let words: Vec<&str> = text.split_whitespace().collect();
79 let pair = words.windows(ADJACENT_WORD_COUNT).find(|pair| {
80 let [first, second] = *pair else {
81 return false;
82 };
83
84 first.eq_ignore_ascii_case(second)
85 })?;
86 let &[first, second] = pair else { return None };
87 let mut input = line;
88 let before_word: &str = parse::try_parse(&mut input, take_until(0.., first))?;
89 let after_first = before_word.len() + first.len();
90 let remaining = line.get(after_first..)?;
91 let mut rem_input = remaining;
92 let gap: &str = parse::try_parse(&mut rem_input, take_until(0.., second))?;
93 let remove_end = after_first + gap.len() + second.len();
94
95 Some(Fix::replace_line(
96 v.line,
97 format!("{}{}", line.get(..after_first)?, line.get(remove_end..)?),
98 ))
99}
100
101crate::tidy_test!(check_duplicate_words, {
102 crate::example_tests!(EXAMPLES, check_duplicate_words);
103 crate::fix_tests!(line, check_duplicate_words, fix_duplicate_words);
104});