use crate::cli::{SatdOutputFormat, SatdSeverity};
use anyhow::Result;
use std::path::{Path, PathBuf};
use super::output::{format_satd_output, write_satd_output};
#[allow(clippy::too_many_arguments)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_analyze_satd(
path: PathBuf,
format: SatdOutputFormat,
severity: Option<SatdSeverity>,
critical_only: bool,
include_tests: bool,
strict: bool,
evolution: bool,
days: u32,
metrics: bool,
output: Option<PathBuf>,
top_files: usize,
fail_on_violation: bool,
timeout: u64,
) -> Result<()> {
print_satd_analysis_info(strict, timeout);
let mut result = run_satd_analysis(&path, include_tests, strict, timeout).await?;
apply_satd_filters(&mut result, severity, critical_only, top_files);
eprintln!(
"📊 Found {} SATD items in {} files",
result.items.len(),
result.files_with_debt
);
let content = format_satd_output(&result, format, metrics, evolution, days)?;
write_satd_output(content, output).await?;
check_satd_violations(&result, fail_on_violation)?;
Ok(())
}
fn print_satd_analysis_info(strict: bool, timeout: u64) {
eprintln!("🔍 Analyzing self-admitted technical debt...");
eprintln!("⏰ Analysis timeout set to {timeout} seconds");
if strict {
eprintln!("📝 Using strict mode (only explicit SATD markers)");
}
}
async fn run_satd_analysis(
path: &Path,
include_tests: bool,
strict: bool,
timeout: u64,
) -> Result<crate::services::satd_detector::SATDAnalysisResult> {
use crate::services::satd_detector::SATDDetector;
let detector = if strict {
SATDDetector::new_strict()
} else {
SATDDetector::new()
};
let timeout_duration = tokio::time::Duration::from_secs(timeout);
let result = tokio::time::timeout(timeout_duration, async {
detector.analyze_project(path, include_tests).await
})
.await
.map_err(|_| anyhow::anyhow!("SATD analysis timed out after {timeout} seconds"))??;
Ok(result)
}
fn apply_satd_filters(
result: &mut crate::services::satd_detector::SATDAnalysisResult,
severity: Option<SatdSeverity>,
critical_only: bool,
top_files: usize,
) {
use crate::services::satd_detector::Severity as DetectorSeverity;
if let Some(min_severity) = severity {
let min_detector_severity = match min_severity {
SatdSeverity::Critical => DetectorSeverity::Critical,
SatdSeverity::High => DetectorSeverity::High,
SatdSeverity::Medium => DetectorSeverity::Medium,
SatdSeverity::Low => DetectorSeverity::Low,
};
result
.items
.retain(|item| item.severity >= min_detector_severity);
}
if critical_only {
result
.items
.retain(|item| item.severity == DetectorSeverity::Critical);
}
if top_files > 0 {
filter_top_files(result, top_files);
}
}
fn check_satd_violations(
result: &crate::services::satd_detector::SATDAnalysisResult,
fail_on_violation: bool,
) -> Result<()> {
if fail_on_violation && !result.items.is_empty() {
eprintln!(
"\n❌ SATD violations found: {} technical debt items",
result.items.len()
);
std::process::exit(1);
}
Ok(())
}
fn filter_top_files(
result: &mut crate::services::satd_detector::SATDAnalysisResult,
top_files: usize,
) {
use std::collections::HashMap;
let mut file_counts: HashMap<std::path::PathBuf, usize> = HashMap::new();
for item in &result.items {
*file_counts.entry(item.file.clone()).or_insert(0) += 1;
}
let mut sorted_files: Vec<_> = file_counts.into_iter().collect();
sorted_files.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
let top_file_paths: std::collections::HashSet<_> = sorted_files
.into_iter()
.take(top_files)
.map(|(path, _)| path)
.collect();
result
.items
.retain(|item| top_file_paths.contains(&item.file));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::satd_detector::{
DebtCategory, SATDAnalysisResult, SATDSummary, Severity as DetectorSeverity, TechnicalDebt,
};
use chrono::Utc;
use std::collections::HashMap;
fn debt(file: &str, line: u32, severity: DetectorSeverity) -> TechnicalDebt {
TechnicalDebt {
category: DebtCategory::Requirement,
severity,
text: "TODO: thing".into(),
file: PathBuf::from(file),
line,
column: 0,
context_hash: [0u8; 16],
}
}
fn make_result(items: Vec<TechnicalDebt>) -> SATDAnalysisResult {
let files_with_debt = items
.iter()
.map(|i| &i.file)
.collect::<std::collections::HashSet<_>>()
.len();
SATDAnalysisResult {
total_files_analyzed: 100,
files_with_debt,
analysis_timestamp: Utc::now(),
summary: SATDSummary {
total_items: items.len(),
by_severity: HashMap::new(),
by_category: HashMap::new(),
files_with_satd: files_with_debt,
avg_age_days: 0.0,
},
items,
}
}
#[test]
fn test_print_satd_analysis_info_strict_no_panic() {
print_satd_analysis_info(true, 30);
print_satd_analysis_info(false, 60);
}
#[test]
fn test_apply_satd_filters_severity_critical_keeps_only_critical() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Critical),
debt("a.rs", 2, DetectorSeverity::High),
debt("a.rs", 3, DetectorSeverity::Low),
]);
apply_satd_filters(&mut r, Some(SatdSeverity::Critical), false, 0);
assert_eq!(r.items.len(), 1);
assert_eq!(r.items[0].severity, DetectorSeverity::Critical);
}
#[test]
fn test_apply_satd_filters_severity_high_keeps_high_and_critical() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Critical),
debt("a.rs", 2, DetectorSeverity::High),
debt("a.rs", 3, DetectorSeverity::Medium),
]);
apply_satd_filters(&mut r, Some(SatdSeverity::High), false, 0);
assert_eq!(r.items.len(), 2);
}
#[test]
fn test_apply_satd_filters_severity_low_keeps_all() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Critical),
debt("a.rs", 2, DetectorSeverity::Medium),
debt("a.rs", 3, DetectorSeverity::Low),
]);
apply_satd_filters(&mut r, Some(SatdSeverity::Low), false, 0);
assert_eq!(r.items.len(), 3);
}
#[test]
fn test_apply_satd_filters_severity_medium_keeps_med_high_critical() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Critical),
debt("a.rs", 2, DetectorSeverity::High),
debt("a.rs", 3, DetectorSeverity::Medium),
debt("a.rs", 4, DetectorSeverity::Low),
]);
apply_satd_filters(&mut r, Some(SatdSeverity::Medium), false, 0);
assert_eq!(r.items.len(), 3);
}
#[test]
fn test_apply_satd_filters_critical_only_drops_others() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Critical),
debt("a.rs", 2, DetectorSeverity::High),
]);
apply_satd_filters(&mut r, None, true, 0);
assert_eq!(r.items.len(), 1);
assert_eq!(r.items[0].severity, DetectorSeverity::Critical);
}
#[test]
fn test_apply_satd_filters_top_files_keeps_n_files() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Low),
debt("a.rs", 2, DetectorSeverity::Low),
debt("a.rs", 3, DetectorSeverity::Low),
debt("b.rs", 1, DetectorSeverity::Low),
debt("b.rs", 2, DetectorSeverity::Low),
debt("c.rs", 1, DetectorSeverity::Low),
]);
apply_satd_filters(&mut r, None, false, 2);
assert_eq!(r.items.len(), 5);
assert!(!r.items.iter().any(|i| i.file == Path::new("c.rs")));
}
#[test]
fn test_apply_satd_filters_top_files_zero_keeps_all() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Low),
debt("b.rs", 1, DetectorSeverity::Low),
debt("c.rs", 1, DetectorSeverity::Low),
]);
apply_satd_filters(&mut r, None, false, 0);
assert_eq!(r.items.len(), 3);
}
#[test]
fn test_filter_top_files_picks_highest_count() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Low),
debt("b.rs", 1, DetectorSeverity::Low),
debt("b.rs", 2, DetectorSeverity::Low),
debt("b.rs", 3, DetectorSeverity::Low),
]);
filter_top_files(&mut r, 1);
assert_eq!(r.items.len(), 3);
assert!(r.items.iter().all(|i| i.file == Path::new("b.rs")));
}
#[test]
fn test_filter_top_files_n_larger_than_files_keeps_all() {
let mut r = make_result(vec![
debt("a.rs", 1, DetectorSeverity::Low),
debt("b.rs", 1, DetectorSeverity::Low),
]);
filter_top_files(&mut r, 100);
assert_eq!(r.items.len(), 2);
}
#[test]
fn test_check_satd_violations_disabled_flag_returns_ok() {
let r = make_result(vec![debt("a.rs", 1, DetectorSeverity::High)]);
assert!(check_satd_violations(&r, false).is_ok());
}
#[test]
fn test_check_satd_violations_empty_items_returns_ok() {
let r = make_result(vec![]);
assert!(check_satd_violations(&r, true).is_ok());
}
}