mod complexity_predicates;
mod positions;
use std::collections::HashMap;
use crate::adapters::suppression::qual_allow::InvalidQualAllow;
use crate::domain::findings::{OrphanKind, OrphanSuppression};
use crate::findings::Suppression;
use positions::{enumerate_finding_positions, FindingPosition, MatchMode};
pub(crate) fn detect_orphan_suppressions(
suppression_lines: &HashMap<String, Vec<Suppression>>,
invalid_qual_allow_lines: &HashMap<String, Vec<(usize, InvalidQualAllow)>>,
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
) -> Vec<OrphanSuppression> {
let positions = enumerate_finding_positions(analysis, config);
let headroom = config.suppression.pin_headroom;
let mut orphans: Vec<OrphanSuppression> = suppression_lines
.iter()
.flat_map(|(file, sups)| {
sups.iter()
.filter_map(|sup| classify_marker(file, sup, &positions, headroom))
.collect::<Vec<_>>()
})
.collect();
orphans.extend(invalid_marker_orphans(invalid_qual_allow_lines));
orphans.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
orphans
}
fn classify_marker(
file: &str,
sup: &Suppression,
positions: &HashMap<String, Vec<FindingPosition>>,
headroom: f64,
) -> Option<OrphanSuppression> {
if !is_verifiable(sup, file, positions) {
return None;
}
if !has_matching_finding(file, sup, positions) {
return Some(orphan(file, sup, OrphanKind::Stale, sup.reason.clone()));
}
let value = too_loose_value(file, sup, positions, headroom)?;
let pin = sup
.target
.as_ref()
.and_then(|t| t.pin())
.unwrap_or_default();
let reason = format!(
"pin {} sits >{:.0}% above the actual value {} — tighten to ~{} or remove",
fmt_num(pin),
headroom * 100.0,
fmt_num(value),
fmt_num(value),
);
Some(orphan(file, sup, OrphanKind::PinTooLoose, Some(reason)))
}
fn orphan(
file: &str,
sup: &Suppression,
kind: OrphanKind,
reason: Option<String>,
) -> OrphanSuppression {
OrphanSuppression {
file: file.to_string(),
line: sup.line,
dimensions: sup.dimensions.clone(),
target: sup.target.clone(),
reason,
kind,
}
}
fn fmt_num(v: f64) -> String {
if v.fract() == 0.0 {
format!("{v:.0}")
} else {
format!("{v}")
}
}
fn too_loose_value(
file: &str,
sup: &Suppression,
positions: &HashMap<String, Vec<FindingPosition>>,
headroom: f64,
) -> Option<f64> {
let pin = sup.target.as_ref()?.pin()?;
let covered_max = positions
.get(file)?
.iter()
.filter(|p| position_matches(sup, p))
.filter_map(|p| p.value)
.filter(|&v| v <= pin)
.fold(None, |acc: Option<f64>, v| {
Some(acc.map_or(v, |a| a.max(v)))
})?;
(pin > covered_max * (1.0 + headroom)).then_some(covered_max)
}
fn invalid_marker_orphans(
invalid_qual_allow_lines: &HashMap<String, Vec<(usize, InvalidQualAllow)>>,
) -> Vec<OrphanSuppression> {
invalid_qual_allow_lines
.iter()
.flat_map(|(file, markers)| {
markers.iter().map(move |(line, kind)| OrphanSuppression {
file: file.clone(),
line: *line,
dimensions: Vec::new(),
target: None,
reason: Some(kind.reason()),
kind: OrphanKind::Stale,
})
})
.collect()
}
fn is_verifiable(
sup: &Suppression,
file: &str,
positions: &HashMap<String, Vec<FindingPosition>>,
) -> bool {
use crate::findings::Dimension;
if sup.dimensions.is_empty() {
return true;
}
if sup.dimensions.iter().any(|d| *d != Dimension::Coupling) {
return true;
}
if sup
.target
.as_ref()
.is_some_and(|t| is_module_global_coupling(t.name()))
{
return false;
}
positions
.get(file)
.is_some_and(|ps| ps.iter().any(|p| p.dim == Dimension::Coupling))
}
fn is_module_global_coupling(target: &str) -> bool {
matches!(
target,
"max_fan_in" | "max_fan_out" | "max_instability" | "sdp"
)
}
fn has_matching_finding(
file: &str,
sup: &Suppression,
positions: &HashMap<String, Vec<FindingPosition>>,
) -> bool {
positions
.get(file)
.is_some_and(|ps| ps.iter().any(|p| position_matches(sup, p)))
}
fn position_matches(sup: &Suppression, p: &FindingPosition) -> bool {
if !sup.covers(p.dim) || !mode_accepts(sup.line, p.line, p.mode) {
return false;
}
match &sup.target {
None => true,
Some(t) => p.target == Some(t.name()),
}
}
fn mode_accepts(sup_line: usize, finding_line: usize, mode: MatchMode) -> bool {
match mode {
MatchMode::FileScope => true,
MatchMode::LineWindow(n) => finding_line >= sup_line && finding_line - sup_line <= n,
}
}