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")
.map(|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 {
line: line_number,
dimensions: vec![Dimension::Iosp],
reason,
})
} else {
None
}
}
fn parse_qual_allow(line_number: usize, rest: &str) -> Suppression {
let rest = rest.trim();
let (dimensions, reason_text) = if rest.is_empty() || !rest.starts_with('(') {
(vec![], rest)
} else {
let close_paren = rest.find(')').unwrap_or(rest.len());
let dims_str = &rest[1..close_paren];
let dimensions: Vec<Dimension> = dims_str
.split(',')
.filter_map(|s| Dimension::from_str_opt(s.trim()))
.collect();
let after_parens = rest.get(close_paren + 1..).map(str::trim).unwrap_or("");
(dimensions, after_parens)
};
let reason = (!reason_text.is_empty())
.then(|| extract_reason(reason_text))
.flatten();
Suppression {
line: line_number,
dimensions,
reason,
}
}
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())
}