use crate::domain::{target_kind, target_names, SuppressionTarget, TargetKind};
pub use crate::domain::{Dimension, Suppression};
pub const ANNOTATION_WINDOW: usize = 3;
pub fn is_within_window(annotation_line: usize, target_line: usize) -> bool {
annotation_line <= target_line && target_line - annotation_line <= ANNOTATION_WINDOW
}
pub fn has_annotation_in_window(
lines: &std::collections::HashSet<usize>,
target_line: usize,
) -> bool {
(0..=ANNOTATION_WINDOW).any(|off| target_line >= off && lines.contains(&(target_line - off)))
}
pub fn is_api_marker(trimmed: &str) -> bool {
trimmed == "// qual:api" || trimmed.starts_with("// qual:api ")
}
pub fn is_test_helper_marker(trimmed: &str) -> bool {
trimmed == "// qual:test_helper" || trimmed.starts_with("// qual:test_helper ")
}
pub fn is_unsafe_allow_marker(trimmed: &str) -> bool {
trimmed == "// qual:allow(unsafe)" || trimmed.starts_with("// qual:allow(unsafe) ")
}
pub fn is_recursive_marker(trimmed: &str) -> bool {
trimmed == "// qual:recursive" || trimmed.starts_with("// qual:recursive ")
}
pub fn parse_inverse_marker(trimmed: &str) -> Option<String> {
trimmed
.strip_prefix("// qual:inverse(")
.and_then(|rest| rest.strip_suffix(')'))
.map(|name| name.trim().to_string())
.filter(|name| !name.is_empty())
}
pub fn parse_suppression(line_number: usize, trimmed: &str) -> Option<Suppression> {
if is_unsafe_allow_marker(trimmed) || is_test_helper_marker(trimmed) {
return None;
}
trimmed
.strip_prefix("// qual:allow")
.and_then(|rest| parse_qual_allow(line_number, rest))
.or_else(|| parse_iosp_legacy(line_number, trimmed))
}
fn parse_iosp_legacy(line_number: usize, trimmed: &str) -> Option<Suppression> {
if trimmed == "// iosp:allow" || trimmed.starts_with("// iosp:allow ") {
let reason = trimmed
.strip_prefix("// iosp:allow ")
.map(|s| s.to_string());
Some(Suppression::blanket(
line_number,
vec![Dimension::Iosp],
reason,
))
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvalidQualAllow {
UnknownDimensions(String),
UnclosedParens(String),
UnknownTarget {
dim: String,
target: String,
valid: Vec<String>,
},
MetricNeedsValue { dim: String, target: String },
BooleanTakesNoValue { dim: String, target: String },
BadPinValue { target: String, value: String },
TargetNeedsReason { dim: String, target: String },
BlanketNotAllowed { dim: String, valid: Vec<String> },
}
impl InvalidQualAllow {
pub fn reason(&self) -> String {
match self {
Self::UnknownDimensions(spec) => {
format!("invalid qual:allow — '{spec}' did not parse to any known dimension")
}
Self::UnclosedParens(spec) => format!(
"invalid qual:allow — marker has unclosed parens (missing `)`); content was '{spec}'"
),
Self::UnknownTarget { dim, target, valid } => format!(
"invalid qual:allow — '{target}' is not a known target of {dim}; valid targets: {}",
valid.join(", ")
),
Self::MetricNeedsValue { dim, target } => format!(
"invalid qual:allow — {dim} target '{target}' is a threshold metric and needs a value, e.g. {target}=<n>"
),
Self::BooleanTakesNoValue { dim, target } => format!(
"invalid qual:allow — {dim} target '{target}' is a boolean finding and takes no value"
),
Self::BadPinValue { target, value } => format!(
"invalid qual:allow — pin value '{value}' for '{target}' must be a finite, non-negative number"
),
Self::TargetNeedsReason { dim, target } => format!(
"invalid qual:allow — targeted suppression ({dim}, {target}) requires a reason: \"…\""
),
Self::BlanketNotAllowed { dim, valid } => format!(
"invalid qual:allow — bare ({dim}) would silence every {dim} finding; name what you mean: allow({dim}, <target>). Targets: {}",
valid.join(", ")
),
}
}
}
pub fn detect_invalid_qual_allow(trimmed: &str) -> Option<InvalidQualAllow> {
if is_unsafe_allow_marker(trimmed) {
return None;
}
let rest = trimmed.strip_prefix("// qual:allow")?;
match classify_qual_allow(0, rest) {
AllowParse::Invalid(invalid) => Some(invalid),
_ => None,
}
}
fn parse_qual_allow(line_number: usize, rest: &str) -> Option<Suppression> {
match classify_qual_allow(line_number, rest) {
AllowParse::Valid(suppression) => Some(suppression),
_ => None,
}
}
enum AllowParse {
Valid(Suppression),
Bare,
Invalid(InvalidQualAllow),
}
fn classify_qual_allow(line: usize, rest: &str) -> AllowParse {
let rest = rest.trim();
if rest.is_empty() || !rest.starts_with('(') {
return AllowParse::Bare;
}
let Some(close) = rest.find(')') else {
return AllowParse::Invalid(InvalidQualAllow::UnclosedParens(
rest[1..].trim().to_string(),
));
};
let inner = rest[1..close].trim();
if inner.is_empty() {
return AllowParse::Bare;
}
let after = rest.get(close + 1..).map(str::trim).unwrap_or("");
let reason = (!after.is_empty()).then(|| extract_reason(after)).flatten();
classify_entries(line, inner, reason)
}
fn classify_entries(line: usize, inner: &str, reason: Option<String>) -> AllowParse {
let entries: Vec<&str> = inner.split(',').map(str::trim).collect();
let Some(dim0) = Dimension::from_str_opt(entries[0]) else {
return AllowParse::Invalid(InvalidQualAllow::UnknownDimensions(inner.to_string()));
};
let extra = &entries[1..];
if extra.is_empty() {
return blanket_or_invalid(line, vec![dim0], reason);
}
let target_like = |e: &&str| e.contains('=') || Dimension::from_str_opt(e).is_none();
if !extra.iter().any(target_like) {
let dims = std::iter::once(dim0)
.chain(extra.iter().filter_map(|e| Dimension::from_str_opt(e)))
.collect();
return blanket_or_invalid(line, dims, reason);
}
classify_target(line, dim0, extra, reason)
}
fn blanket_or_invalid(line: usize, dims: Vec<Dimension>, reason: Option<String>) -> AllowParse {
if let Some(dim) = dims.iter().copied().find(|d| !target_names(*d).is_empty()) {
return AllowParse::Invalid(InvalidQualAllow::BlanketNotAllowed {
dim: dim.to_string(),
valid: target_names(dim).iter().map(|s| s.to_string()).collect(),
});
}
AllowParse::Valid(Suppression::blanket(line, dims, reason))
}
fn classify_target(
line: usize,
dim: Dimension,
extra: &[&str],
reason: Option<String>,
) -> AllowParse {
let unknown = |target: String| {
AllowParse::Invalid(InvalidQualAllow::UnknownTarget {
dim: dim.to_string(),
target,
valid: target_names(dim).iter().map(|s| s.to_string()).collect(),
})
};
if extra.len() != 1 {
return unknown(extra.join(", "));
}
let (name, value) = match extra[0].split_once('=') {
Some((n, v)) => (n.trim(), Some(v.trim())),
None => (extra[0], None),
};
match target_kind(dim, name) {
None => unknown(name.to_string()),
Some(TargetKind::Metric) => classify_metric(line, dim, name, value, reason),
Some(TargetKind::Boolean) => classify_boolean(line, dim, name, value, reason),
}
}
fn classify_metric(
line: usize,
dim: Dimension,
name: &str,
value: Option<&str>,
reason: Option<String>,
) -> AllowParse {
let Some(value) = value else {
return AllowParse::Invalid(InvalidQualAllow::MetricNeedsValue {
dim: dim.to_string(),
target: name.to_string(),
});
};
let pin = match value.parse::<f64>() {
Ok(p) if p.is_finite() && p >= 0.0 => p,
_ => {
return AllowParse::Invalid(InvalidQualAllow::BadPinValue {
target: name.to_string(),
value: value.to_string(),
});
}
};
if reason.is_none() {
return AllowParse::Invalid(InvalidQualAllow::TargetNeedsReason {
dim: dim.to_string(),
target: name.to_string(),
});
}
AllowParse::Valid(Suppression {
line,
dimensions: vec![dim],
reason,
target: Some(SuppressionTarget::Metric {
name: name.to_string(),
pin,
}),
})
}
fn classify_boolean(
line: usize,
dim: Dimension,
name: &str,
value: Option<&str>,
reason: Option<String>,
) -> AllowParse {
if value.is_some() {
return AllowParse::Invalid(InvalidQualAllow::BooleanTakesNoValue {
dim: dim.to_string(),
target: name.to_string(),
});
}
if reason.is_none() {
return AllowParse::Invalid(InvalidQualAllow::TargetNeedsReason {
dim: dim.to_string(),
target: name.to_string(),
});
}
AllowParse::Valid(Suppression {
line,
dimensions: vec![dim],
reason,
target: Some(SuppressionTarget::Boolean {
name: name.to_string(),
}),
})
}
fn extract_reason(text: &str) -> Option<String> {
let text = text.trim();
if text.is_empty() {
return None;
}
if let Some(rest) = text.strip_prefix("reason:") {
let rest = rest.trim();
if rest.starts_with('"') && rest.ends_with('"') && rest.len() > 1 {
return Some(rest[1..rest.len() - 1].to_string());
}
return Some(rest.to_string());
}
Some(text.to_string())
}