wowlab_tidy/languages/rust/rules/hygiene/
allow_reason.rs1use crate::{Example, FileCtx, Violation, infra::parse, violation};
2
3#[rustfmt::skip]
4const EXAMPLES: &[Example] = &[
5 Example {
6 label: "allow without comment",
7 code: "#[allow(dead_code)]",
8 pass: false,
9 },
10 Example {
11 label: "allow with inline comment",
12 code: "#[allow(dead_code)] // webhook response fields",
13 pass: true,
14 },
15 Example {
16 label: "allow with preceding comment",
17 code: "// DBC fields use PascalCase\n#[allow(non_snake_case)]",
18 pass: true,
19 },
20 Example {
21 label: "allow with reason argument",
22 code: "#[allow(dead_code, reason = \"webhook response fields\")]",
23 pass: true,
24 },
25 Example {
26 label: "expect without reason",
27 code: "#[expect(clippy::unused_async)]",
28 pass: false,
29 },
30 Example {
31 label: "expect with reason argument",
32 code: "#[expect(clippy::unused_async, reason = \"API fixed, will use I/O later\")]",
33 pass: true,
34 },
35 Example {
36 label: "multiline allow with reason argument",
37 code: "#[allow(\n dead_code,\n reason = \"webhook response fields\",\n)]",
38 pass: true,
39 },
40 Example {
41 label: "multiline expect with reason argument",
42 code: "#[expect(\n clippy::cast_precision_loss,\n reason = \"sample count enters the f64 domain\",\n)]",
43 pass: true,
44 },
45 Example {
46 label: "multiline module expect with reason argument",
47 code: "#![expect(\n clippy::doc_markdown,\n reason = \"generated documentation is external\",\n)]",
48 pass: true,
49 },
50 Example {
51 label: "multiline expect without reason",
52 code: "#[expect(\n clippy::cast_precision_loss,\n clippy::cast_sign_loss,\n)]",
53 pass: false,
54 },
55 Example {
56 label: "module-level allow without comment",
57 code: "#![allow(clippy::too_many_arguments)]",
58 pass: false,
59 },
60 Example {
61 label: "module-level allow with comment",
62 code: "// spell table has many parameters by design\n#![allow(clippy::too_many_arguments)]",
63 pass: true,
64 },
65 Example {
66 label: "non-allow attr",
67 code: "#[derive(Debug)]",
68 pass: true,
69 },
70 Example {
71 label: "deny attr",
72 code: "#[deny(unused)]",
73 pass: true,
74 },
75];
76
77crate::line_rule!(
78 allow_reason,
79 "Require a `reason = \"...\"` or comment explaining why `#[allow(...)]`/`#[expect(...)]` is used.",
80 "Unexplained lint overrides hide the intent behind suppressing a warning, making it unclear if the suppression is still needed.",
81);
82
83fn is_expect_attr(trimmed: &str) -> bool {
84 trimmed.starts_with("#[expect(") || trimmed.starts_with("#![expect(")
85}
86
87fn code_before_line_comment(line: &str) -> &str {
88 let bytes = line.as_bytes();
89 let mut index = 0;
90 let mut in_string = false;
91 let mut escaped = false;
92
93 while index < bytes.len() {
94 let Some(&byte) = bytes.get(index) else {
95 break;
96 };
97
98 if in_string {
99 if escaped {
100 escaped = false;
101 } else if byte == b'\\' {
102 escaped = true;
103 } else if byte == b'"' {
104 in_string = false;
105 }
106 } else if byte == b'"' {
107 in_string = true;
108 } else if byte == b'/' && bytes.get(index + 1) == Some(&b'/') {
109 return line.get(..index).unwrap_or(line);
110 }
111
112 index += 1;
113 }
114
115 line
116}
117
118fn contains_reason(attribute_line: &str) -> bool {
119 attribute_line.contains("reason = \"") || attribute_line.contains("reason=\"")
120}
121
122fn attribute_span_has_reason(lines: &[&str], start: usize) -> bool {
123 let mut depth = 0_u32;
124 let mut entered_attribute = false;
125
126 for line in lines.iter().skip(start) {
127 let code = code_before_line_comment(line);
128
129 if contains_reason(code) {
130 return true;
131 }
132
133 let mut in_string = false;
134 let mut escaped = false;
135
136 for byte in code.bytes() {
137 if in_string {
138 if escaped {
139 escaped = false;
140 } else if byte == b'\\' {
141 escaped = true;
142 } else if byte == b'"' {
143 in_string = false;
144 }
145
146 continue;
147 }
148
149 match byte {
150 b'"' => in_string = true,
151 b'(' => {
152 entered_attribute = true;
153 depth += 1;
154 }
155 b')' if entered_attribute => {
156 depth = depth.saturating_sub(1);
157
158 if depth == 0 {
159 return false;
160 }
161 }
162 _ => {}
163 }
164 }
165 }
166
167 false
168}
169
170fn check_allow_reason(ctx: &FileCtx<'_>) -> Vec<Violation> {
171 let mut out = Vec::new();
172
173 for (i, line) in ctx.lines.iter().enumerate() {
174 let trimmed = line.trim();
175
176 if !parse::is_allow_attr(trimmed) && !is_expect_attr(trimmed) {
177 continue;
178 }
179
180 if attribute_span_has_reason(ctx.lines, i) {
181 continue;
182 }
183
184 if parse::has_inline_comment(trimmed) {
185 continue;
186 }
187
188 if i.checked_sub(1)
189 .and_then(|previous| ctx.lines.get(previous))
190 .is_some_and(|previous| parse::is_comment(previous.trim()))
191 {
192 continue;
193 }
194
195 out.push(violation(
196 ctx.rel,
197 i + 1,
198 "lint override without a `reason = \"...\"` or comment explaining why",
199 ));
200 }
201
202 out
203}
204
205crate::tidy_test!(check_allow_reason, {
206 crate::example_tests!(EXAMPLES, check_allow_reason);
207});