use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SuppressionKind {
SameLine,
NextLine,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suppression {
pub kind: SuppressionKind,
pub rule_ids: Option<HashSet<String>>,
}
impl Suppression {
pub fn suppresses(&self, rule_id: &str) -> bool {
match &self.rule_ids {
None => true, Some(ids) => ids.contains(rule_id),
}
}
pub fn is_wildcard(&self) -> bool {
self.rule_ids.is_none()
}
}
const DIRECTIVE_PREFIX: &str = "diffguard:";
pub fn parse_suppression(line: &str) -> Option<Suppression> {
let lower = line.to_ascii_lowercase();
lower
.match_indices(DIRECTIVE_PREFIX)
.next()
.and_then(|(idx, _)| parse_suppression_at(line, idx))
}
#[allow(clippy::collapsible_if)]
pub fn parse_suppression_in_comments(line: &str, masked_comments: &str) -> Option<Suppression> {
if line.len() != masked_comments.len() {
return None;
}
let lower = line.to_ascii_lowercase();
let needle = DIRECTIVE_PREFIX.as_bytes();
let masked = masked_comments.as_bytes();
for (idx, _) in lower.match_indices(DIRECTIVE_PREFIX) {
let in_comment = masked[idx..idx + needle.len()].iter().all(|b| *b == b' ');
if in_comment {
if let Some(suppression) = parse_suppression_at(line, idx) {
return Some(suppression);
}
}
}
None
}
fn parse_suppression_at(line: &str, prefix_start: usize) -> Option<Suppression> {
let after_prefix = line.get(prefix_start + DIRECTIVE_PREFIX.len()..)?;
let after_prefix = after_prefix.trim_start();
if let Some(rest) = strip_prefix_ci(after_prefix, "ignore-next-line") {
let rule_ids = parse_rule_ids(rest);
return Some(Suppression {
kind: SuppressionKind::NextLine,
rule_ids,
});
}
if strip_prefix_ci(after_prefix, "ignore-all").is_some() {
return Some(Suppression {
kind: SuppressionKind::SameLine,
rule_ids: None,
});
}
if let Some(rest) = strip_prefix_ci(after_prefix, "ignore") {
let rule_ids = parse_rule_ids(rest);
return Some(Suppression {
kind: SuppressionKind::SameLine,
rule_ids,
});
}
None
}
fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let s_lower = s.to_ascii_lowercase();
if s_lower.starts_with(prefix) {
Some(&s[prefix.len()..])
} else {
None
}
}
fn parse_rule_ids(rest: &str) -> Option<HashSet<String>> {
let rest = rest.trim();
let rest = rest.strip_suffix("*/").unwrap_or(rest).trim();
if rest.is_empty() {
return None;
}
if rest == "*" {
return None;
}
let mut ids = HashSet::new();
for part in rest.split(',') {
let id = part.trim();
if !id.is_empty() && id != "*" {
ids.insert(id.to_string());
} else if id == "*" {
return None;
}
}
if ids.is_empty() { None } else { Some(ids) }
}
#[derive(Debug, Clone, Default)]
pub struct SuppressionTracker {
pending_next_line: Vec<Suppression>,
}
impl SuppressionTracker {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.pending_next_line.clear();
}
pub fn process_line(&mut self, line: &str, masked_comments: &str) -> EffectiveSuppressions {
let mut same_line_suppressions: Vec<Suppression> = Vec::new();
let mut next_line_suppressions: Vec<Suppression> = Vec::new();
same_line_suppressions.append(&mut self.pending_next_line);
if let Some(suppression) = parse_suppression_in_comments(line, masked_comments) {
match suppression.kind {
SuppressionKind::SameLine => {
same_line_suppressions.push(suppression);
}
SuppressionKind::NextLine => {
next_line_suppressions.push(suppression);
}
}
}
self.pending_next_line = next_line_suppressions;
EffectiveSuppressions::from_suppressions(same_line_suppressions)
}
}
#[derive(Debug, Clone, Default)]
pub struct EffectiveSuppressions {
pub suppress_all: bool,
pub suppressed_rules: HashSet<String>,
}
impl EffectiveSuppressions {
fn from_suppressions(suppressions: Vec<Suppression>) -> Self {
let mut result = Self::default();
for s in suppressions {
match s.rule_ids {
None => {
result.suppress_all = true;
}
Some(ids) => {
result.suppressed_rules.extend(ids);
}
}
}
result
}
pub fn is_suppressed(&self, rule_id: &str) -> bool {
self.suppress_all || self.suppressed_rules.contains(rule_id)
}
pub fn is_empty(&self) -> bool {
!self.suppress_all && self.suppressed_rules.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::preprocess::{Language, PreprocessOptions, Preprocessor};
fn masked_comments(line: &str, lang: Language) -> String {
let mut p = Preprocessor::with_language(PreprocessOptions::comments_only(), lang);
p.sanitize_line(line)
}
#[test]
fn parse_same_line_ignore_single_rule() {
let line = "let x = y.unwrap(); // diffguard: ignore rust.no_unwrap";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(!suppression.is_wildcard());
assert!(suppression.suppresses("rust.no_unwrap"));
assert!(!suppression.suppresses("other.rule"));
}
#[test]
fn parse_same_line_ignore_multiple_rules() {
let line = "// diffguard: ignore rule1, rule2, rule3";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(!suppression.is_wildcard());
assert!(suppression.suppresses("rule1"));
assert!(suppression.suppresses("rule2"));
assert!(suppression.suppresses("rule3"));
assert!(!suppression.suppresses("rule4"));
}
#[test]
fn parse_same_line_ignore_wildcard_star() {
let line = "// diffguard: ignore *";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.is_wildcard());
assert!(suppression.suppresses("any.rule"));
assert!(suppression.suppresses("other.rule"));
}
#[test]
fn parse_same_line_ignore_all() {
let line = "// diffguard: ignore-all";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.is_wildcard());
assert!(suppression.suppresses("any.rule"));
}
#[test]
fn parse_same_line_ignore_empty_means_wildcard() {
let line = "// diffguard: ignore";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.is_wildcard());
}
#[test]
fn parse_next_line_ignore_single_rule() {
let line = "// diffguard: ignore-next-line rust.no_dbg";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::NextLine);
assert!(!suppression.is_wildcard());
assert!(suppression.suppresses("rust.no_dbg"));
assert!(!suppression.suppresses("other.rule"));
}
#[test]
fn parse_next_line_ignore_wildcard() {
let line = "// diffguard: ignore-next-line *";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::NextLine);
assert!(suppression.is_wildcard());
}
#[test]
fn parse_next_line_ignore_empty_means_wildcard() {
let line = "// diffguard: ignore-next-line";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::NextLine);
assert!(suppression.is_wildcard());
}
#[test]
fn parse_case_insensitive() {
let line = "// DIFFGUARD: IGNORE rule.id";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.suppresses("rule.id"));
}
#[test]
fn parse_mixed_case() {
let line = "// DiffGuard: Ignore-Next-Line rule.id";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::NextLine);
assert!(suppression.suppresses("rule.id"));
}
#[test]
fn parse_in_hash_comment() {
let line = "x = 1 # diffguard: ignore python.no_print";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.suppresses("python.no_print"));
}
#[test]
fn parse_in_block_comment() {
let line = "let x = y.unwrap(); /* diffguard: ignore rust.no_unwrap */";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.suppresses("rust.no_unwrap"));
}
#[test]
fn parse_no_directive_returns_none() {
let line = "let x = y.unwrap();";
assert!(parse_suppression(line).is_none());
}
#[test]
fn parse_unrelated_comment_returns_none() {
let line = "// This is a normal comment";
assert!(parse_suppression(line).is_none());
}
#[test]
fn parse_partial_directive_returns_none() {
let line = "// diffguard";
assert!(parse_suppression(line).is_none());
}
#[test]
fn parse_in_comments_length_mismatch_returns_none() {
let line = "let x = 1; // diffguard: ignore rust.no_unwrap";
let masked = "short";
assert!(parse_suppression_in_comments(line, masked).is_none());
}
#[test]
fn parse_in_string_is_ignored_when_not_in_comment() {
let line = "let x = \"diffguard: ignore rust.no_unwrap\";";
let masked = masked_comments(line, Language::Rust);
assert!(parse_suppression_in_comments(line, &masked).is_none());
}
#[test]
fn parse_in_comment_is_detected() {
let line = "let x = 1; // diffguard: ignore rust.no_unwrap";
let masked = masked_comments(line, Language::Rust);
let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
assert!(suppression.suppresses("rust.no_unwrap"));
}
#[test]
fn parse_in_python_hash_comment_is_detected() {
let line = "x = 1 # diffguard: ignore python.no_print";
let masked = masked_comments(line, Language::Python);
let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
assert!(suppression.suppresses("python.no_print"));
}
#[test]
fn parse_string_then_comment_prefers_comment_directive() {
let line =
r#"let x = "diffguard: ignore rust.no_unwrap"; // diffguard: ignore rust.no_dbg"#;
let masked = masked_comments(line, Language::Rust);
let suppression = parse_suppression_in_comments(line, &masked).expect("should parse");
assert!(suppression.suppresses("rust.no_dbg"));
assert!(!suppression.suppresses("rust.no_unwrap"));
}
#[test]
fn parse_directive_with_extra_whitespace() {
let line = "// diffguard: ignore rule.id ";
let suppression = parse_suppression(line).expect("should parse");
assert_eq!(suppression.kind, SuppressionKind::SameLine);
assert!(suppression.suppresses("rule.id"));
}
#[test]
fn parse_multiple_rules_with_varying_whitespace() {
let line = "// diffguard: ignore rule1,rule2, rule3 ,rule4";
let suppression = parse_suppression(line).expect("should parse");
assert!(suppression.suppresses("rule1"));
assert!(suppression.suppresses("rule2"));
assert!(suppression.suppresses("rule3"));
assert!(suppression.suppresses("rule4"));
}
#[test]
fn parse_wildcard_in_list_becomes_wildcard() {
let line = "// diffguard: ignore rule1, *, rule2";
let suppression = parse_suppression(line).expect("should parse");
assert!(suppression.is_wildcard());
}
#[test]
fn parse_suppression_at_unknown_directive_returns_none() {
let line = "// diffguard: nope rust.no_unwrap";
let prefix_start = line.find(DIRECTIVE_PREFIX).expect("prefix");
assert!(parse_suppression_at(line, prefix_start).is_none());
}
#[test]
fn parse_suppression_in_comments_skips_invalid_directive() {
let line = "// diffguard: nope rust.no_unwrap";
let masked = masked_comments(line, Language::Rust);
assert!(parse_suppression_in_comments(line, &masked).is_none());
}
#[test]
fn parse_rule_ids_empty_returns_none() {
assert!(parse_rule_ids(" ").is_none());
assert!(parse_rule_ids(" , , ").is_none());
}
#[test]
fn tracker_same_line_suppression() {
let mut tracker = SuppressionTracker::new();
let line = "let x = y.unwrap(); // diffguard: ignore rust.no_unwrap";
let masked = masked_comments(line, Language::Rust);
let effective = tracker.process_line(line, &masked);
assert!(effective.is_suppressed("rust.no_unwrap"));
assert!(!effective.is_suppressed("other.rule"));
}
#[test]
fn tracker_next_line_suppression() {
let mut tracker = SuppressionTracker::new();
let line1 = "// diffguard: ignore-next-line rust.no_dbg";
let masked1 = masked_comments(line1, Language::Rust);
let effective1 = tracker.process_line(line1, &masked1);
assert!(!effective1.is_suppressed("rust.no_dbg"));
let line2 = "dbg!(value);";
let masked2 = masked_comments(line2, Language::Rust);
let effective2 = tracker.process_line(line2, &masked2);
assert!(effective2.is_suppressed("rust.no_dbg"));
let line3 = "dbg!(other);";
let masked3 = masked_comments(line3, Language::Rust);
let effective3 = tracker.process_line(line3, &masked3);
assert!(!effective3.is_suppressed("rust.no_dbg"));
}
#[test]
fn tracker_both_same_and_next_line() {
let mut tracker = SuppressionTracker::new();
let line1 = "// diffguard: ignore-next-line rule1";
let masked1 = masked_comments(line1, Language::Rust);
let effective1 = tracker.process_line(line1, &masked1);
assert!(!effective1.is_suppressed("rule1"));
let line2 = "x = 1 // diffguard: ignore rule2";
let masked2 = masked_comments(line2, Language::Rust);
let effective2 = tracker.process_line(line2, &masked2);
assert!(effective2.is_suppressed("rule1")); assert!(effective2.is_suppressed("rule2")); }
#[test]
fn tracker_wildcard_suppression() {
let mut tracker = SuppressionTracker::new();
let line = "// diffguard: ignore *";
let masked = masked_comments(line, Language::Rust);
let effective = tracker.process_line(line, &masked);
assert!(effective.is_suppressed("any.rule"));
assert!(effective.is_suppressed("other.rule"));
assert!(effective.suppress_all);
}
#[test]
fn tracker_reset_clears_pending() {
let mut tracker = SuppressionTracker::new();
let line1 = "// diffguard: ignore-next-line rule1";
let masked1 = masked_comments(line1, Language::Rust);
tracker.process_line(line1, &masked1);
tracker.reset();
let line2 = "some code";
let masked2 = masked_comments(line2, Language::Rust);
let effective = tracker.process_line(line2, &masked2);
assert!(!effective.is_suppressed("rule1"));
}
#[test]
fn tracker_multiple_next_line_directives() {
let mut tracker = SuppressionTracker::new();
let line1 = "// diffguard: ignore-next-line rule1";
let masked1 = masked_comments(line1, Language::Rust);
tracker.process_line(line1, &masked1);
let line2 = "// diffguard: ignore-next-line rule2";
let masked2 = masked_comments(line2, Language::Rust);
let effective1 = tracker.process_line(line2, &masked2);
assert!(effective1.is_suppressed("rule1"));
let line3 = "actual code";
let masked3 = masked_comments(line3, Language::Rust);
let effective2 = tracker.process_line(line3, &masked3);
assert!(effective2.is_suppressed("rule2"));
assert!(!effective2.is_suppressed("rule1"));
}
#[test]
fn effective_suppressions_is_empty() {
let effective = EffectiveSuppressions::default();
assert!(effective.is_empty());
assert!(!effective.is_suppressed("any.rule"));
}
#[test]
fn effective_suppressions_specific_rules() {
let mut effective = EffectiveSuppressions::default();
effective.suppressed_rules.insert("rule1".to_string());
effective.suppressed_rules.insert("rule2".to_string());
assert!(!effective.is_empty());
assert!(effective.is_suppressed("rule1"));
assert!(effective.is_suppressed("rule2"));
assert!(!effective.is_suppressed("rule3"));
}
#[test]
fn effective_suppressions_wildcard() {
let effective = EffectiveSuppressions {
suppress_all: true,
..Default::default()
};
assert!(!effective.is_empty());
assert!(effective.is_suppressed("any.rule"));
assert!(effective.is_suppressed("other.rule"));
}
}