Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
duplicate_strings.rs

1use wowlab_types::sim::FastMap;
2
3use crate::{Example, Violation, languages::workspace::WorkspaceCtx, violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "long string repeated three times",
9        code: "const A: &str = \"a deliberately long repeated fixture string value\";\nconst B: &str = \"a deliberately long repeated fixture string value\";\nconst C: &str = \"a deliberately long repeated fixture string value\";",
10        pass: false,
11    },
12    Example {
13        label: "only two occurrences",
14        code: "const A: &str = \"a deliberately long repeated fixture string value\";\nconst B: &str = \"a deliberately long repeated fixture string value\";",
15        pass: true,
16    },
17    Example {
18        label: "short strings are exempt",
19        code: "const A: &str = \"short\"; const B: &str = \"short\"; const C: &str = \"short\";",
20        pass: true,
21    },
22];
23
24crate::workspace_rule!(
25    duplicate_strings,
26    "Find long string literals repeated across files; full-workspace runs are authoritative.",
27    "Repeated long literals should have one named source of truth or a shared fixture.",
28    Low,
29    params {
30        min_chars: i64 = 40,
31        min_occurrences: i64 = 3
32    },
33);
34
35fn check_duplicate_strings(ctx: &WorkspaceCtx<'_>) -> Vec<Violation> {
36    let min_chars = ctx.config.get_usize("rust_duplicate_strings", &PARAMS[0]);
37    let min_occurrences = ctx.config.get_usize("rust_duplicate_strings", &PARAMS[1]);
38    let mut occurrences = FastMap::<&str, Vec<(&str, usize)>>::default();
39
40    for file in ctx.files {
41        for string in &file.strings {
42            let content = string
43                .value
44                .strip_prefix('"')
45                .and_then(|value| value.strip_suffix('"'))
46                .unwrap_or(&string.value);
47
48            if content.chars().count() >= min_chars {
49                occurrences
50                    .entry(&string.value)
51                    .or_default()
52                    .push((&file.rel, string.line));
53            }
54        }
55    }
56
57    let mut violations = Vec::new();
58
59    for locations in occurrences
60        .values_mut()
61        .filter(|locations| locations.len() >= min_occurrences)
62    {
63        locations.sort_unstable();
64        let Some(&(first_rel, first_line)) = locations.first() else {
65            continue;
66        };
67
68        for &(rel, line) in locations.iter().skip(1) {
69            violations.push(violation(rel, line, format!("duplicate long string; hoist to a shared const (first at {first_rel}:{first_line})")));
70        }
71    }
72
73    violations
74}
75
76crate::tidy_workspace_test!(check_duplicate_strings, {
77    crate::example_tests!(EXAMPLES, check_duplicate_strings);
78});