Skip to main content

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

1use crate::{
2    Example, FileCtx, Violation,
3    infra::{helpers, parse, scanner},
4    violation,
5};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "outer allow",
11        code: "#[allow(dead_code)]\nfn f() {}",
12        pass: false,
13    },
14    Example {
15        label: "inner allow",
16        code: "#![allow(clippy::too_many_arguments)]",
17        pass: false,
18    },
19    Example {
20        label: "allow with comment still flagged",
21        code: "#[allow(dead_code)] // webhook response fields",
22        pass: false,
23    },
24    Example {
25        label: "expect with reason",
26        code: "#[expect(dead_code, reason = \"kept for wire format\")]\nfn f() {}",
27        pass: true,
28    },
29    Example {
30        label: "allow inside macro_rules body",
31        code: "macro_rules! m {\n    () => {\n        #[allow(unused)]\n        fn f() {}\n    };\n}",
32        pass: true,
33    },
34    Example {
35        label: "allow after macro_rules closed",
36        code: "macro_rules! m {\n    () => {};\n}\n#[allow(unused)]\nfn f() {}",
37        pass: false,
38    },
39    Example {
40        label: "allow in comment",
41        code: "// #[allow(dead_code)]",
42        pass: true,
43    },
44    Example {
45        label: "allow in string literal",
46        code: "fn f() { let s = \"#[allow(dead_code)]\"; }",
47        pass: true,
48    },
49];
50
51crate::line_rule!(
52    expect_over_allow,
53    "Flag `#[allow(...)]` in hand-written code — use `#[expect(..., reason = \"...\")]` instead.",
54    "#[expect] warns when the suppressed lint no longer fires, preventing stale suppressions from accumulating; #[allow] silences forever (clippy analogue: allow_attributes).",
55    Medium,
56);
57
58fn check_expect_over_allow(ctx: &FileCtx<'_>) -> Vec<Violation> {
59    let mut out = Vec::new();
60    let mut in_macro = false;
61    let mut brace_depth: usize = 0;
62    let mut seen_brace = false;
63
64    for (i, line) in ctx.lines.iter().enumerate() {
65        let trimmed = line.trim();
66
67        if parse::is_comment(trimmed) {
68            continue;
69        }
70
71        let code = scanner::code_only(line);
72
73        if !in_macro && code.contains("macro_rules!") {
74            in_macro = true;
75            brace_depth = 0;
76            seen_brace = false;
77        }
78
79        if in_macro {
80            for ch in code.chars() {
81                match ch {
82                    '{' => {
83                        brace_depth += 1;
84                        seen_brace = true;
85                    }
86                    '}' => brace_depth = brace_depth.saturating_sub(1),
87                    _ => {}
88                }
89            }
90
91            if seen_brace && brace_depth == 0 {
92                in_macro = false;
93            }
94
95            continue;
96        }
97
98        if helpers::contains_outside_strings(line, "#[allow(")
99            || helpers::contains_outside_strings(line, "#![allow(")
100        {
101            out.push(violation(
102                ctx.rel,
103                i + 1,
104                "#[allow(...)] — use #[expect(..., reason = \"...\")] so stale suppressions warn",
105            ));
106        }
107    }
108
109    out
110}
111
112crate::tidy_test!(check_expect_over_allow, {
113    crate::example_tests!(EXAMPLES, check_expect_over_allow);
114});