use crate::check::{RuleId, Violation};
const NEXT_LINE: &str = "easement:disable-next-line";
const SAME_LINE: &str = "easement:disable-line";
#[derive(Debug, Clone)]
pub struct Suppression {
pub file: String,
pub line: usize,
pub target_line: usize,
pub rules: Vec<String>,
pub reason: Option<String>,
}
pub fn parse(file: &str, source: &str) -> Vec<Suppression> {
let mut found = Vec::new();
for (i, text) in source.lines().enumerate() {
let line = i + 1;
let (rest, target_line) = if let Some(at) = text.find(NEXT_LINE) {
(&text[at + NEXT_LINE.len()..], line + 1)
} else if let Some(at) = text.find(SAME_LINE) {
(&text[at + SAME_LINE.len()..], line)
} else {
continue;
};
let rest = rest
.trim()
.trim_end_matches("*/")
.trim_end_matches("-->")
.trim();
let (rules, reason) = match rest.split_once("--") {
Some((rules, reason)) => (rules, Some(reason.trim())),
None => (rest, None),
};
found.push(Suppression {
file: file.to_string(),
line,
target_line,
rules: rules
.split([',', ' '])
.filter(|r| !r.is_empty())
.map(str::to_string)
.collect(),
reason: reason.filter(|r| !r.is_empty()).map(str::to_string),
});
}
found
}
pub fn apply(violations: Vec<Violation>, suppressions: &[Suppression]) -> (Vec<Violation>, usize) {
let mut used = vec![false; suppressions.len()];
let mut kept = Vec::new();
let mut suppressed = 0;
for v in violations {
let hit = suppressions.iter().position(|s| {
s.reason.is_some()
&& s.file == v.file
&& s.target_line == v.line
&& s.rules.iter().any(|r| r == v.rule.as_str())
});
match hit {
Some(i) => {
used[i] = true;
suppressed += 1;
}
None => kept.push(v),
}
}
for (s, used) in suppressions.iter().zip(used) {
let problem = if s.reason.is_none() {
Some((
RuleId::SuppressionReason,
format!(
"suppression of {} needs a reason after `--`",
s.rules.join(",")
),
))
} else if let Some(bad) = s.rules.iter().find(|r| RuleId::suppressible(r).is_none()) {
Some((
RuleId::UnusedSuppression,
format!(
"`{bad}` is not a rule that can be suppressed (use {})",
RuleId::SUPPRESSIBLE.map(RuleId::as_str).join(", ")
),
))
} else if !used {
Some((
RuleId::UnusedSuppression,
format!(
"suppression of {} matches no violation on line {}; remove it",
s.rules.join(","),
s.target_line
),
))
} else {
None
};
if let Some((rule, message)) = problem {
kept.push(Violation {
rule,
file: s.file.clone(),
line: s.line,
message,
because: None,
from: String::new(),
to: String::new(),
target: format!("line {}", s.line),
specifier: String::new(),
evidence: vec![(s.file.clone(), s.line)],
});
}
}
(kept, suppressed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_comment_styles() {
let src = "// easement:disable-next-line dependency -- legacy export\n\
import x from './x'; // easement:disable-line dependency, privacy -- generated\n\
# easement:disable-next-line privacy\n\
/* easement:disable-line privacy -- why */\n";
let got = parse("a.ts", src);
assert_eq!(got.len(), 4);
assert_eq!(got[0].target_line, 2);
assert_eq!(got[0].reason.as_deref(), Some("legacy export"));
assert_eq!(got[1].target_line, 2);
assert_eq!(got[1].rules, ["dependency", "privacy"]);
assert_eq!(got[2].reason, None);
assert_eq!(got[3].rules, ["privacy"]);
assert_eq!(got[3].reason.as_deref(), Some("why"));
}
}