use crate::analysis::FileContext;
use crate::config::DebtmapConfig;
use crate::core::{AnalysisResults, DuplicationBlock, FileMetrics, FunctionMetrics, Language};
use crate::extraction::{ExtractedFileData, UnifiedFileExtractor};
use crate::formatting::FormattingConfig;
use crate::io;
use crate::progress::ProgressManager;
use crate::tui::app::StageStatus;
use crate::utils::{analysis_helpers, language_parser};
use crate::{analysis_utils, core::DebtItem};
use anyhow::{Context, Result};
use chrono::Utc;
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::time_span;
use super::config::AnalyzeConfig;
pub struct ProjectAnalysisOutput {
pub results: AnalysisResults,
pub extracted_data: Option<HashMap<PathBuf, ExtractedFileData>>,
pub file_outcomes: FileOutcomeSummary,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FileOutcomeSummary {
pub discovered: usize,
pub analyzed: usize,
pub failed: usize,
pub omitted_by_limit: usize,
pub production_loc: usize,
pub test_loc: usize,
pub production_functions: usize,
pub test_functions: usize,
}
#[allow(dead_code)] pub fn run_analysis(config: &AnalyzeConfig) -> Result<AnalysisResults> {
run_analysis_with_extraction(config).map(|output| output.results)
}
pub fn run_analysis_with_extraction(config: &AnalyzeConfig) -> Result<ProjectAnalysisOutput> {
let languages = language_parser::parse_languages(config.languages.clone());
analyze_project_with_extraction(
config.path.clone(),
languages,
config.threshold_complexity,
config.threshold_duplication,
config.parallel,
config._formatting_config,
)
}
pub fn analyze_project(
path: PathBuf,
languages: Vec<Language>,
complexity_threshold: u32,
duplication_threshold: usize,
parallel_enabled: bool,
formatting_config: FormattingConfig,
) -> Result<AnalysisResults> {
analyze_project_with_extraction(
path,
languages,
complexity_threshold,
duplication_threshold,
parallel_enabled,
formatting_config,
)
.map(|output| output.results)
}
pub fn analyze_project_with_extraction(
path: PathBuf,
languages: Vec<Language>,
complexity_threshold: u32,
duplication_threshold: usize,
parallel_enabled: bool,
formatting_config: FormattingConfig,
) -> Result<ProjectAnalysisOutput> {
time_span!("analyze_project");
let config = crate::config::get_config();
init_global_progress();
start_files_phase();
let discovered_files = discover_files(&path, &languages, config)?;
let discovered_count = discovered_files.len();
let (files, omitted_by_limit) = apply_configured_file_limit(discovered_files);
let (file_metrics, extracted_data, file_outcomes) = parse_and_extract_metrics_hybrid(
&files,
discovered_count,
omitted_by_limit,
parallel_enabled,
formatting_config,
)?;
let (all_functions, all_debt_items, file_contexts) = extract_analysis_data(&file_metrics);
let duplications = detect_duplications(&files, duplication_threshold);
complete_files_phase(files.len());
let results = build_analysis_results(
path,
all_functions,
all_debt_items,
duplications,
file_contexts,
complexity_threshold,
&file_metrics,
);
Ok(ProjectAnalysisOutput {
results,
extracted_data,
file_outcomes,
})
}
fn init_global_progress() {
let quiet_mode = std::env::var("DEBTMAP_QUIET").is_ok();
if !quiet_mode {
io::progress::AnalysisProgress::init_global();
}
}
fn start_files_phase() {
io::progress::AnalysisProgress::with_global(|p| p.start_phase(0));
if let Some(manager) = ProgressManager::global() {
manager.tui_start_stage(0);
manager.tui_update_subtask(0, 0, StageStatus::Active, None);
}
}
fn discover_files(
path: &Path,
languages: &[Language],
config: &DebtmapConfig,
) -> Result<Vec<PathBuf>> {
time_span!("file_discovery", parent: "analyze_project");
let files = io::walker::find_project_files_with_config(path, languages.to_vec(), config)
.context("Failed to find project files")?;
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(0, 0, StageStatus::Completed, None);
std::thread::sleep(std::time::Duration::from_millis(150));
manager.tui_update_subtask(0, 1, StageStatus::Active, None);
}
Ok(files)
}
#[allow(dead_code)]
fn parse_and_extract_metrics(
files: &[PathBuf],
parallel_enabled: bool,
formatting_config: FormattingConfig,
) -> Result<Vec<FileMetrics>> {
update_file_count(files.len());
configure_project_size(files, parallel_enabled, formatting_config)?;
let file_metrics = analysis_utils::collect_file_metrics(files);
complete_parsing(files.len());
Ok(file_metrics)
}
type HybridMetricsResult = (
Vec<FileMetrics>,
Option<HashMap<PathBuf, ExtractedFileData>>,
FileOutcomeSummary,
);
fn parse_and_extract_metrics_hybrid(
files: &[PathBuf],
discovered_count: usize,
omitted_by_limit: usize,
parallel_enabled: bool,
formatting_config: FormattingConfig,
) -> Result<HybridMetricsResult> {
time_span!("parsing", parent: "analyze_project");
update_file_count(files.len());
configure_project_size(files, parallel_enabled, formatting_config)?;
let (extractable_files, legacy_files): (Vec<PathBuf>, Vec<PathBuf>) =
files.iter().cloned().partition(|path| {
matches!(
crate::core::Language::from_path(path),
crate::core::Language::Rust | crate::core::Language::Python
)
});
let (extracted_metrics, extracted_data, extraction_failures) = if !extractable_files.is_empty()
{
let extracted = extract_all_files_with_outcomes(&extractable_files);
let metrics =
crate::extraction::adapters::metrics::all_file_metrics_from_extracted(&extracted.data);
(metrics, Some(extracted.data), extracted.failed)
} else {
(vec![], None, 0)
};
let legacy_outcomes = if !legacy_files.is_empty() {
analysis_utils::collect_file_metrics_with_errors(&legacy_files)
} else {
crate::errors::AnalysisResults::new(vec![], vec![])
};
let legacy_failures = legacy_outcomes.failure_count();
let policy = crate::config::AnalysisPolicy::from_config(crate::config::get_config());
let mut all_metrics: Vec<_> = extracted_metrics
.into_iter()
.chain(legacy_outcomes.successes)
.filter_map(|metrics| match std::fs::read_to_string(&metrics.path) {
Ok(source) => policy.apply_file_metrics(metrics, &source),
Err(_) => Some(policy.filter_file_metrics(metrics)),
})
.collect();
let failed = extraction_failures + legacy_failures;
all_metrics.sort_by(|a, b| a.path.cmp(&b.path));
complete_parsing(files.len());
let code = summarize_code_breakdown(&all_metrics);
let outcomes = FileOutcomeSummary {
discovered: discovered_count,
analyzed: all_metrics.len(),
failed,
omitted_by_limit,
production_loc: code.0,
test_loc: code.1,
production_functions: code.2,
test_functions: code.3,
};
Ok((all_metrics, extracted_data, outcomes))
}
fn summarize_code_breakdown(metrics: &[FileMetrics]) -> (usize, usize, usize, usize) {
metrics.iter().fold((0, 0, 0, 0), |summary, file| {
(
summary.0 + file.production_lines(),
summary.1 + file.test_lines,
summary.2 + file.production_function_count(),
summary.3 + file.test_function_count(),
)
})
}
fn apply_configured_file_limit(files: Vec<PathBuf>) -> (Vec<PathBuf>, usize) {
let limit = std::env::var("DEBTMAP_MAX_FILES")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|limit| *limit > 0);
limit_files(files, limit)
}
fn limit_files(files: Vec<PathBuf>, limit: Option<usize>) -> (Vec<PathBuf>, usize) {
let retained = limit.unwrap_or(files.len()).min(files.len());
let omitted = files.len() - retained;
(files.into_iter().take(retained).collect(), omitted)
}
fn update_file_count(count: usize) {
io::progress::AnalysisProgress::with_global(|p| {
p.update_progress(io::progress::PhaseProgress::Count(count));
});
}
fn configure_project_size(
files: &[PathBuf],
parallel_enabled: bool,
_formatting_config: FormattingConfig,
) -> Result<()> {
let file_count = files.len();
let quiet_mode = std::env::var("DEBTMAP_QUIET").is_ok();
if !quiet_mode {
log_project_size_info(file_count, parallel_enabled);
configure_large_project_env(file_count);
}
Ok(())
}
fn log_project_size_info(file_count: usize, parallel_enabled: bool) {
match file_count {
0..=100 => log::info!("Analyzing {} files (small project)", file_count),
101..=500 => {
log::info!("Analyzing {} files (medium project)", file_count);
log_parallel_status(parallel_enabled);
}
501..=1000 => log::info!("Analyzing {} files (large project)", file_count),
1001..=2000 => log::info!("Analyzing {} files (very large project)", file_count),
_ => log_massive_project(file_count),
}
}
fn log_parallel_status(parallel_enabled: bool) {
if parallel_enabled {
log::info!("Parallel processing enabled for better performance");
} else {
log::warn!("Using sequential processing (use default for better performance)");
}
}
fn log_massive_project(file_count: usize) {
log::info!("Analyzing {} files (massive project)", file_count);
}
fn configure_large_project_env(file_count: usize) {
if file_count > 500 {
unsafe { std::env::set_var("RUST_BACKTRACE", "0") };
}
}
fn complete_parsing(file_count: usize) {
io::progress::AnalysisProgress::with_global(|p| {
p.update_progress(io::progress::PhaseProgress::Progress {
current: file_count,
total: file_count,
});
p.complete_phase();
});
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(0, 1, StageStatus::Completed, None);
std::thread::sleep(std::time::Duration::from_millis(150));
manager.tui_update_subtask(0, 2, StageStatus::Active, None);
}
}
fn extract_analysis_data(
file_metrics: &[FileMetrics],
) -> (
Vec<FunctionMetrics>,
Vec<DebtItem>,
HashMap<PathBuf, FileContext>,
) {
let all_functions = analysis_utils::extract_all_functions(file_metrics);
let all_debt_items = analysis_utils::extract_all_debt_items(file_metrics);
let file_contexts = analysis_utils::extract_file_contexts(file_metrics);
if let Some(manager) = ProgressManager::global() {
manager.tui_update_counts(all_functions.len(), all_debt_items.len());
manager.tui_update_subtask(0, 2, StageStatus::Completed, None);
std::thread::sleep(std::time::Duration::from_millis(150));
manager.tui_update_subtask(0, 3, StageStatus::Active, Some((0, 0)));
}
(all_functions, all_debt_items, file_contexts)
}
fn detect_duplications(files: &[PathBuf], threshold: usize) -> Vec<DuplicationBlock> {
time_span!("duplication_detection", parent: "analyze_project");
let file_count = files.len();
let policy = crate::config::AnalysisPolicy::from_config(crate::config::get_config());
let duplications = analysis_helpers::detect_duplications_with_policy_and_progress(
files,
threshold,
&policy,
|current, total| {
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(0, 3, StageStatus::Active, Some((current, total)));
}
},
);
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(0, 3, StageStatus::Completed, Some((file_count, file_count)));
}
duplications
}
fn complete_files_phase(file_count: usize) {
if let Some(manager) = ProgressManager::global() {
manager.tui_complete_stage(0, format!("{} files parsed", file_count));
manager.tui_set_progress(1.0 / 6.0);
}
}
fn build_analysis_results(
path: PathBuf,
all_functions: Vec<FunctionMetrics>,
all_debt_items: Vec<DebtItem>,
duplications: Vec<DuplicationBlock>,
file_contexts: HashMap<PathBuf, FileContext>,
complexity_threshold: u32,
file_metrics: &[FileMetrics],
) -> AnalysisResults {
let complexity_report =
analysis_helpers::build_complexity_report(&all_functions, complexity_threshold);
let suppression = analysis_utils::apply_debt_suppressions(all_debt_items);
let technical_debt = analysis_utils::build_technical_debt_report_with_audit(
suppression.emitted,
duplications.clone(),
suppression.audit,
);
let dependencies = analysis_helpers::create_dependency_report(file_metrics);
AnalysisResults {
project_path: path,
timestamp: Utc::now(),
complexity: complexity_report,
technical_debt,
dependencies,
duplications,
file_contexts,
}
}
const EXTRACTION_BATCH_SIZE: usize = 200;
fn filter_extractable_files(files: &[PathBuf]) -> Vec<PathBuf> {
files
.iter()
.filter(|path| {
matches!(
crate::core::Language::from_path(path),
crate::core::Language::Rust | crate::core::Language::Python
)
})
.cloned()
.collect()
}
fn read_file_contents(paths: &[PathBuf]) -> Vec<(PathBuf, String)> {
paths
.par_iter()
.filter_map(|path| {
std::fs::read_to_string(path)
.ok()
.map(|content| (path.clone(), content))
})
.collect()
}
fn collect_extraction_results(
results: Vec<(PathBuf, anyhow::Result<ExtractedFileData>)>,
) -> HashMap<PathBuf, ExtractedFileData> {
results
.into_iter()
.filter_map(|(path, result)| match result {
Ok(data) => Some((path, data)),
Err(e) => {
log::warn!("Failed to extract {}: {}", path.display(), e);
None
}
})
.collect()
}
struct ExtractionOutcome {
data: HashMap<PathBuf, ExtractedFileData>,
failed: usize,
}
fn extract_all_files_with_outcomes(files: &[PathBuf]) -> ExtractionOutcome {
let extractable_files = filter_extractable_files(files);
if extractable_files.is_empty() {
return ExtractionOutcome {
data: HashMap::new(),
failed: 0,
};
}
let contents = read_file_contents(&extractable_files);
let results = UnifiedFileExtractor::extract_batch(&contents, EXTRACTION_BATCH_SIZE);
let extraction_failures = results.iter().filter(|(_, result)| result.is_err()).count();
ExtractionOutcome {
data: collect_extraction_results(results),
failed: extractable_files.len() - contents.len() + extraction_failures,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_limit_reports_omitted_files() {
let files = vec![
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
PathBuf::from("c.rs"),
];
let (retained, omitted) = limit_files(files, Some(2));
assert_eq!(retained, vec![PathBuf::from("a.rs"), PathBuf::from("b.rs")]);
assert_eq!(omitted, 1);
}
#[test]
fn extraction_outcome_counts_read_failures() {
let outcome = extract_all_files_with_outcomes(&[PathBuf::from("missing.rs")]);
assert!(outcome.data.is_empty());
assert_eq!(outcome.failed, 1);
}
#[test]
fn filter_extractable_files_includes_rust_and_python() {
let files = vec![
PathBuf::from("src/main.rs"),
PathBuf::from("src/lib.py"),
PathBuf::from("README.md"),
PathBuf::from("src/utils.rs"),
];
let result = filter_extractable_files(&files);
assert_eq!(result.len(), 3);
assert!(result.contains(&PathBuf::from("src/main.rs")));
assert!(result.contains(&PathBuf::from("src/lib.py")));
assert!(result.contains(&PathBuf::from("src/utils.rs")));
}
#[test]
fn filter_extractable_files_handles_empty_input() {
let files: Vec<PathBuf> = vec![];
let result = filter_extractable_files(&files);
assert!(result.is_empty());
}
#[test]
fn filter_extractable_files_handles_unsupported_files() {
let files = vec![
PathBuf::from("src/main.ts"),
PathBuf::from("README.md"),
PathBuf::from("Cargo.toml"),
];
let result = filter_extractable_files(&files);
assert!(result.is_empty());
}
#[test]
fn filter_extractable_files_handles_files_without_extension() {
let files = vec![
PathBuf::from("Makefile"),
PathBuf::from("src/main.rs"),
PathBuf::from(".gitignore"),
];
let result = filter_extractable_files(&files);
assert_eq!(result.len(), 1);
assert_eq!(result[0], PathBuf::from("src/main.rs"));
}
#[test]
fn collect_extraction_results_filters_errors() {
let data = ExtractedFileData::empty(PathBuf::from("test.rs"));
let results = vec![
(PathBuf::from("good.rs"), Ok(data)),
(PathBuf::from("bad.rs"), Err(anyhow::anyhow!("parse error"))),
];
let collected = collect_extraction_results(results);
assert_eq!(collected.len(), 1);
assert!(collected.contains_key(&PathBuf::from("good.rs")));
assert!(!collected.contains_key(&PathBuf::from("bad.rs")));
}
#[test]
fn collect_extraction_results_handles_all_errors() {
let results = vec![
(
PathBuf::from("bad1.rs"),
Err(anyhow::anyhow!("parse error")),
),
(
PathBuf::from("bad2.rs"),
Err(anyhow::anyhow!("syntax error")),
),
];
let collected = collect_extraction_results(results);
assert!(collected.is_empty());
}
#[test]
fn collect_extraction_results_handles_empty_input() {
let results: Vec<(PathBuf, anyhow::Result<ExtractedFileData>)> = vec![];
let collected = collect_extraction_results(results);
assert!(collected.is_empty());
}
}