mod complexity_predicates;
use std::collections::HashMap;
use crate::adapters::analyzers::iosp::Classification;
use crate::domain::findings::OrphanSuppression;
use crate::findings::Suppression;
use super::suppression_windows as windows;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MatchMode {
LineWindow(usize),
FileScope,
}
pub(crate) fn detect_orphan_suppressions(
suppression_lines: &HashMap<String, Vec<Suppression>>,
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
) -> Vec<OrphanSuppression> {
let positions = enumerate_finding_positions(analysis, config);
let mut orphans: Vec<OrphanSuppression> = suppression_lines
.iter()
.flat_map(|(file, sups)| {
sups.iter()
.filter(|sup| is_verifiable(sup, file, &positions))
.filter(|sup| !has_matching_finding(file, sup, &positions))
.map(|sup| OrphanSuppression {
file: file.clone(),
line: sup.line,
dimensions: sup.dimensions.clone(),
reason: sup.reason.clone(),
})
.collect::<Vec<_>>()
})
.collect();
orphans.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
orphans
}
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;
}
positions
.get(file)
.is_some_and(|ps| ps.iter().any(|p| p.dim == Dimension::Coupling))
}
fn has_matching_finding(
file: &str,
sup: &Suppression,
positions: &HashMap<String, Vec<FindingPosition>>,
) -> bool {
let Some(file_positions) = positions.get(file) else {
return false;
};
file_positions
.iter()
.any(|p| sup.covers(p.dim) && mode_accepts(sup.line, p.line, p.mode))
}
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,
}
}
#[derive(Debug, Clone, Copy)]
struct FindingPosition {
line: usize,
dim: crate::findings::Dimension,
mode: MatchMode,
}
fn enumerate_finding_positions(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
) -> HashMap<String, Vec<FindingPosition>> {
let mut out: HashMap<String, Vec<FindingPosition>> = HashMap::new();
let mut push = |file: &str, line: usize, dim: crate::findings::Dimension, mode: MatchMode| {
if !file.is_empty() {
out.entry(file.to_string())
.or_default()
.push(FindingPosition { line, dim, mode });
}
};
collect_iosp_complexity_positions(analysis, config, &mut push);
collect_dry_positions(analysis, config, &mut push);
collect_srp_positions(analysis, config, &mut push);
collect_tq_positions(analysis, config, &mut push);
collect_structural_positions(analysis, config, &mut push);
collect_architecture_positions(analysis, config, &mut push);
out
}
fn collect_iosp_complexity_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::findings::Dimension;
let mode = MatchMode::LineWindow(windows::DEFAULT);
let complexity_enabled = config.complexity.enabled;
analysis.results.iter().for_each(|f| {
if matches!(f.classification, Classification::Violation { .. }) {
push(&f.file, f.line, Dimension::Iosp, mode);
}
if !complexity_enabled {
return;
}
if let Some(c) = &f.complexity {
if complexity_predicates::would_trigger(f, c, &config.complexity) {
push(&f.file, f.line, Dimension::Complexity, mode);
}
push_magic_numbers(f, c, &config.complexity, push);
}
});
}
fn push_magic_numbers<F>(
f: &crate::adapters::analyzers::iosp::FunctionAnalysis,
c: &crate::adapters::analyzers::iosp::ComplexityMetrics,
cx: &crate::config::sections::ComplexityConfig,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
if f.is_test || !cx.detect_magic_numbers {
return;
}
let mode = MatchMode::LineWindow(windows::DEFAULT);
c.magic_numbers.iter().for_each(|m| {
push(
&f.file,
m.line,
crate::findings::Dimension::Complexity,
mode,
)
});
}
fn collect_dry_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::findings::Dimension;
if !config.duplicates.enabled && !config.boilerplate.enabled {
return;
}
use crate::domain::findings::DryFindingKind;
let mode = MatchMode::LineWindow(windows::DEFAULT);
let wildcard_mode = MatchMode::LineWindow(windows::WILDCARD);
analysis.findings.dry.iter().for_each(|f| {
let m = match f.kind {
DryFindingKind::DuplicateExact
| DryFindingKind::DuplicateSimilar
| DryFindingKind::Fragment
| DryFindingKind::Boilerplate
| DryFindingKind::RepeatedMatch => mode,
DryFindingKind::Wildcard => wildcard_mode,
DryFindingKind::DeadCodeUncalled | DryFindingKind::DeadCodeTestOnly => return,
};
push(&f.common.file, f.common.line, Dimension::Dry, m);
});
}
fn collect_srp_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::domain::findings::SrpFindingKind;
use crate::findings::Dimension;
if !config.srp.enabled {
return;
}
let line_mode = MatchMode::LineWindow(windows::SRP_STRUCT_PARAM);
analysis.findings.srp.iter().for_each(|f| match f.kind {
SrpFindingKind::StructCohesion | SrpFindingKind::ParameterCount => {
push(&f.common.file, f.common.line, Dimension::Srp, line_mode);
}
SrpFindingKind::ModuleLength => {
push(&f.common.file, 1, Dimension::Srp, MatchMode::FileScope);
}
SrpFindingKind::Structural => {}
});
}
fn collect_tq_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::findings::Dimension;
if !config.test_quality.enabled {
return;
}
let mode = MatchMode::LineWindow(windows::TQ);
analysis.findings.test_quality.iter().for_each(|f| {
push(&f.common.file, f.common.line, Dimension::TestQuality, mode);
});
}
fn collect_structural_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::domain::findings::{CouplingFindingKind, SrpFindingKind};
use crate::findings::Dimension;
if !config.structural.enabled {
return;
}
let mode = MatchMode::LineWindow(windows::STRUCTURAL);
analysis
.findings
.srp
.iter()
.filter(|f| matches!(f.kind, SrpFindingKind::Structural))
.for_each(|f| push(&f.common.file, f.common.line, Dimension::Srp, mode));
analysis
.findings
.coupling
.iter()
.filter(|f| matches!(f.kind, CouplingFindingKind::Structural))
.for_each(|f| push(&f.common.file, f.common.line, Dimension::Coupling, mode));
}
fn collect_architecture_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, usize, crate::findings::Dimension, MatchMode),
{
use crate::findings::Dimension;
if !config.architecture.enabled {
return;
}
let mode = MatchMode::LineWindow(windows::DEFAULT);
analysis
.findings
.architecture
.iter()
.for_each(|f| push(&f.common.file, f.common.line, Dimension::Architecture, mode));
}