wowlab_tidy/languages/rust/rules/hygiene/
expect_over_allow.rs1use 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});