use std::collections::HashMap;
use crate::adapters::analyzers::iosp::Classification;
use crate::app::suppression_windows as windows;
use super::complexity_predicates;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum MatchMode {
LineWindow(usize),
FileScope,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct FindingPosition {
pub(super) line: usize,
pub(super) dim: crate::findings::Dimension,
pub(super) mode: MatchMode,
pub(super) target: Option<&'static str>,
pub(super) value: Option<f64>,
}
pub(super) 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, pos: FindingPosition| {
if !file.is_empty() {
out.entry(file.to_string()).or_default().push(pos);
}
};
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, FindingPosition),
{
use crate::findings::Dimension;
let mode = MatchMode::LineWindow(windows::DEFAULT);
let mk = |line, dim, target, value| FindingPosition {
line,
dim,
mode,
target,
value,
};
let complexity_enabled = config.complexity.enabled;
let test_max_lines = config
.tests
.max_function_lines
.unwrap_or(config.complexity.max_function_lines);
analysis.results.iter().for_each(|f| {
if matches!(f.classification, Classification::Violation { .. }) {
push(&f.file, mk(f.line, Dimension::Iosp, None, None));
}
if !complexity_enabled {
return;
}
if let Some(c) = &f.complexity {
for (target, value) in
complexity_predicates::triggered_targets(f, c, &config.complexity, test_max_lines)
{
push(
&f.file,
mk(f.line, Dimension::Complexity, Some(target), value),
);
}
let cx = &config.complexity;
if cx.detect_magic_numbers && !f.is_test && !c.magic_numbers.is_empty() {
push(
&f.file,
mk(f.line, Dimension::Complexity, Some("magic_numbers"), None),
);
}
}
});
}
fn collect_dry_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, FindingPosition),
{
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, target) = match f.kind {
DryFindingKind::DuplicateExact | DryFindingKind::DuplicateSimilar => {
(mode, "duplicate")
}
DryFindingKind::Fragment => (mode, "fragment"),
DryFindingKind::Boilerplate => (mode, "boilerplate"),
DryFindingKind::RepeatedMatch => (mode, "repeated_matches"),
DryFindingKind::Wildcard => (wildcard_mode, "wildcard_imports"),
DryFindingKind::DeadCodeUncalled | DryFindingKind::DeadCodeTestOnly => return,
};
push(
&f.common.file,
FindingPosition {
line: f.common.line,
dim: Dimension::Dry,
mode: m,
target: Some(target),
value: None,
},
);
});
}
fn collect_srp_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, FindingPosition),
{
use crate::findings::Dimension;
if !config.srp.enabled {
return;
}
let line_mode = MatchMode::LineWindow(windows::SRP_STRUCT_PARAM);
let max_clusters = config.srp.max_independent_clusters;
analysis.findings.srp.iter().for_each(|f| {
for (line, mode, target, value) in srp_finding_targets(f, line_mode, max_clusters) {
push(
&f.common.file,
FindingPosition {
line,
dim: Dimension::Srp,
mode,
target: Some(target),
value,
},
);
}
});
}
fn srp_finding_targets(
f: &crate::domain::findings::SrpFinding,
line_mode: MatchMode,
max_clusters: usize,
) -> Vec<(usize, MatchMode, &'static str, Option<f64>)> {
use crate::domain::findings::{SrpFindingDetails, SrpFindingKind};
let scope = MatchMode::FileScope;
match (&f.kind, &f.details) {
(SrpFindingKind::StructCohesion, _) => {
vec![(f.common.line, line_mode, "god_struct", None)]
}
(
SrpFindingKind::ParameterCount,
SrpFindingDetails::ParameterCount {
parameter_count, ..
},
) => vec![(
f.common.line,
line_mode,
"max_parameters",
Some(*parameter_count as f64),
)],
(
SrpFindingKind::ModuleLength,
SrpFindingDetails::ModuleLength {
production_lines,
independent_clusters,
length_score,
..
},
) => [
(*length_score > 1.0).then_some((
1,
scope,
"file_length",
Some(*production_lines as f64),
)),
(*independent_clusters > max_clusters).then_some((
1,
scope,
"max_independent_clusters",
Some(*independent_clusters as f64),
)),
]
.into_iter()
.flatten()
.collect(),
(SrpFindingKind::Structural, _) => vec![],
(kind, _) => {
debug_assert!(false, "SRP kind/details mismatch for {kind:?}");
vec![]
}
}
}
fn collect_tq_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, FindingPosition),
{
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,
FindingPosition {
line: f.common.line,
dim: Dimension::TestQuality,
mode,
target: Some(f.kind.meta().json_kind),
value: None,
},
);
});
}
fn collect_structural_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, FindingPosition),
{
use crate::adapters::analyzers::structural::target_name_for_code;
use crate::domain::findings::{
CouplingFindingDetails, CouplingFindingKind, SrpFindingDetails, SrpFindingKind,
};
use crate::findings::Dimension;
if !config.structural.enabled {
return;
}
let mode = MatchMode::LineWindow(windows::STRUCTURAL);
let pos = |line, dim, code: &str| FindingPosition {
line,
dim,
mode,
target: target_name_for_code(code),
value: None,
};
analysis.findings.srp.iter().for_each(|f| {
if let (SrpFindingKind::Structural, SrpFindingDetails::Structural { code, .. }) =
(&f.kind, &f.details)
{
push(&f.common.file, pos(f.common.line, Dimension::Srp, code));
}
});
analysis.findings.coupling.iter().for_each(|f| {
if let (CouplingFindingKind::Structural, CouplingFindingDetails::Structural { code, .. }) =
(&f.kind, &f.details)
{
push(
&f.common.file,
pos(f.common.line, Dimension::Coupling, code),
);
}
});
}
fn collect_architecture_positions<F>(
analysis: &crate::report::AnalysisResult,
config: &crate::config::Config,
push: &mut F,
) where
F: FnMut(&str, FindingPosition),
{
use crate::findings::Dimension;
if !config.architecture.enabled {
return;
}
let mode = MatchMode::LineWindow(windows::DEFAULT);
analysis.findings.architecture.iter().for_each(|f| {
let family = crate::app::architecture::arch_family(&f.common.rule_id);
let target = crate::domain::target_names(Dimension::Architecture)
.iter()
.copied()
.find(|n| *n == family);
push(
&f.common.file,
FindingPosition {
line: f.common.line,
dim: Dimension::Architecture,
mode,
target,
value: None,
},
);
});
}