use crate::{
analysis_utils, config,
core::{
AnalysisResults, ComplexityReport, DependencyReport, DuplicationBlock, FileMetrics,
FunctionMetrics, Language, TechnicalDebtReport,
},
debt, io,
};
use anyhow::{Context, Result};
use chrono::Utc;
use rayon::prelude::*;
use std::path::{Path, PathBuf};
const DEFAULT_SIMILARITY_THRESHOLD: f64 = 0.8;
pub fn analyze_project(
path: PathBuf,
languages: Vec<Language>,
complexity_threshold: u32,
duplication_threshold: usize,
) -> Result<AnalysisResults> {
let config = config::get_config();
let files = io::walker::find_project_files_with_config(&path, languages.clone(), config)
.context("Failed to find project files")?;
let file_metrics = analysis_utils::collect_file_metrics(&files);
let all_functions = analysis_utils::extract_all_functions(&file_metrics);
let all_debt_items = analysis_utils::extract_all_debt_items(&file_metrics);
let duplications = detect_duplications(&files, duplication_threshold);
let file_contexts = analysis_utils::extract_file_contexts(&file_metrics);
let complexity_report = build_complexity_report(&all_functions, complexity_threshold);
let technical_debt = build_technical_debt_report(all_debt_items, duplications.clone());
let dependencies = create_dependency_report(&file_metrics);
Ok(AnalysisResults {
project_path: path,
timestamp: Utc::now(),
complexity: complexity_report,
technical_debt,
dependencies,
duplications,
file_contexts,
})
}
pub fn detect_duplications_with_progress<F>(
files: &[PathBuf],
threshold: usize,
mut progress_callback: F,
) -> Vec<DuplicationBlock>
where
F: FnMut(usize, usize),
{
let total_files = files.len();
let files_with_content: Vec<(PathBuf, String)> = files
.par_iter()
.filter_map(|path| match io::read_file(path) {
Ok(content) => Some((path.clone(), content)),
Err(e) => {
log::debug!(
"Skipping file {} for duplication check: {}",
path.display(),
e
);
None
}
})
.collect();
progress_callback(total_files, total_files);
debt::duplication::detect_duplication(
files_with_content,
threshold,
DEFAULT_SIMILARITY_THRESHOLD,
)
}
pub fn detect_duplications(files: &[PathBuf], threshold: usize) -> Vec<DuplicationBlock> {
detect_duplications_with_progress(files, threshold, |_, _| {})
}
pub fn prepare_files_for_duplication_check(files: &[PathBuf]) -> Vec<(PathBuf, String)> {
files
.par_iter()
.filter_map(|path| match io::read_file(path) {
Ok(content) => Some((path.clone(), content)),
Err(e) => {
log::debug!(
"Skipping file {} for duplication check: {}",
path.display(),
e
);
None
}
})
.collect()
}
pub fn build_complexity_report(
all_functions: &[FunctionMetrics],
complexity_threshold: u32,
) -> ComplexityReport {
analysis_utils::build_complexity_report(all_functions, complexity_threshold)
}
pub fn build_technical_debt_report(
all_debt_items: Vec<crate::core::DebtItem>,
duplications: Vec<DuplicationBlock>,
) -> TechnicalDebtReport {
analysis_utils::build_technical_debt_report(all_debt_items, duplications)
}
pub fn create_dependency_report(file_metrics: &[FileMetrics]) -> DependencyReport {
analysis_utils::create_dependency_report(file_metrics)
}
pub fn is_in_current_project(path: &Path) -> bool {
if path.starts_with("..") {
return false;
}
if path.is_absolute() {
if let Ok(current_dir) = std::env::current_dir() {
if let (Ok(canonical_path), Ok(canonical_cwd)) =
(path.canonicalize(), current_dir.canonicalize())
{
return canonical_path.starts_with(canonical_cwd);
}
}
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_in_current_project_relative_path() {
let path = Path::new("src/lib.rs");
assert!(is_in_current_project(path));
}
#[test]
fn test_is_in_current_project_parent_directory() {
let path = Path::new("../other-project/file.rs");
assert!(!is_in_current_project(path));
}
#[test]
fn test_is_in_current_project_nested_parent() {
let path = Path::new("../../another-project/src/file.rs");
assert!(!is_in_current_project(path));
}
#[test]
fn test_is_in_current_project_nested_relative() {
let path = Path::new("src/subdir/nested/file.rs");
assert!(is_in_current_project(path));
}
#[test]
fn test_detect_duplications_with_progress_calls_callback() {
use std::sync::{Arc, Mutex};
let temp_dir = tempfile::tempdir().unwrap();
let file1 = temp_dir.path().join("file1.rs");
let file2 = temp_dir.path().join("file2.rs");
let file3 = temp_dir.path().join("file3.rs");
std::fs::write(&file1, "fn test() { println!(\"hello\"); }").unwrap();
std::fs::write(&file2, "fn test() { println!(\"world\"); }").unwrap();
std::fs::write(&file3, "fn test() { println!(\"foo\"); }").unwrap();
let files = vec![file1, file2, file3];
let progress_calls = Arc::new(Mutex::new(Vec::new()));
let progress_calls_clone = progress_calls.clone();
detect_duplications_with_progress(&files, 50, |current, total| {
progress_calls_clone.lock().unwrap().push((current, total));
});
let calls = progress_calls.lock().unwrap();
assert!(!calls.is_empty(), "Progress callback should be called");
let final_call = calls.last().unwrap();
assert_eq!(final_call.0, 3, "Final current should be total files");
assert_eq!(final_call.1, 3, "Final total should be total files");
}
#[test]
fn test_detect_duplications_with_progress_parallel() {
use std::sync::{Arc, Mutex};
let temp_dir = tempfile::tempdir().unwrap();
let mut files = Vec::new();
for i in 0..25 {
let file = temp_dir.path().join(format!("file{}.rs", i));
std::fs::write(&file, format!("fn test{}() {{}}", i)).unwrap();
files.push(file);
}
let progress_calls = Arc::new(Mutex::new(Vec::new()));
let progress_calls_clone = progress_calls.clone();
detect_duplications_with_progress(&files, 50, |current, total| {
progress_calls_clone.lock().unwrap().push((current, total));
});
let calls = progress_calls.lock().unwrap();
assert_eq!(
calls.len(),
1,
"Parallel implementation reports once at completion"
);
let final_call = calls.last().unwrap();
assert_eq!(final_call.0, 25, "Should report all files complete");
assert_eq!(final_call.1, 25, "Total should be correct");
}
#[test]
fn test_detect_duplications_with_progress_correct_values() {
use std::sync::{Arc, Mutex};
let temp_dir = tempfile::tempdir().unwrap();
let mut files = Vec::new();
for i in 0..15 {
let file = temp_dir.path().join(format!("file{}.rs", i));
std::fs::write(&file, format!("fn test{}() {{}}", i)).unwrap();
files.push(file);
}
let total_files = files.len();
let progress_calls = Arc::new(Mutex::new(Vec::new()));
let progress_calls_clone = progress_calls.clone();
detect_duplications_with_progress(&files, 50, |current, total| {
progress_calls_clone.lock().unwrap().push((current, total));
});
let calls = progress_calls.lock().unwrap();
for (current, total) in calls.iter() {
assert_eq!(
*total, total_files,
"Total should always be {}",
total_files
);
assert!(*current <= total_files, "Current should never exceed total");
assert!(*current > 0, "Current should be positive");
}
}
#[test]
fn test_detect_duplications_without_progress_works() {
let temp_dir = tempfile::tempdir().unwrap();
let file1 = temp_dir.path().join("file1.rs");
let file2 = temp_dir.path().join("file2.rs");
std::fs::write(&file1, "fn test() { println!(\"hello\"); }").unwrap();
std::fs::write(&file2, "fn test() { println!(\"hello\"); }").unwrap();
let files = vec![file1, file2];
let _result = detect_duplications(&files, 50);
}
}