1use std::collections::HashSet;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum SuppressionKind {
28 SameLine,
30 NextLine,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Suppression {
37 pub kind: SuppressionKind,
39 pub rule_ids: Option<HashSet<String>>,
42}
43
44impl Suppression {
45 pub fn suppresses(&self, rule_id: &str) -> bool {
47 match &self.rule_ids {
48 None => true, Some(ids) => ids.contains(rule_id),
50 }
51 }
52
53 pub fn is_wildcard(&self) -> bool {
55 self.rule_ids.is_none()
56 }
57}
58
59const DIRECTIVE_PREFIX: &str = "diffguard:";
61
62pub fn parse_suppression(line: &str) -> Option<Suppression> {
70 let lower = line.to_ascii_lowercase();
71 lower
72 .match_indices(DIRECTIVE_PREFIX)
73 .next()
74 .and_then(|(idx, _)| parse_suppression_at(line, idx))
75}
76
77#[allow(clippy::collapsible_if)]
84pub fn parse_suppression_in_comments(line: &str, masked_comments: &str) -> Option<Suppression> {
85 if line.len() != masked_comments.len() {
86 return None;
87 }
88
89 let lower = line.to_ascii_lowercase();
90 let needle = DIRECTIVE_PREFIX.as_bytes();
91 let masked = masked_comments.as_bytes();
92
93 for (idx, _) in lower.match_indices(DIRECTIVE_PREFIX) {
94 let in_comment = masked[idx..idx + needle.len()].iter().all(|b| *b == b' ');
95 if in_comment {
96 if let Some(suppression) = parse_suppression_at(line, idx) {
97 return Some(suppression);
98 }
99 }
100 }
101
102 None
103}
104
105fn parse_suppression_at(line: &str, prefix_start: usize) -> Option<Suppression> {
107 let after_prefix = line.get(prefix_start + DIRECTIVE_PREFIX.len()..)?;
108 let after_prefix = after_prefix.trim_start();
109
110 if let Some(rest) = strip_prefix_ci(after_prefix, "ignore-next-line") {
112 let rule_ids = parse_rule_ids(rest);
113 return Some(Suppression {
114 kind: SuppressionKind::NextLine,
115 rule_ids,
116 });
117 }
118
119 if strip_prefix_ci(after_prefix, "ignore-all").is_some() {
121 return Some(Suppression {
122 kind: SuppressionKind::SameLine,
123 rule_ids: None,
124 });
125 }
126
127 if let Some(rest) = strip_prefix_ci(after_prefix, "ignore") {
129 let rule_ids = parse_rule_ids(rest);
130 return Some(Suppression {
131 kind: SuppressionKind::SameLine,
132 rule_ids,
133 });
134 }
135
136 None
137}
138
139fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
141 let s_lower = s.to_ascii_lowercase();
142 if s_lower.starts_with(prefix) {
143 Some(&s[prefix.len()..])
144 } else {
145 None
146 }
147}
148
149fn parse_rule_ids(rest: &str) -> Option<HashSet<String>> {
153 let rest = rest.trim();
155 let rest = rest.strip_suffix("*/").unwrap_or(rest).trim();
156
157 if rest.is_empty() {
159 return None;
160 }
161
162 if rest == "*" {
164 return None;
165 }
166
167 let mut ids = HashSet::new();
169 for part in rest.split(',') {
170 let id = part.trim();
171 if !id.is_empty() && id != "*" {
172 ids.insert(id.to_string());
173 } else if id == "*" {
174 return None;
176 }
177 }
178
179 if ids.is_empty() { None } else { Some(ids) }
180}
181
182#[derive(Debug, Clone, Default)]
187pub struct SuppressionTracker {
188 pending_next_line: Vec<Suppression>,
190}
191
192impl SuppressionTracker {
193 pub fn new() -> Self {
195 Self::default()
196 }
197
198 pub fn reset(&mut self) {
200 self.pending_next_line.clear();
201 }
202
203 pub fn process_line(&mut self, line: &str, masked_comments: &str) -> EffectiveSuppressions {
212 let mut same_line_suppressions: Vec<Suppression> = Vec::new();
214 let mut next_line_suppressions: Vec<Suppression> = Vec::new();
215
216 same_line_suppressions.append(&mut self.pending_next_line);
218
219 if let Some(suppression) = parse_suppression_in_comments(line, masked_comments) {
221 match suppression.kind {
222 SuppressionKind::SameLine => {
223 same_line_suppressions.push(suppression);
224 }
225 SuppressionKind::NextLine => {
226 next_line_suppressions.push(suppression);
227 }
228 }
229 }
230
231 self.pending_next_line = next_line_suppressions;
233
234 EffectiveSuppressions::from_suppressions(same_line_suppressions)
235 }
236}
237
238#[derive(Debug, Clone, Default)]
240pub struct EffectiveSuppressions {
241 pub suppress_all: bool,
243 pub suppressed_rules: HashSet<String>,
245}
246
247impl EffectiveSuppressions {
248 fn from_suppressions(suppressions: Vec<Suppression>) -> Self {
250 let mut result = Self::default();
251
252 for s in suppressions {
253 match s.rule_ids {
254 None => {
255 result.suppress_all = true;
256 }
257 Some(ids) => {
258 result.suppressed_rules.extend(ids);
259 }
260 }
261 }
262
263 result
264 }
265
266 pub fn is_suppressed(&self, rule_id: &str) -> bool {
268 self.suppress_all || self.suppressed_rules.contains(rule_id)
269 }
270
271 pub fn is_empty(&self) -> bool {
273 !self.suppress_all && self.suppressed_rules.is_empty()
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::preprocess::{Language, PreprocessOptions, Preprocessor};
281
282 fn masked_comments(line: &str, lang: Language) -> String {
283 let mut p = Preprocessor::with_language(PreprocessOptions::comments_only(), lang);
284 p.sanitize_line(line)
285 }
286
287 #[test]
290 fn parse_same_line_ignore_single_rule() {
291 let line = "let x = y.unwrap(); // diffguard: ignore rust.no_unwrap";
292 let suppression = parse_suppression(line).expect("should parse");
293
294 assert_eq!(suppression.kind, SuppressionKind::SameLine);
295 assert!(!suppression.is_wildcard());
296 assert!(suppression.suppresses("rust.no_unwrap"));
297 assert!(!suppression.suppresses("other.rule"));
298 }
299
300 #[test]
301 fn parse_same_line_ignore_multiple_rules() {
302 let line = "// diffguard: ignore rule1, rule2, rule3";
303 let suppression = parse_suppression(line).expect("should parse");
304
305 assert_eq!(suppression.kind, SuppressionKind::SameLine);
306 assert!(!suppression.is_wildcard());
307 assert!(suppression.suppresses("rule1"));
308 assert!(suppression.suppresses("rule2"));
309 assert!(suppression.suppresses("rule3"));
310 assert!(!suppression.suppresses("rule4"));
311 }
312
313 #[test]
314 fn parse_same_line_ignore_wildcard_star() {
315 let line = "// diffguard: ignore *";
316 let suppression = parse_suppression(line).expect("should parse");
317
318 assert_eq!(suppression.kind, SuppressionKind::SameLine);
319 assert!(suppression.is_wildcard());
320 assert!(suppression.suppresses("any.rule"));
321 assert!(suppression.suppresses("other.rule"));
322 }
323
324 #[test]
325 fn parse_same_line_ignore_all() {
326 let line = "// diffguard: ignore-all";
327 let suppression = parse_suppression(line).expect("should parse");
328
329 assert_eq!(suppression.kind, SuppressionKind::SameLine);
330 assert!(suppression.is_wildcard());
331 assert!(suppression.suppresses("any.rule"));
332 }
333
334 #[test]
335 fn parse_same_line_ignore_empty_means_wildcard() {
336 let line = "// diffguard: ignore";
337 let suppression = parse_suppression(line).expect("should parse");
338
339 assert_eq!(suppression.kind, SuppressionKind::SameLine);
340 assert!(suppression.is_wildcard());
341 }
342
343 #[test]
344 fn parse_next_line_ignore_single_rule() {
345 let line = "// diffguard: ignore-next-line rust.no_dbg";
346 let suppression = parse_suppression(line).expect("should parse");
347
348 assert_eq!(suppression.kind, SuppressionKind::NextLine);
349 assert!(!suppression.is_wildcard());
350 assert!(suppression.suppresses("rust.no_dbg"));
351 assert!(!suppression.suppresses("other.rule"));
352 }
353
354 #[test]
355 fn parse_next_line_ignore_wildcard() {
356 let line = "// diffguard: ignore-next-line *";
357 let suppression = parse_suppression(line).expect("should parse");
358
359 assert_eq!(suppression.kind, SuppressionKind::NextLine);
360 assert!(suppression.is_wildcard());
361 }
362
363 #[test]
364 fn parse_next_line_ignore_empty_means_wildcard() {
365 let line = "// diffguard: ignore-next-line";
366 let suppression = parse_suppression(line).expect("should parse");
367
368 assert_eq!(suppression.kind, SuppressionKind::NextLine);
369 assert!(suppression.is_wildcard());
370 }
371
372 #[test]
373 fn parse_case_insensitive() {
374 let line = "// DIFFGUARD: IGNORE rule.id";
375 let suppression = parse_suppression(line).expect("should parse");
376
377 assert_eq!(suppression.kind, SuppressionKind::SameLine);
378 assert!(suppression.suppresses("rule.id"));
379 }
380
381 #[test]
382 fn parse_mixed_case() {
383 let line = "// DiffGuard: Ignore-Next-Line rule.id";
384 let suppression = parse_suppression(line).expect("should parse");
385
386 assert_eq!(suppression.kind, SuppressionKind::NextLine);
387 assert!(suppression.suppresses("rule.id"));
388 }
389
390 #[test]
391 fn parse_in_hash_comment() {
392 let line = "x = 1 # diffguard: ignore python.no_print";
393 let suppression = parse_suppression(line).expect("should parse");
394
395 assert_eq!(suppression.kind, SuppressionKind::SameLine);
396 assert!(suppression.suppresses("python.no_print"));
397 }
398
399 #[test]
400 fn parse_in_block_comment() {
401 let line = "let x = y.unwrap(); /* diffguard: ignore rust.no_unwrap */";
402 let suppression = parse_suppression(line).expect("should parse");
403
404 assert_eq!(suppression.kind, SuppressionKind::SameLine);
405 assert!(suppression.suppresses("rust.no_unwrap"));
406 }
407
408 #[test]
409 fn parse_no_directive_returns_none() {
410 let line = "let x = y.unwrap();";
411 assert!(parse_suppression(line).is_none());
412 }
413
414 #[test]
415 fn parse_unrelated_comment_returns_none() {
416 let line = "// This is a normal comment";
417 assert!(parse_suppression(line).is_none());
418 }
419
420 #[test]
421 fn parse_partial_directive_returns_none() {
422 let line = "// diffguard";
423 assert!(parse_suppression(line).is_none());
424 }
425
426 #[test]
427 fn parse_in_comments_length_mismatch_returns_none() {
428 let line = "let x = 1; // diffguard: ignore rust.no_unwrap";
429 let masked = "short";
430 assert!(parse_suppression_in_comments(line, masked).is_none());
431 }
432
433 #[test]
434 fn parse_in_string_is_ignored_when_not_in_comment() {
435 let line = "let x = \"diffguard: ignore rust.no_unwrap\";";
436 let masked = masked_comments(line, Language::Rust);
437 assert!(parse_suppression_in_comments(line, &masked).is_none());
438 }
439
440 #[test]
441 fn parse_in_comment_is_detected() {
442 let line = "let x = 1; // diffguard: ignore rust.no_unwrap";
443 let masked = masked_comments(line, Language::Rust);
444 let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
445 assert!(suppression.suppresses("rust.no_unwrap"));
446 }
447
448 #[test]
449 fn parse_in_python_hash_comment_is_detected() {
450 let line = "x = 1 # diffguard: ignore python.no_print";
451 let masked = masked_comments(line, Language::Python);
452 let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
453 assert!(suppression.suppresses("python.no_print"));
454 }
455
456 #[test]
457 fn parse_string_then_comment_prefers_comment_directive() {
458 let line =
459 r#"let x = "diffguard: ignore rust.no_unwrap"; // diffguard: ignore rust.no_dbg"#;
460 let masked = masked_comments(line, Language::Rust);
461 let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
462 assert!(suppression.suppresses("rust.no_dbg"));
463 assert!(!suppression.suppresses("rust.no_unwrap"));
464 }
465
466 #[test]
467 fn parse_directive_with_extra_whitespace() {
468 let line = "// diffguard: ignore rule.id ";
469 let suppression = parse_suppression(line).expect("should parse");
470
471 assert_eq!(suppression.kind, SuppressionKind::SameLine);
472 assert!(suppression.suppresses("rule.id"));
473 }
474
475 #[test]
476 fn parse_multiple_rules_with_varying_whitespace() {
477 let line = "// diffguard: ignore rule1,rule2, rule3 ,rule4";
478 let suppression = parse_suppression(line).expect("should parse");
479
480 assert!(suppression.suppresses("rule1"));
481 assert!(suppression.suppresses("rule2"));
482 assert!(suppression.suppresses("rule3"));
483 assert!(suppression.suppresses("rule4"));
484 }
485
486 #[test]
487 fn parse_wildcard_in_list_becomes_wildcard() {
488 let line = "// diffguard: ignore rule1, *, rule2";
489 let suppression = parse_suppression(line).expect("should parse");
490
491 assert!(suppression.is_wildcard());
493 }
494
495 #[test]
496 fn parse_suppression_at_unknown_directive_returns_none() {
497 let line = "// diffguard: nope rust.no_unwrap";
498 let prefix_start = line.find(DIRECTIVE_PREFIX).expect("prefix");
499 assert!(parse_suppression_at(line, prefix_start).is_none());
500 }
501
502 #[test]
503 fn parse_suppression_in_comments_skips_invalid_directive() {
504 let line = "// diffguard: nope rust.no_unwrap";
505 let masked = masked_comments(line, Language::Rust);
506 assert!(parse_suppression_in_comments(line, &masked).is_none());
507 }
508
509 #[test]
510 fn parse_rule_ids_empty_returns_none() {
511 assert!(parse_rule_ids(" ").is_none());
512 assert!(parse_rule_ids(" , , ").is_none());
513 }
514
515 #[test]
518 fn tracker_same_line_suppression() {
519 let mut tracker = SuppressionTracker::new();
520
521 let line = "let x = y.unwrap(); // diffguard: ignore rust.no_unwrap";
522 let masked = masked_comments(line, Language::Rust);
523 let effective = tracker.process_line(line, &masked);
524
525 assert!(effective.is_suppressed("rust.no_unwrap"));
526 assert!(!effective.is_suppressed("other.rule"));
527 }
528
529 #[test]
530 fn tracker_next_line_suppression() {
531 let mut tracker = SuppressionTracker::new();
532
533 let line1 = "// diffguard: ignore-next-line rust.no_dbg";
535 let masked1 = masked_comments(line1, Language::Rust);
536 let effective1 = tracker.process_line(line1, &masked1);
537 assert!(!effective1.is_suppressed("rust.no_dbg")); let line2 = "dbg!(value);";
541 let masked2 = masked_comments(line2, Language::Rust);
542 let effective2 = tracker.process_line(line2, &masked2);
543 assert!(effective2.is_suppressed("rust.no_dbg"));
544
545 let line3 = "dbg!(other);";
547 let masked3 = masked_comments(line3, Language::Rust);
548 let effective3 = tracker.process_line(line3, &masked3);
549 assert!(!effective3.is_suppressed("rust.no_dbg"));
550 }
551
552 #[test]
553 fn tracker_both_same_and_next_line() {
554 let mut tracker = SuppressionTracker::new();
555
556 let line1 = "// diffguard: ignore-next-line rule1";
558 let masked1 = masked_comments(line1, Language::Rust);
559 let effective1 = tracker.process_line(line1, &masked1);
560 assert!(!effective1.is_suppressed("rule1"));
561
562 let line2 = "x = 1 // diffguard: ignore rule2";
563 let masked2 = masked_comments(line2, Language::Rust);
564 let effective2 = tracker.process_line(line2, &masked2);
565 assert!(effective2.is_suppressed("rule1")); assert!(effective2.is_suppressed("rule2")); }
568
569 #[test]
570 fn tracker_wildcard_suppression() {
571 let mut tracker = SuppressionTracker::new();
572
573 let line = "// diffguard: ignore *";
574 let masked = masked_comments(line, Language::Rust);
575 let effective = tracker.process_line(line, &masked);
576 assert!(effective.is_suppressed("any.rule"));
577 assert!(effective.is_suppressed("other.rule"));
578 assert!(effective.suppress_all);
579 }
580
581 #[test]
582 fn tracker_reset_clears_pending() {
583 let mut tracker = SuppressionTracker::new();
584
585 let line1 = "// diffguard: ignore-next-line rule1";
587 let masked1 = masked_comments(line1, Language::Rust);
588 tracker.process_line(line1, &masked1);
589
590 tracker.reset();
592
593 let line2 = "some code";
595 let masked2 = masked_comments(line2, Language::Rust);
596 let effective = tracker.process_line(line2, &masked2);
597 assert!(!effective.is_suppressed("rule1"));
598 }
599
600 #[test]
601 fn tracker_multiple_next_line_directives() {
602 let mut tracker = SuppressionTracker::new();
603
604 let line1 = "// diffguard: ignore-next-line rule1";
606 let masked1 = masked_comments(line1, Language::Rust);
607 tracker.process_line(line1, &masked1);
608 let line2 = "// diffguard: ignore-next-line rule2";
609 let masked2 = masked_comments(line2, Language::Rust);
610 let effective1 = tracker.process_line(line2, &masked2);
611
612 assert!(effective1.is_suppressed("rule1"));
615
616 let line3 = "actual code";
618 let masked3 = masked_comments(line3, Language::Rust);
619 let effective2 = tracker.process_line(line3, &masked3);
620 assert!(effective2.is_suppressed("rule2"));
621 assert!(!effective2.is_suppressed("rule1"));
622 }
623
624 #[test]
627 fn effective_suppressions_is_empty() {
628 let effective = EffectiveSuppressions::default();
629 assert!(effective.is_empty());
630 assert!(!effective.is_suppressed("any.rule"));
631 }
632
633 #[test]
634 fn effective_suppressions_specific_rules() {
635 let mut effective = EffectiveSuppressions::default();
636 effective.suppressed_rules.insert("rule1".to_string());
637 effective.suppressed_rules.insert("rule2".to_string());
638
639 assert!(!effective.is_empty());
640 assert!(effective.is_suppressed("rule1"));
641 assert!(effective.is_suppressed("rule2"));
642 assert!(!effective.is_suppressed("rule3"));
643 }
644
645 #[test]
646 fn effective_suppressions_wildcard() {
647 let effective = EffectiveSuppressions {
648 suppress_all: true,
649 ..Default::default()
650 };
651
652 assert!(!effective.is_empty());
653 assert!(effective.is_suppressed("any.rule"));
654 assert!(effective.is_suppressed("other.rule"));
655 }
656}