Skip to main content

wowlab_tidy/languages/rust/rules/style/
duplicate_words.rs

1use 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});