use crate::Finding;
pub fn filter_ignored(findings: Vec<Finding>, source: &str) -> Vec<Finding> {
let ignores = parse_ignore_comments(source);
let overrides = parse_set_comments(source);
if ignores.is_empty() && overrides.is_empty() {
return findings;
}
findings
.into_iter()
.filter(|f| !is_suppressed(f, &ignores))
.filter(|f| !is_overridden_away(f, &overrides))
.collect()
}
struct IgnoreDirective {
line: usize,
rules: Vec<String>,
}
struct SetDirective {
line: usize,
rule: Option<String>,
value: f64,
}
fn parse_ignore_comments(source: &str) -> Vec<IgnoreDirective> {
source
.lines()
.enumerate()
.filter_map(|(i, line)| {
let after = extract_directive_payload(line.trim(), "cha:ignore")?;
let rules = if after.is_empty() {
vec![]
} else {
after.split(',').map(|s| s.trim().to_string()).collect()
};
Some(IgnoreDirective { line: i + 1, rules })
})
.collect()
}
fn parse_set_comments(source: &str) -> Vec<SetDirective> {
source
.lines()
.enumerate()
.filter_map(|(i, line)| {
let after = extract_directive_payload(line.trim(), "cha:set")?;
let (key, val_str) = after.split_once('=')?;
let value: f64 = val_str.trim().parse().ok()?;
let key = key.trim();
let rule = if key == "threshold" {
None
} else {
Some(key.to_string())
};
Some(SetDirective {
line: i + 1,
rule,
value,
})
})
.collect()
}
fn extract_directive_payload<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
for prefix in ["//", "#", "--"] {
if let Some(rest) = line.strip_prefix(prefix)
&& let Some(payload) = rest.trim().strip_prefix(directive)
{
return Some(payload.trim());
}
}
if let Some(rest) = line.strip_prefix("/*") {
let rest = rest.strip_suffix("*/").unwrap_or(rest);
if let Some(payload) = rest.trim().strip_prefix(directive) {
return Some(payload.trim());
}
}
None
}
fn covers(directive_line: usize, finding: &Finding) -> bool {
directive_line == finding.location.start_line
|| directive_line + 1 == finding.location.start_line
}
fn is_suppressed(finding: &Finding, ignores: &[IgnoreDirective]) -> bool {
ignores.iter().any(|ig| {
if !covers(ig.line, finding) {
return false;
}
ig.rules.is_empty() || ig.rules.iter().any(|r| r == &finding.smell_name)
})
}
fn is_overridden_away(finding: &Finding, overrides: &[SetDirective]) -> bool {
let (actual, _threshold) = match (finding.actual_value, finding.threshold) {
(Some(a), Some(t)) => (a, t),
_ => return false, };
overrides.iter().any(|sd| {
if !covers(sd.line, finding) {
return false;
}
let rule_matches = match &sd.rule {
None => true,
Some(r) => r == &finding.smell_name,
};
rule_matches && actual < sd.value
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Location, Severity, SmellCategory};
use std::path::PathBuf;
fn make_finding(name: &str, start: usize) -> Finding {
Finding {
smell_name: name.to_string(),
category: SmellCategory::Bloaters,
severity: Severity::Warning,
location: Location {
path: PathBuf::from("test.rs"),
start_line: start,
end_line: start + 5,
name: None,
},
message: "test".into(),
suggested_refactorings: vec![],
..Default::default()
}
}
fn make_finding_with_value(name: &str, start: usize, actual: f64, threshold: f64) -> Finding {
Finding {
actual_value: Some(actual),
threshold: Some(threshold),
..make_finding(name, start)
}
}
#[test]
fn ignore_all_rules() {
let src = "// cha:ignore\nfn foo() {}";
let findings = vec![make_finding("switch_statement", 2)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn ignore_specific_rule() {
let src = "// cha:ignore switch_statement\nfn foo() {}";
let findings = vec![
make_finding("switch_statement", 2),
make_finding("long_method", 2),
];
let result = filter_ignored(findings, src);
assert_eq!(result.len(), 1);
assert_eq!(result[0].smell_name, "long_method");
}
#[test]
fn ignore_multiple_rules() {
let src = "// cha:ignore switch_statement,long_method\nfn foo() {}";
let findings = vec![
make_finding("switch_statement", 2),
make_finding("long_method", 2),
];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn no_ignore_comment() {
let src = "fn foo() {}";
let findings = vec![make_finding("switch_statement", 1)];
assert_eq!(filter_ignored(findings, src).len(), 1);
}
#[test]
fn python_style() {
let src = "# cha:ignore\ndef foo(): pass";
let findings = vec![make_finding("long_method", 2)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn set_raises_threshold_suppresses() {
let src = "// cha:set long_method=100\nfn foo() {}";
let findings = vec![make_finding_with_value("long_method", 2, 80.0, 50.0)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn set_threshold_still_exceeded() {
let src = "// cha:set long_method=100\nfn foo() {}";
let findings = vec![make_finding_with_value("long_method", 2, 120.0, 50.0)];
assert_eq!(filter_ignored(findings, src).len(), 1);
}
#[test]
fn set_generic_threshold() {
let src = "// cha:set threshold=100\nfn foo() {}";
let findings = vec![make_finding_with_value("long_method", 2, 80.0, 50.0)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn set_wrong_rule_no_effect() {
let src = "// cha:set high_complexity=100\nfn foo() {}";
let findings = vec![make_finding_with_value("long_method", 2, 80.0, 50.0)];
assert_eq!(filter_ignored(findings, src).len(), 1);
}
#[test]
fn set_no_actual_value_no_effect() {
let src = "// cha:set long_method=100\nfn foo() {}";
let findings = vec![make_finding("long_method", 2)];
assert_eq!(filter_ignored(findings, src).len(), 1);
}
#[test]
fn set_python_style() {
let src = "# cha:set long_method=100\ndef foo(): pass";
let findings = vec![make_finding_with_value("long_method", 2, 80.0, 50.0)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn set_block_comment_style() {
let src = "/* cha:set long_method=100 */\nfn foo() {}";
let findings = vec![make_finding_with_value("long_method", 2, 80.0, 50.0)];
assert!(filter_ignored(findings, src).is_empty());
}
#[test]
fn set_does_not_affect_other_lines() {
let src = "fn bar() {}\n// cha:set long_method=100\nfn foo() {}";
let findings = vec![
make_finding_with_value("long_method", 1, 80.0, 50.0), make_finding_with_value("long_method", 3, 80.0, 50.0), ];
let result = filter_ignored(findings, src);
assert_eq!(result.len(), 1);
assert_eq!(result[0].location.start_line, 1);
}
}