use crate::cli::ComprehensiveOutputFormat;
use crate::services::defect_report_service::{DefectReportService, ReportFormat};
use anyhow::{Context, Result};
use serde_json;
use std::path::{Path, PathBuf};
use std::time::Instant;
use tracing::{info, warn};
pub struct ComprehensiveConfig {
pub project_path: PathBuf,
pub file: Option<PathBuf>,
pub files: Vec<PathBuf>,
pub format: ComprehensiveOutputFormat,
pub include_duplicates: bool,
pub include_dead_code: bool,
pub include_defects: bool,
pub include_complexity: bool,
pub include_tdg: bool,
pub confidence_threshold: f32,
pub min_lines: usize,
pub include: Option<String>,
pub exclude: Option<String>,
pub output: Option<PathBuf>,
pub perf: bool,
pub executive_summary: bool,
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_analyze_comprehensive(
project_path: PathBuf,
file: Option<PathBuf>,
files: Vec<PathBuf>,
format: ComprehensiveOutputFormat,
include_duplicates: bool,
include_dead_code: bool,
include_defects: bool,
include_complexity: bool,
include_tdg: bool,
confidence_threshold: f32,
min_lines: usize,
include: Option<String>,
exclude: Option<String>,
output: Option<PathBuf>,
perf: bool,
executive_summary: bool,
) -> Result<()> {
let config = ComprehensiveConfig {
project_path,
file,
files,
format,
include_duplicates,
include_dead_code,
include_defects,
include_complexity,
include_tdg,
confidence_threshold,
min_lines,
include,
exclude,
output,
perf,
executive_summary,
};
handle_analyze_comprehensive_with_config(config).await
}
async fn handle_analyze_comprehensive_with_config(config: ComprehensiveConfig) -> Result<()> {
let start_time = Instant::now();
info!("🔍 Starting comprehensive analysis");
let (analysis_path, single_file_mode, target_files) = determine_analysis_mode(&config)?;
let enabled_analyses = get_enabled_analyses(&config);
info!("📊 Enabled analyses: {}", enabled_analyses.join(", "));
let service = DefectReportService::new();
let report = service.generate_report(&analysis_path).await?;
let filtered_defects = filter_defects(
&report.defects,
single_file_mode,
&target_files,
config.confidence_threshold,
);
info!("📈 Total defects found: {}", report.defects.len());
info!(
"📉 After confidence filter (>={:.0}%): {}",
config.confidence_threshold * 100.0,
filtered_defects.len()
);
let formatted_output = format_report(&service, &report, filtered_defects, &config)?;
write_output(&config.output, &formatted_output).await?;
let elapsed = start_time.elapsed();
if config.perf {
print_performance_metrics(elapsed, &report);
}
info!("\n📊 Defects by Category:");
for (category, count) in &report.summary.by_category {
info!(" {}: {}", category, count);
}
warn_ignored_parameters(&config);
Ok(())
}
fn determine_analysis_mode(config: &ComprehensiveConfig) -> Result<(PathBuf, bool, Vec<PathBuf>)> {
let analysis_path = if let Some(ref file) = config.file {
find_project_root(file)?
} else {
config.project_path.clone()
};
let single_file_mode = config.file.is_some();
let target_files = if !config.files.is_empty() {
config.files.clone()
} else if let Some(ref file) = config.file {
vec![file.clone()]
} else {
vec![]
};
Ok((analysis_path, single_file_mode, target_files))
}
fn get_enabled_analyses(config: &ComprehensiveConfig) -> Vec<String> {
let mut analyses = Vec::new();
if config.include_complexity {
analyses.push("Complexity".to_string());
}
if config.include_tdg {
analyses.push("TDG".to_string());
}
if config.include_defects {
analyses.push("Defects".to_string());
}
if config.include_dead_code {
analyses.push("Dead Code".to_string());
}
if config.include_duplicates {
analyses.push("Duplicates".to_string());
}
analyses
}
fn filter_defects(
defects: &[crate::models::defect_report::Defect],
single_file_mode: bool,
target_files: &[PathBuf],
confidence_threshold: f32,
) -> Vec<crate::models::defect_report::Defect> {
defects
.iter()
.filter(|defect| {
let confidence = defect.metrics.get("confidence").copied().unwrap_or(1.0) as f32;
if confidence < confidence_threshold {
return false;
}
if single_file_mode && !target_files.is_empty() {
return target_files.contains(&defect.file_path);
}
true
})
.cloned()
.collect()
}
fn format_report(
service: &DefectReportService,
report: &crate::models::defect_report::DefectReport,
filtered_defects: Vec<crate::models::defect_report::Defect>,
config: &ComprehensiveConfig,
) -> Result<String> {
let format = match config.format {
ComprehensiveOutputFormat::Json => ReportFormat::Json,
ComprehensiveOutputFormat::Summary => ReportFormat::Markdown,
ComprehensiveOutputFormat::Detailed => ReportFormat::Markdown,
ComprehensiveOutputFormat::Markdown => ReportFormat::Markdown,
ComprehensiveOutputFormat::Sarif => ReportFormat::Json, };
let mut filtered_report = report.clone();
filtered_report.defects = filtered_defects;
match format {
ReportFormat::Json => serde_json::to_string_pretty(&filtered_report)
.context("Failed to serialize report to JSON"),
ReportFormat::Markdown | ReportFormat::Text => service
.format_text(&filtered_report)
.context("Failed to format report as text"),
ReportFormat::Csv => {
serde_json::to_string_pretty(&filtered_report).context("Failed to serialize report")
}
}
}
async fn write_output(output: &Option<PathBuf>, content: &str) -> Result<()> {
if let Some(output_path) = output {
tokio::fs::write(output_path, content)
.await
.context("Failed to write output file")?;
info!("📝 Report written to: {}", output_path.display());
} else {
println!("{content}");
}
Ok(())
}
fn print_performance_metrics(
elapsed: std::time::Duration,
report: &crate::models::defect_report::DefectReport,
) {
info!("\n⏱️ Performance Metrics:");
info!(" Total time: {:.2}s", elapsed.as_secs_f64());
info!(" Hotspot files: {}", report.summary.hotspot_files.len());
info!(" Defects found: {}", report.summary.total_defects);
let defects_per_second = report.summary.total_defects as f64 / elapsed.as_secs_f64();
info!(" Defects/second: {:.2}", defects_per_second);
}
fn warn_ignored_parameters(config: &ComprehensiveConfig) {
if config.min_lines > 0 {
warn!("Note: min_lines parameter is currently handled by the DefectReportService");
}
if config.include.is_some() || config.exclude.is_some() {
warn!("Note: include/exclude patterns are currently handled by the DefectReportService");
}
}
fn find_project_root(start_path: &Path) -> Result<PathBuf> {
let mut current = if start_path.is_file() {
start_path
.parent()
.context("File has no parent directory")?
} else {
start_path
};
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
return Ok(current.to_path_buf());
}
match current.parent() {
Some(parent) => current = parent,
None => break,
}
}
Ok(start_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comprehensive_handler_params() {
assert_eq!(
ComprehensiveOutputFormat::Json as i32,
ComprehensiveOutputFormat::Json as i32
);
}
#[test]
fn test_find_project_root() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let src_dir = project_root.join("src");
let sub_dir = src_dir.join("module");
fs::create_dir_all(&sub_dir).unwrap();
fs::write(
project_root.join("Cargo.toml"),
"[package]\nname = \"test\"",
)
.unwrap();
let test_file = sub_dir.join("test.rs");
fs::write(&test_file, "// test file").unwrap();
let found_root = find_project_root(&test_file).unwrap();
assert_eq!(found_root, project_root);
let found_root = find_project_root(&sub_dir).unwrap();
assert_eq!(found_root, project_root);
let isolated_dir = TempDir::new().unwrap();
let isolated_file = isolated_dir.path().join("isolated.rs");
fs::write(&isolated_file, "// isolated file").unwrap();
let found_root = find_project_root(&isolated_file).unwrap();
assert_eq!(found_root, isolated_dir.path());
}
#[tokio::test]
async fn test_comprehensive_single_file_filter() {
use crate::models::defect_report::{Defect, DefectCategory, Severity};
use std::collections::HashMap;
let defects = vec![
Defect {
id: "1".to_string(),
category: DefectCategory::Complexity,
severity: Severity::High,
file_path: PathBuf::from("src/main.rs"),
line_start: 10,
line_end: Some(20),
column_start: Some(5),
column_end: Some(10),
message: "High complexity in main".to_string(),
rule_id: "complexity".to_string(),
fix_suggestion: Some("Refactor".to_string()),
metrics: HashMap::from([("confidence".to_string(), 0.8)]),
},
Defect {
id: "2".to_string(),
category: DefectCategory::Complexity,
severity: Severity::Medium,
file_path: PathBuf::from("src/lib.rs"),
line_start: 15,
line_end: Some(25),
column_start: Some(3),
column_end: Some(8),
message: "Medium complexity in lib".to_string(),
rule_id: "complexity".to_string(),
fix_suggestion: Some("Consider refactoring".to_string()),
metrics: HashMap::from([("confidence".to_string(), 0.7)]),
},
];
let target_file = Some(PathBuf::from("src/main.rs"));
let filtered: Vec<_> = defects
.iter()
.filter(|d| {
if let Some(ref tf) = target_file {
d.file_path == *tf
} else {
true
}
})
.collect();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].id, "1");
assert_eq!(filtered[0].file_path, PathBuf::from("src/main.rs"));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}