1use std::collections::HashMap;
7
8#[cfg(test)]
9use googletest::prelude::*;
10use winnow::token::any;
11
12use super::{
13 parse::{self, DirectiveResult, Scope},
14 scanner,
15};
16use crate::{Violation, violation};
17
18#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
20pub(crate) struct SuppressionEntry {
21 pub rel: String,
22 pub line: usize,
23 end_line: Option<usize>,
24 pub scope: Scope,
25 pub rules: Vec<String>,
26 pub values: std::collections::BTreeMap<String, usize>,
27 pub wildcard: bool,
28 pub reason: String,
29}
30
31impl SuppressionEntry {
32 pub(crate) fn covers_line(&self, line: usize) -> bool {
34 match self.scope {
35 Scope::File => true,
36 Scope::NextLine | Scope::Block | Scope::Fn => self
37 .end_line
38 .is_some_and(|end| line > self.line && line <= end),
39 }
40 }
41
42 pub(crate) fn suppresses(&self, violation: &Violation) -> bool {
44 let Some(rule) = violation.rule else {
45 return false;
46 };
47 let targets_rule = self.wildcard || self.rules.iter().any(|target| target == rule);
48
49 if !targets_rule {
50 return false;
51 }
52
53 self.covers_line(violation.line)
54 }
55}
56
57pub(crate) type LineSuppressMap = HashMap<usize, Vec<String>>;
59
60fn brace_depth_change(line: &str) -> (i32, bool) {
61 let mut delta: i32 = 0;
62 let mut found_open = false;
63 let mut input = line;
64
65 while let Some(ch) = parse::try_parse(&mut input, any) {
66 match ch {
67 '/' if parse::matches(input, '/') => return (delta, found_open),
68 '"' => scanner::skip_string(&mut input),
69 'r' if parse::matches(input, '#') || parse::matches(input, '"') => {
70 scanner::try_skip_raw_string(&mut input);
71 }
72 '\'' => scanner::skip_char_literal(&mut input),
73 '{' => {
74 delta += 1;
75 found_open = true;
76 }
77 '}' => {
78 delta -= 1;
79 }
80 _ => {}
81 }
82 }
83
84 (delta, found_open)
85}
86
87#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
89pub(crate) struct Suppressions {
90 pub lines: LineSuppressMap,
91 pub file_rules: Vec<String>,
92 pub file_all: bool,
93 pub entries: Vec<SuppressionEntry>,
94}
95
96const WILDCARD: &str = "*";
97const NEXT_LINE_OFFSET: usize = 2;
98
99pub(crate) fn suppressed_lines(
102 rel: &str,
103 lines: &[&str],
104 errors: &mut Vec<Violation>,
105 registered_rules: Option<&std::collections::HashSet<&str>>,
106) -> Suppressions {
107 let mut map: LineSuppressMap = HashMap::new();
108 let mut file_rules: Vec<String> = Vec::new();
109 let mut file_all = false;
110 let mut entries: Vec<SuppressionEntry> = Vec::new();
111
112 for (i, line) in lines.iter().enumerate() {
113 match parse::directive(line) {
114 Some(DirectiveResult::Valid(d)) => {
115 if let Some(known) = registered_rules {
116 for name in &d.rules {
117 if !known.contains(name.as_str()) {
118 let msg = format!(
120 "unknown rule `{name}` in #t directive — \
121 check for typos (use `tidy --list` to see all rules)"
122 );
123
124 errors.push(violation(rel, i + 1, msg));
125 }
126 }
127 }
128
129 let scope = d.scope;
130 let wildcard = d.wildcard;
131 let rule_names: Vec<String> = if wildcard {
132 vec![WILDCARD.to_string()]
133 } else {
134 d.rules
135 };
136
137 let directive_line = i + 1;
138 let end_line = match scope {
139 Scope::File => {
140 if wildcard {
141 file_all = true;
142 } else {
143 file_rules.extend(
144 rule_names
145 .iter()
146 .filter(|rule| !d.values.contains_key(rule.as_str()))
147 .cloned(),
148 );
149 }
150
151 None
152 }
153 Scope::NextLine => {
154 map.entry(i + NEXT_LINE_OFFSET)
155 .or_default()
156 .extend(rule_names.clone());
158
159 Some(i + NEXT_LINE_OFFSET)
160 }
161 Scope::Block => {
162 let mut end_line = directive_line;
163
164 for (j, bline) in lines.iter().enumerate().skip(i + 1) {
165 let t = bline.trim();
166
167 if t.is_empty() || matches!(t, "}" | "};" | ")" | ");") {
168 break;
169 }
170
171 end_line = j + 1;
172 map.entry(j + 1).or_default().extend(rule_names.clone());
174 }
175
176 Some(end_line)
177 }
178 Scope::Fn => {
179 let mut depth: i32 = 0;
180 let mut found_open = false;
181 let mut end_line = directive_line;
182
183 for (j, fline) in lines.iter().enumerate().skip(i + 1) {
184 let (delta, has_open) = brace_depth_change(fline);
185
186 depth += delta;
187 found_open |= has_open;
188 end_line = j + 1;
189 map.entry(j + 1).or_default().extend(rule_names.clone());
191
192 if found_open && depth <= 0 {
193 break;
194 }
195 }
196
197 Some(end_line)
198 }
199 };
200
201 entries.push(SuppressionEntry {
203 rel: rel.to_string(),
204 line: directive_line,
205 end_line,
206 scope,
207 rules: rule_names,
208 values: d.values,
209 wildcard,
210 reason: d.reason,
211 });
212 }
213 Some(DirectiveResult::MissingReason(scope)) => {
214 let name = scope.prefix();
215
216 errors.push(violation(
217 rel,
218 i + 1,
219 format!("{name} requires a reason after the closing `)`"),
220 ));
221 }
222 Some(DirectiveResult::Malformed(msg)) => {
223 errors.push(violation(rel, i + 1, format!("#t directive error: {msg}")));
224 }
225 None => {}
226 }
227 }
228
229 Suppressions {
230 lines: map,
231 file_rules,
232 file_all,
233 entries,
234 }
235}
236
237pub(crate) fn is_file_suppressed(suppressed: &Suppressions, rule: &str) -> bool {
239 suppressed.file_all || suppressed.file_rules.iter().any(|r| r == rule)
240}
241
242pub(crate) fn filter(suppressed: &Suppressions, violations: Vec<Violation>) -> Vec<Violation> {
244 if suppressed.lines.is_empty() {
245 return violations;
246 }
247
248 violations
249 .into_iter()
250 .filter(|v| {
251 let Some(rule) = v.rule else { return true };
252
253 suppressed
254 .lines
255 .get(&v.line)
256 .is_none_or(|rules| !rules.iter().any(|r| r == rule || r == WILDCARD))
257 })
258 .collect()
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn make_errors() -> Vec<Violation> {
266 Vec::new()
267 }
268
269 #[gtest]
270 fn empty_directive_is_error() -> Result<()> {
271 let lines = vec!["// #t()", "let x = 1;"];
272 let mut errors = make_errors();
273 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
274
275 verify_true!(sup.lines.is_empty())?;
276 verify_eq!(errors.len(), 1)?;
277
278 verify_true!(errors[0].message.contains("empty"))
279 }
280
281 #[gtest]
282 fn missing_reason_is_error() -> Result<()> {
283 let lines = vec!["// #t(rust_panic)", "let x = 1;"];
284 let mut errors = make_errors();
285 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
286
287 verify_true!(sup.lines.is_empty())?;
288 verify_eq!(errors.len(), 1)?;
289 verify_true!(errors[0].message.contains("requires a reason"))?;
290
291 Ok(())
292 }
293
294 #[gtest]
295 fn next_line_works() -> Result<()> {
296 let lines = vec![
297 "// #t(rust_panic) startup invariant",
298 "panic!(\"oh no\");",
299 "let y = 2;",
300 ];
301 let mut errors = make_errors();
302 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
303
304 verify_eq!(sup.lines.get(&2).or_fail()?, &["rust_panic"])?;
305 verify_false!(sup.lines.contains_key(&3))?;
306 verify_true!(errors.is_empty())?;
307
308 Ok(())
309 }
310
311 #[gtest]
312 fn multi_rule_works() -> Result<()> {
313 let lines = vec![
314 "// #t(rust_alloc_in_loop, rust_clone_in_loop) error path",
315 "let msg = format!(\"err: {}\", e);",
316 ];
317 let mut errors = make_errors();
318 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
319 let rules = sup.lines.get(&2).or_fail()?;
320
321 verify_eq!(rules, &["rust_alloc_in_loop", "rust_clone_in_loop"])?;
322 verify_true!(errors.is_empty())?;
323
324 Ok(())
325 }
326
327 #[gtest]
328 fn file_scope_works() -> Result<()> {
329 let lines = vec![
330 "// #t(file: rust_commented_code) generated file",
331 "let x = 1;",
332 ];
333 let mut errors = make_errors();
334 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
335
336 verify_true!(errors.is_empty())?;
337 verify_true!(is_file_suppressed(&sup, "rust_commented_code"))?;
338 verify_false!(is_file_suppressed(&sup, "rust_style"))?;
339
340 Ok(())
341 }
342
343 #[gtest]
344 fn block_scope_works() -> Result<()> {
345 let lines = vec![
346 "// #t(block: rust_wildcard_imports) barrel re-exports",
347 "use foo::*;",
348 "use bar::*;",
349 "",
350 "use baz::*;",
351 ];
352 let mut errors = make_errors();
353 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
354
355 verify_eq!(sup.lines.get(&2).or_fail()?, &["rust_wildcard_imports"])?;
356 verify_eq!(sup.lines.get(&3).or_fail()?, &["rust_wildcard_imports"])?;
357 verify_false!(sup.lines.contains_key(&5))?;
358 verify_true!(errors.is_empty())?;
359
360 Ok(())
361 }
362
363 #[gtest]
364 fn fn_scope_works() -> Result<()> {
365 let lines = vec![
366 "// #t(fn: rust_alloc_in_loop) error reporting function",
367 "fn validate() {",
368 " let x = format!(\"a\");",
369 " let y = format!(\"b\");",
370 "}",
371 "fn other() {}",
372 ];
373 let mut errors = make_errors();
374 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
375
376 verify_true!(sup.lines.contains_key(&2))?;
377 verify_true!(sup.lines.contains_key(&3))?;
378 verify_true!(sup.lines.contains_key(&4))?;
379 verify_true!(sup.lines.contains_key(&5))?;
380 verify_false!(sup.lines.contains_key(&6))?;
381 verify_true!(errors.is_empty())?;
382
383 Ok(())
384 }
385
386 #[gtest]
387 fn fn_scope_ignores_braces_in_strings() -> Result<()> {
389 let lines = vec![
390 "// #t(fn: rust_alloc_in_loop) error reporting function",
392 "fn validate() {",
393 " let s = \"{ not a real brace }\";",
394 " let x = format!(\"a\");",
395 "}",
396 "fn other() {}",
397 ];
398 let mut errors = make_errors();
399 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
400
401 verify_true!(sup.lines.contains_key(&2))?;
402 verify_true!(sup.lines.contains_key(&3))?;
403 verify_true!(sup.lines.contains_key(&4))?;
404 verify_true!(sup.lines.contains_key(&5))?;
405 verify_false!(sup.lines.contains_key(&6))?;
406 verify_true!(errors.is_empty())?;
407
408 Ok(())
409 }
410
411 #[gtest]
412 fn fn_scope_ignores_braces_in_comments() -> Result<()> {
414 let lines = vec![
415 "// #t(fn: rust_alloc_in_loop) error reporting function",
417 "fn validate() {",
418 " // closing } here doesn't count",
419 " let x = format!(\"a\");",
420 "}",
421 "fn other() {}",
422 ];
423 let mut errors = make_errors();
424 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
425
426 verify_true!(sup.lines.contains_key(&2))?;
427 verify_true!(sup.lines.contains_key(&3))?;
428 verify_true!(sup.lines.contains_key(&4))?;
429 verify_true!(sup.lines.contains_key(&5))?;
430 verify_false!(sup.lines.contains_key(&6))?;
431 verify_true!(errors.is_empty())?;
432
433 Ok(())
434 }
435
436 #[gtest]
437 fn fn_scope_ignores_braces_in_char_literals() -> Result<()> {
439 let lines = vec![
440 "// #t(fn: rust_alloc_in_loop) error reporting function",
442 "fn validate() {",
443 " let c = '{';",
444 " let d = '}';",
445 " let x = format!(\"a\");",
446 "}",
447 "fn other() {}",
448 ];
449 let mut errors = make_errors();
450 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
451
452 verify_true!(sup.lines.contains_key(&2))?;
453 verify_true!(sup.lines.contains_key(&3))?;
454 verify_true!(sup.lines.contains_key(&4))?;
455 verify_true!(sup.lines.contains_key(&5))?;
456 verify_true!(sup.lines.contains_key(&6))?;
457 verify_false!(sup.lines.contains_key(&7))?;
458 verify_true!(errors.is_empty())?;
459
460 Ok(())
461 }
462}
463
464#[cfg(test)]
465mod filter_tests {
466 use super::*;
467
468 fn make_errors() -> Vec<Violation> {
469 Vec::new()
470 }
471
472 fn empty_suppressions() -> Suppressions {
473 Suppressions {
474 lines: HashMap::new(),
475 file_rules: Vec::new(),
476 file_all: false,
477 entries: Vec::new(),
478 }
479 }
480
481 #[gtest]
482 fn wildcard_works() -> Result<()> {
483 let lines = vec!["// #t(*) generated code", "let x = 1;"];
484 let mut errors = make_errors();
485 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
486
487 verify_eq!(sup.lines.get(&2).or_fail()?, &["*"])?;
488 verify_true!(errors.is_empty())?;
489
490 Ok(())
491 }
492
493 #[gtest]
494 fn wildcard_file_works() -> Result<()> {
495 let lines = vec!["// #t(file: *) fully generated"];
496 let mut errors = make_errors();
497 let sup = suppressed_lines("t.rs", &lines, &mut errors, None);
498
499 verify_true!(sup.file_all)?;
500 verify_true!(is_file_suppressed(&sup, "anything"))?;
501 verify_true!(errors.is_empty())?;
502
503 Ok(())
504 }
505
506 #[gtest]
507 fn wildcard_suppresses_any_rule() -> Result<()> {
508 let mut sup = empty_suppressions();
509
510 sup.lines.insert(2, vec!["*".to_string()]);
511
512 let violations = vec![
513 violation("t.rs", 2, "a").with_rule("panic"),
514 violation("t.rs", 2, "b").with_rule("unwrap_in_lib"),
515 ];
516 let filtered = filter(&sup, violations);
517
518 verify_true!(filtered.is_empty())?;
519
520 Ok(())
521 }
522
523 #[gtest]
524 fn suppression_only_applies_to_named_rule() -> Result<()> {
525 let mut sup = empty_suppressions();
526
527 sup.lines.insert(2, vec!["panic".to_string()]);
528
529 let violations = vec![
530 violation("t.rs", 2, "a").with_rule("panic"),
531 violation("t.rs", 2, "b").with_rule("unwrap_in_lib"),
532 ];
533 let filtered = filter(&sup, violations);
534
535 verify_eq!(filtered.len(), 1)?;
536 verify_eq!(filtered[0].rule, Some("unwrap_in_lib"))?;
537
538 Ok(())
539 }
540
541 #[gtest]
542 fn filter_removes_matching_rule_only() -> Result<()> {
543 let mut sup = empty_suppressions();
544
545 sup.lines.insert(2, vec!["panic".to_string()]);
546 sup.lines.insert(3, vec!["panic".to_string()]);
547
548 let violations = vec![
549 violation("t.rs", 1, "a").with_rule("panic"),
550 violation("t.rs", 2, "b").with_rule("panic"),
551 violation("t.rs", 3, "c").with_rule("style"),
552 violation("t.rs", 4, "d").with_rule("panic"),
553 ];
554 let filtered = filter(&sup, violations);
555
556 verify_eq!(filtered.len(), 3)?;
557 verify_eq!(filtered[0].line, 1)?;
558 verify_eq!(filtered[1].line, 3)?;
559 verify_eq!(filtered[2].line, 4)?;
560
561 Ok(())
562 }
563}