#![cfg_attr(coverage_nightly, coverage(off))]
use super::analysis::{
run_complexity_analysis, run_coverage_analysis, run_dead_code_analysis,
run_duplication_analysis, run_satd_analysis, run_tdg_analysis, AnalysisScope,
};
use super::types::{EnforcementState, PhaseOutcome, QualityProfile, QualityViolation};
use crate::cli::colors as c;
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct QualityAssessment {
pub violations: Vec<QualityViolation>,
pub score: f64,
pub measured_phases: usize,
pub total_phases: usize,
pub files_examined: usize,
}
impl QualityAssessment {
#[must_use]
pub fn any_unmeasured(&self) -> bool {
self.measured_phases < self.total_phases
}
#[must_use]
pub fn verdict_state(&self) -> EnforcementState {
if self.violations.is_empty() && !self.any_unmeasured() {
EnforcementState::Complete
} else {
EnforcementState::Violating
}
}
}
pub(super) fn phase_score(violations: &[QualityViolation]) -> f64 {
violations
.iter()
.map(violation_score)
.fold(1.0_f64, f64::min)
}
fn is_floor_dimension(violation_type: &str) -> bool {
violation_type == "coverage"
}
fn violation_score(v: &QualityViolation) -> f64 {
let ratio = if is_floor_dimension(&v.violation_type) {
if v.target > 0.0 {
v.current / v.target
} else {
0.0
}
} else if v.current > 0.0 {
v.target / v.current
} else {
0.0
};
ratio.clamp(0.0, 1.0)
}
macro_rules! phase {
($label:expr, $call:expr) => {{
$crate::status_eprintln!(" {} {}...", c::dim(">>"), $label);
$call.await?
}};
}
pub async fn assess_project(
project_path: &Path,
profile: &QualityProfile,
specific_file: Option<&Path>,
include_pattern: Option<&String>,
exclude_pattern: Option<&String>,
) -> Result<QualityAssessment> {
if !project_path.exists() {
anyhow::bail!(
"path not found: {} — enforce cannot report a verdict on a path it cannot read",
project_path.display()
);
}
let scope = AnalysisScope::resolve(project_path, specific_file);
if let AnalysisScope::SingleFile { module_dir, .. } = &scope {
eprintln!(
" {} Single-file mode: SATD/dead-code/duplication scoped to parent module {}",
c::dim(">>"),
module_dir.display()
);
}
let outcomes: Vec<(&str, PhaseOutcome)> = vec![
(
"complexity",
phase!(
"Analyzing complexity",
run_complexity_analysis(scope.walk_root(), profile, scope.single_file())
),
),
(
"satd",
phase!(
"Analyzing technical debt (SATD)",
run_satd_analysis(scope.walk_root(), profile, scope.single_file())
),
),
(
"tdg",
phase!(
"Analyzing technical debt gradient",
run_tdg_analysis(scope.file_or_root(), profile)
),
),
(
"dead code",
phase!(
"Analyzing dead code",
run_dead_code_analysis(scope.walk_root(), profile)
),
),
(
"duplication",
phase!(
"Analyzing code duplication",
run_duplication_analysis(scope.walk_root(), profile)
),
),
(
"coverage",
phase!(
"Checking test coverage",
run_coverage_analysis(scope.walk_root(), profile)
),
),
];
summarize(
outcomes,
project_path,
specific_file,
include_pattern,
exclude_pattern,
)
}
pub(super) fn summarize(
mut outcomes: Vec<(&str, PhaseOutcome)>,
project_path: &Path,
specific_file: Option<&Path>,
include_pattern: Option<&String>,
exclude_pattern: Option<&String>,
) -> Result<QualityAssessment> {
if include_pattern.is_some() || exclude_pattern.is_some() {
let filter = crate::utils::file_filter::FileFilter::from_optional(
&include_pattern.cloned(),
&exclude_pattern.cloned(),
)?;
for (_, outcome) in &mut outcomes {
outcome
.violations
.retain(|v| violation_is_included(&filter, project_path, v));
}
}
let measured: Vec<f64> = outcomes
.iter()
.filter(|(_, o)| o.is_measured())
.map(|(_, o)| phase_score(&o.violations))
.collect();
let total_phases = outcomes.len();
let measured_phases = measured.len();
let files_examined = outcomes
.iter()
.map(|(_, o)| o.files_examined)
.max()
.unwrap_or(0);
let score = if measured.is_empty() {
0.0
} else {
let mean = measured.iter().sum::<f64>() / measured.len() as f64;
mean * (measured_phases as f64 / total_phases as f64)
};
let location = specific_file.map_or_else(
|| project_path.display().to_string(),
|p| p.display().to_string(),
);
let mut violations: Vec<QualityViolation> = outcomes
.iter()
.filter_map(|(kind, o)| {
o.unmeasured.as_ref().map(|reason| QualityViolation {
violation_type: "not_measured".to_string(),
severity: "error".to_string(),
location: location.clone(),
current: 0.0,
target: 0.0,
suggestion: format!(
"{kind} could not be measured ({reason}); this verdict does not cover it"
),
})
})
.collect();
for (_, outcome) in outcomes {
violations.extend(outcome.violations);
}
Ok(QualityAssessment {
violations,
score,
measured_phases,
total_phases,
files_examined,
})
}
pub(super) fn violation_is_included(
filter: &crate::utils::file_filter::FileFilter,
project_path: &Path,
violation: &QualityViolation,
) -> bool {
let raw = violation
.location
.split(':')
.next()
.unwrap_or(&violation.location);
let path = Path::new(raw);
filter.should_include(path.strip_prefix(project_path).unwrap_or(path))
}