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>>,
}
#[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");
setup_parallel_env(parallel_enabled);
let config = crate::config::get_config();
init_global_progress();
start_files_phase();
let files = discover_files(&path, &languages, config)?;
let (file_metrics, extracted_data) =
parse_and_extract_metrics_hybrid(&files, 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,
})
}
fn setup_parallel_env(parallel_enabled: bool) {
if parallel_enabled {
std::env::set_var("DEBTMAP_PARALLEL", "true");
}
}
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>>,
);
fn parse_and_extract_metrics_hybrid(
files: &[PathBuf],
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 (rust_files, non_rust_files): (Vec<PathBuf>, Vec<PathBuf>) = files
.iter()
.cloned()
.partition(|p| p.extension().map(|e| e == "rs").unwrap_or(false));
let (rust_metrics, extracted_data) = if !rust_files.is_empty() {
let extracted = extract_all_files(&rust_files);
let metrics =
crate::extraction::adapters::metrics::all_file_metrics_from_extracted(&extracted);
(metrics, Some(extracted))
} else {
(vec![], None)
};
let non_rust_metrics = if !non_rust_files.is_empty() {
analysis_utils::collect_file_metrics(&non_rust_files)
} else {
vec![]
};
let mut all_metrics = rust_metrics;
all_metrics.extend(non_rust_metrics);
all_metrics.sort_by(|a, b| a.path.cmp(&b.path));
complete_parsing(files.len());
Ok((all_metrics, extracted_data))
}
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 {
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 duplications =
analysis_helpers::detect_duplications_with_progress(files, threshold, |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 technical_debt =
analysis_helpers::build_technical_debt_report(all_debt_items, duplications.clone());
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_rust_files(files: &[PathBuf]) -> Vec<PathBuf> {
files
.iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
.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()
}
pub fn extract_all_files(files: &[PathBuf]) -> HashMap<PathBuf, ExtractedFileData> {
let rust_files = filter_rust_files(files);
if rust_files.is_empty() {
return HashMap::new();
}
let contents = read_file_contents(&rust_files);
let results = UnifiedFileExtractor::extract_batch(&contents, EXTRACTION_BATCH_SIZE);
collect_extraction_results(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_rust_files_includes_only_rs_extension() {
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_rust_files(&files);
assert_eq!(result.len(), 2);
assert!(result.contains(&PathBuf::from("src/main.rs")));
assert!(result.contains(&PathBuf::from("src/utils.rs")));
}
#[test]
fn filter_rust_files_handles_empty_input() {
let files: Vec<PathBuf> = vec![];
let result = filter_rust_files(&files);
assert!(result.is_empty());
}
#[test]
fn filter_rust_files_handles_no_rust_files() {
let files = vec![
PathBuf::from("src/main.py"),
PathBuf::from("README.md"),
PathBuf::from("Cargo.toml"),
];
let result = filter_rust_files(&files);
assert!(result.is_empty());
}
#[test]
fn filter_rust_files_handles_files_without_extension() {
let files = vec![
PathBuf::from("Makefile"),
PathBuf::from("src/main.rs"),
PathBuf::from(".gitignore"),
];
let result = filter_rust_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());
}
}