use crate::{
analysis::ContextDetector,
analyzers::FileAnalyzer,
builders::unified_analysis_phases::phases::scoring::{
build_suppression_context_cache, SuppressionContextCache,
},
core::FunctionMetrics,
data_flow::DataFlowGraph,
extraction::ExtractedFileData,
priority::{
call_graph::{CallGraph, FunctionId},
debt_aggregator::{DebtAggregator, FunctionId as AggregatorFunctionId},
file_metrics::FileDebtItem,
scoring::ContextRecommendationEngine,
UnifiedAnalysis, UnifiedAnalysisUtils, UnifiedDebtItem,
},
progress::ProgressManager,
risk::lcov::LcovData,
};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use indicatif::ParallelProgressIterator;
use parking_lot::Mutex;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug_span, warn};
fn filter_suppressed_items(
items: Vec<UnifiedDebtItem>,
suppression_cache: &SuppressionContextCache,
) -> Vec<UnifiedDebtItem> {
items
.into_iter()
.filter(|item| {
if let Some(context) = suppression_cache.get(&item.location.file) {
!context.is_function_allowed(item.location.line, &item.debt_type)
} else {
true
}
})
.collect()
}
fn is_god_object_suppressed_parallel(
god_analysis: &crate::organization::GodObjectAnalysis,
file_content: &str,
file_path: &Path,
) -> bool {
use crate::core::Language;
use crate::debt::suppression::parse_suppression_comments;
use crate::organization::DetectionType;
use crate::priority::DebtType;
let language = file_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| match ext {
"rs" => Language::Rust,
"py" | "pyw" => Language::Python,
_ => Language::Rust,
})
.unwrap_or(Language::Rust);
let suppression_context = parse_suppression_comments(file_content, language, file_path);
let god_object_debt_type = DebtType::GodObject {
methods: god_analysis.method_count as u32,
fields: Some(god_analysis.field_count as u32),
responsibilities: god_analysis.responsibility_count as u32,
god_object_score: god_analysis.god_object_score,
lines: god_analysis.lines_of_code as u32,
};
for check_line in 1..=6 {
if suppression_context.is_suppressed(check_line, &god_object_debt_type) {
return true;
}
if suppression_context.is_function_allowed(check_line, &god_object_debt_type) {
return true;
}
}
if let DetectionType::GodClass = god_analysis.detection_type {
let struct_line = god_analysis.struct_line.unwrap_or(1);
if suppression_context.is_suppressed(struct_line, &god_object_debt_type) {
return true;
}
if suppression_context.is_function_allowed(struct_line, &god_object_debt_type) {
return true;
}
}
false
}
mod transformations {
use super::*;
pub fn create_function_mappings(
metrics: &[FunctionMetrics],
) -> Vec<(AggregatorFunctionId, usize, usize)> {
metrics
.iter()
.map(|m| {
let func_id = AggregatorFunctionId::new(m.file.clone(), m.name.clone(), m.line);
(func_id, m.line, m.line + m.length)
})
.collect()
}
pub fn metrics_to_purity_map(metrics: &[FunctionMetrics]) -> HashMap<String, bool> {
metrics
.iter()
.map(|m| (m.name.clone(), m.is_pure.unwrap_or(false)))
.collect()
}
}
mod predicates {
use super::*;
pub fn is_test_function(metric: &FunctionMetrics) -> bool {
metric.is_test || metric.in_test_module
}
pub fn is_closure(metric: &FunctionMetrics) -> bool {
metric.name.contains("<closure@")
}
pub fn is_trivial_function(metric: &FunctionMetrics, callee_count: usize) -> bool {
metric.cyclomatic == 1 && metric.cognitive == 0 && metric.length <= 3 && callee_count == 1
}
pub fn should_process_metric(
metric: &FunctionMetrics,
test_only_functions: &HashSet<FunctionId>,
callee_count: usize,
) -> bool {
if is_test_function(metric) || is_closure(metric) {
return false;
}
let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
if test_only_functions.contains(&func_id) {
return false;
}
!is_trivial_function(metric, callee_count)
}
}
mod file_analysis {
use super::*;
use crate::analyzers::file_analyzer::UnifiedFileAnalyzer;
use crate::priority::file_metrics::FileDebtMetrics;
pub fn aggregate_file_metrics(
functions: &[FunctionMetrics],
coverage_data: Option<&LcovData>,
) -> FileDebtMetrics {
let file_analyzer = UnifiedFileAnalyzer::new(coverage_data.cloned());
file_analyzer.aggregate_functions(functions)
}
pub fn analyze_god_object(
content: &str,
file_path: &Path,
coverage_data: Option<&LcovData>,
) -> Result<Option<crate::organization::GodObjectAnalysis>, String> {
let file_analyzer = UnifiedFileAnalyzer::new(coverage_data.cloned());
file_analyzer
.analyze_file(file_path, content)
.map(|analyzed| analyzed.god_object_analysis)
.map_err(|e| format!("Failed to analyze god object: {}", e))
}
pub fn should_include_file(score: f64) -> bool {
score > 50.0
}
}
#[derive(Debug, Clone)]
pub struct ParallelUnifiedAnalysisOptions {
pub parallel: bool,
pub jobs: Option<usize>,
pub batch_size: usize,
pub progress: bool,
pub reference_time: DateTime<Utc>,
}
impl Default for ParallelUnifiedAnalysisOptions {
fn default() -> Self {
Self {
parallel: true,
jobs: None,
batch_size: 100,
progress: true,
reference_time: Utc::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct AnalysisPhaseTimings {
pub call_graph_building: Duration,
pub trait_resolution: Duration,
pub coverage_loading: Duration,
pub data_flow_creation: Duration,
pub purity_analysis: Duration,
pub test_detection: Duration,
pub debt_aggregation: Duration,
pub function_analysis: Duration,
pub file_analysis: Duration,
pub aggregation: Duration,
pub sorting: Duration,
pub total: Duration,
}
impl Default for AnalysisPhaseTimings {
fn default() -> Self {
Self {
call_graph_building: Duration::from_secs(0),
trait_resolution: Duration::from_secs(0),
coverage_loading: Duration::from_secs(0),
data_flow_creation: Duration::from_secs(0),
purity_analysis: Duration::from_secs(0),
test_detection: Duration::from_secs(0),
debt_aggregation: Duration::from_secs(0),
function_analysis: Duration::from_secs(0),
file_analysis: Duration::from_secs(0),
aggregation: Duration::from_secs(0),
sorting: Duration::from_secs(0),
total: Duration::from_secs(0),
}
}
}
struct FunctionAnalysisContext<'a> {
call_graph: &'a CallGraph,
debt_aggregator: &'a DebtAggregator,
data_flow_graph: &'a DataFlowGraph,
coverage_data: Option<&'a LcovData>,
framework_exclusions: &'a HashSet<FunctionId>,
function_pointer_used_functions: Option<&'a HashSet<FunctionId>>,
risk_analyzer: Option<&'a crate::risk::RiskAnalyzer>,
project_path: &'a Path,
context_detector: &'a ContextDetector,
recommendation_engine: &'a ContextRecommendationEngine,
}
pub struct OptimizedTestDetector {
call_graph: Arc<CallGraph>,
test_roots: HashSet<FunctionId>,
reachability_cache: DashMap<FunctionId, bool>,
}
impl OptimizedTestDetector {
pub fn new(call_graph: Arc<CallGraph>) -> Self {
let test_roots = Self::find_test_roots(&call_graph);
Self {
call_graph,
test_roots,
reachability_cache: DashMap::new(),
}
}
fn find_test_roots(call_graph: &Arc<CallGraph>) -> HashSet<FunctionId> {
let mut test_roots = HashSet::new();
for func_id in call_graph.get_all_functions() {
let callers = call_graph.get_callers(func_id);
if callers.is_empty() {
if func_id.name.starts_with("test_")
|| func_id.name.contains("::test")
|| func_id.file.to_string_lossy().contains("/tests/")
|| func_id.file.to_string_lossy().contains("_test.rs")
{
test_roots.insert(func_id.clone());
}
}
}
test_roots
}
pub fn is_test_only(&self, func_id: &FunctionId) -> bool {
if let Some(result) = self.reachability_cache.get(func_id) {
return *result;
}
if self.test_roots.contains(func_id) {
self.reachability_cache.insert(func_id.clone(), true);
return true;
}
let callers = self.call_graph.get_callers(func_id);
if callers.is_empty() {
self.reachability_cache.insert(func_id.clone(), false);
return false;
}
let is_test_only = self.is_reachable_only_from_tests(func_id);
self.reachability_cache
.insert(func_id.clone(), is_test_only);
is_test_only
}
fn is_reachable_only_from_tests(&self, func_id: &FunctionId) -> bool {
let mut visited = HashSet::new();
let mut queue = vec![func_id.clone()];
while let Some(current) = queue.pop() {
if !visited.insert(current.clone()) {
continue;
}
let callers = self.call_graph.get_callers(¤t);
if callers.is_empty() {
if !self.test_roots.contains(¤t) {
return false;
}
} else {
for caller in callers {
if !visited.contains(&caller) {
queue.push(caller);
}
}
}
}
true
}
pub fn find_all_test_only_functions(&self) -> HashSet<FunctionId> {
let all_functions: Vec<FunctionId> = self.call_graph.get_all_functions().cloned().collect();
all_functions
.par_iter()
.filter(|func_id| self.is_test_only(func_id))
.cloned()
.collect()
}
}
pub struct ParallelUnifiedAnalysisBuilder {
call_graph: Arc<CallGraph>,
options: ParallelUnifiedAnalysisOptions,
timings: AnalysisPhaseTimings,
risk_analyzer: Option<crate::risk::RiskAnalyzer>,
project_path: PathBuf,
line_count_index: HashMap<PathBuf, usize>,
extracted_data: Option<Arc<HashMap<PathBuf, ExtractedFileData>>>,
}
impl ParallelUnifiedAnalysisBuilder {
pub fn new(call_graph: CallGraph, options: ParallelUnifiedAnalysisOptions) -> Self {
Self {
call_graph: Arc::new(call_graph),
options,
timings: AnalysisPhaseTimings::default(),
risk_analyzer: None,
project_path: PathBuf::from("."),
line_count_index: HashMap::new(),
extracted_data: None,
}
}
pub fn with_extracted_data(mut self, extracted: HashMap<PathBuf, ExtractedFileData>) -> Self {
self.extracted_data = Some(Arc::new(extracted));
self
}
pub fn with_line_count_index(mut self, index: HashMap<PathBuf, usize>) -> Self {
self.line_count_index = index;
self
}
pub fn build_line_count_index(
file_metrics: &[crate::core::FileMetrics],
) -> HashMap<PathBuf, usize> {
file_metrics
.iter()
.filter(|fm| fm.total_lines > 0)
.map(|fm| (fm.path.clone(), fm.total_lines))
.collect()
}
pub fn with_risk_analyzer(mut self, risk_analyzer: crate::risk::RiskAnalyzer) -> Self {
let analyzer = risk_analyzer.with_reference_time(self.options.reference_time);
self.risk_analyzer = Some(analyzer);
self
}
pub fn with_project_path(mut self, project_path: PathBuf) -> Self {
self.project_path = project_path;
self
}
pub fn set_preliminary_timings(
&mut self,
call_graph_building: Duration,
coverage_loading: Duration,
) {
self.timings.call_graph_building = call_graph_building;
self.timings.trait_resolution = Duration::from_secs(0);
self.timings.coverage_loading = coverage_loading;
}
pub fn execute_phase1_parallel(
&mut self,
metrics: &[FunctionMetrics],
debt_items: Option<&[crate::core::DebtItem]>,
) -> (
DataFlowGraph,
HashMap<String, bool>, // purity analysis
HashSet<FunctionId>, // test-only functions
DebtAggregator,
) {
let start = Instant::now();
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(5, 0, crate::tui::app::StageStatus::Active, None);
}
let (data_flow, purity, test_funcs, debt_agg) =
self.execute_phase1_tasks(metrics, debt_items);
let phase1_time = start.elapsed();
self.report_phase1_completion(phase1_time);
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(5, 0, crate::tui::app::StageStatus::Completed, None);
std::thread::sleep(std::time::Duration::from_millis(150));
}
(data_flow, purity, test_funcs, debt_agg)
}
fn execute_phase1_tasks(
&mut self,
metrics: &[FunctionMetrics],
debt_items: Option<&[crate::core::DebtItem]>,
) -> (
DataFlowGraph,
HashMap<String, bool>,
HashSet<FunctionId>,
DebtAggregator,
) {
let call_graph = Arc::clone(&self.call_graph);
let metrics_arc = Arc::new(metrics.to_vec());
let debt_items_opt = debt_items.map(|d| d.to_vec());
let data_flow_result = Arc::new(Mutex::new(None));
let purity_result = Arc::new(Mutex::new(None));
let test_funcs_result = Arc::new(Mutex::new(None));
let debt_agg_result = Arc::new(Mutex::new(None));
let timings = Arc::new(Mutex::new(self.timings.clone()));
let (df_progress, purity_progress, test_progress, debt_progress) = (
indicatif::ProgressBar::hidden(),
indicatif::ProgressBar::hidden(),
indicatif::ProgressBar::hidden(),
indicatif::ProgressBar::hidden(),
);
let df_progress = Arc::new(df_progress);
let purity_progress = Arc::new(purity_progress);
let test_progress = Arc::new(test_progress);
let debt_progress = Arc::new(debt_progress);
rayon::scope(|s| {
self.spawn_data_flow_task(
s,
Arc::clone(&call_graph),
Arc::clone(&metrics_arc),
Arc::clone(&data_flow_result),
Arc::clone(&timings),
Arc::clone(&df_progress),
);
self.spawn_purity_task(
s,
Arc::clone(&metrics_arc),
Arc::clone(&purity_result),
Arc::clone(&timings),
Arc::clone(&purity_progress),
);
self.spawn_test_detection_task(
s,
Arc::clone(&call_graph),
Arc::clone(&test_funcs_result),
Arc::clone(&timings),
Arc::clone(&test_progress),
);
self.spawn_debt_aggregation_task(
s,
Arc::clone(&metrics_arc),
debt_items_opt,
Arc::clone(&debt_agg_result),
Arc::clone(&timings),
Arc::clone(&debt_progress),
);
});
let data_flow = data_flow_result
.lock()
.take()
.expect("data flow analysis task completed but produced no result");
let purity = purity_result
.lock()
.take()
.expect("purity analysis task completed but produced no result");
let test_funcs = test_funcs_result
.lock()
.take()
.expect("test detection task completed but produced no result");
let debt_agg = debt_agg_result
.lock()
.take()
.expect("debt aggregation task completed but produced no result");
let t = timings.lock();
self.timings = t.clone();
(data_flow, purity, test_funcs, debt_agg)
}
fn spawn_data_flow_task<'a>(
&self,
scope: &rayon::Scope<'a>,
call_graph: Arc<CallGraph>,
metrics: Arc<Vec<FunctionMetrics>>,
result: Arc<Mutex<Option<DataFlowGraph>>>,
timings: Arc<Mutex<AnalysisPhaseTimings>>,
progress: Arc<indicatif::ProgressBar>,
) {
let extracted_data = self.extracted_data.clone();
scope.spawn(move |_| {
progress.tick();
let start = Instant::now();
let mut data_flow = DataFlowGraph::from_call_graph((*call_graph).clone());
let (purity_count, mutation_count, io_count, dep_count, trans_count) =
if let Some(ref extracted) = extracted_data {
progress.set_message("Populating from extracted data (spec 214)...");
let stats = crate::extraction::adapters::data_flow::populate_data_flow(
&mut data_flow,
extracted,
);
(
stats.purity_entries,
stats.purity_entries, stats.io_operations,
stats.variable_deps,
stats.transformations,
)
} else {
progress.set_message("Extracting file data (fallback path)...");
let file_paths: HashSet<PathBuf> = metrics.iter().map(|m| m.file.clone()).collect();
let fallback_extracted: HashMap<PathBuf, ExtractedFileData> = file_paths
.into_iter()
.filter(|p| p.extension().map(|e| e == "rs").unwrap_or(false))
.filter_map(|path| {
std::fs::read_to_string(&path)
.ok()
.and_then(|content| {
crate::extraction::UnifiedFileExtractor::extract(&path, &content).ok()
})
.map(|data| (path, data))
})
.collect();
progress.set_message("Populating from extracted data (fallback)...");
let stats = crate::extraction::adapters::data_flow::populate_data_flow(
&mut data_flow,
&fallback_extracted,
);
(
stats.purity_entries,
stats.purity_entries, stats.io_operations,
stats.variable_deps,
stats.transformations,
)
};
progress.set_message("Populating purity analysis from metrics...");
for metric in metrics.iter() {
let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
let purity_info = crate::data_flow::PurityInfo {
is_pure: metric.is_pure.unwrap_or(false),
confidence: metric.purity_confidence.unwrap_or(0.0),
impurity_reasons: if !metric.is_pure.unwrap_or(false) {
vec!["Function may have side effects".to_string()]
} else {
vec![]
},
};
data_flow.set_purity_info(func_id, purity_info);
}
timings.lock().data_flow_creation = start.elapsed();
*result.lock() = Some(data_flow);
progress.finish_with_message(format!(
"Data flow complete: {} functions, {} mutations, {} I/O ops, {} deps, {} transforms",
purity_count,
mutation_count,
io_count,
dep_count,
trans_count
));
});
}
fn spawn_purity_task<'a>(
&self,
scope: &rayon::Scope<'a>,
metrics: Arc<Vec<FunctionMetrics>>,
result: Arc<Mutex<Option<HashMap<String, bool>>>>,
timings: Arc<Mutex<AnalysisPhaseTimings>>,
progress: Arc<indicatif::ProgressBar>,
) {
scope.spawn(move |_| {
progress.tick();
let start = Instant::now();
let purity_map = transformations::metrics_to_purity_map(&metrics);
timings.lock().purity_analysis = start.elapsed();
*result.lock() = Some(purity_map);
progress.finish_with_message("Purity analysis complete");
});
}
fn spawn_test_detection_task<'a>(
&self,
scope: &rayon::Scope<'a>,
call_graph: Arc<CallGraph>,
result: Arc<Mutex<Option<HashSet<FunctionId>>>>,
timings: Arc<Mutex<AnalysisPhaseTimings>>,
progress: Arc<indicatif::ProgressBar>,
) {
scope.spawn(move |_| {
progress.tick();
let start = Instant::now();
let detector = OptimizedTestDetector::new(call_graph);
let test_funcs = detector.find_all_test_only_functions();
timings.lock().test_detection = start.elapsed();
*result.lock() = Some(test_funcs);
progress.finish_with_message("Test detection complete");
});
}
fn spawn_debt_aggregation_task<'a>(
&self,
scope: &rayon::Scope<'a>,
metrics: Arc<Vec<FunctionMetrics>>,
debt_items: Option<Vec<crate::core::DebtItem>>,
result: Arc<Mutex<Option<DebtAggregator>>>,
timings: Arc<Mutex<AnalysisPhaseTimings>>,
progress: Arc<indicatif::ProgressBar>,
) {
scope.spawn(move |_| {
progress.tick();
let start = Instant::now();
let mut debt_aggregator = DebtAggregator::new();
if let Some(debt_items) = debt_items {
let function_mappings = transformations::create_function_mappings(&metrics);
debt_aggregator.aggregate_debt(debt_items, &function_mappings);
}
timings.lock().debt_aggregation = start.elapsed();
*result.lock() = Some(debt_aggregator);
progress.finish_with_message("Debt aggregation complete");
});
}
fn report_phase1_completion(&self, phase1_time: Duration) {
log::debug!(
"Phase 1 complete in {:?} (DF: {:?}, Purity: {:?}, Test: {:?}, Debt: {:?})",
phase1_time,
self.timings.data_flow_creation,
self.timings.purity_analysis,
self.timings.test_detection,
self.timings.debt_aggregation,
);
}
#[allow(clippy::too_many_arguments)]
pub fn execute_phase2_parallel(
&mut self,
metrics: &[FunctionMetrics],
test_only_functions: &HashSet<FunctionId>,
debt_aggregator: &DebtAggregator,
data_flow_graph: &DataFlowGraph,
coverage_data: Option<&LcovData>,
framework_exclusions: &HashSet<FunctionId>,
function_pointer_used_functions: Option<&HashSet<FunctionId>>,
) -> Vec<UnifiedDebtItem> {
let start = Instant::now();
let total_metrics = metrics.len();
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(
5,
1,
crate::tui::app::StageStatus::Active,
Some((0, total_metrics)),
);
}
let progress: Option<indicatif::ProgressBar> = None;
let suppression_cache = build_suppression_context_cache(metrics);
let context_detector = ContextDetector::new();
let recommendation_engine = ContextRecommendationEngine::new();
let context = FunctionAnalysisContext {
call_graph: &self.call_graph,
debt_aggregator,
data_flow_graph,
coverage_data,
framework_exclusions,
function_pointer_used_functions,
risk_analyzer: self.risk_analyzer.as_ref(),
project_path: &self.project_path,
context_detector: &context_detector,
recommendation_engine: &recommendation_engine,
};
let items: Vec<UnifiedDebtItem> = self.process_metrics_pipeline(
metrics,
test_only_functions,
&context,
progress.as_ref(),
);
self.timings.function_analysis = start.elapsed();
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(
5,
1,
crate::tui::app::StageStatus::Completed,
Some((total_metrics, total_metrics)),
);
std::thread::sleep(std::time::Duration::from_millis(150));
}
if let Some(pb) = progress {
pb.finish_with_message(format!(
"Function analysis complete ({} items in {:?})",
items.len(),
self.timings.function_analysis
));
}
filter_suppressed_items(items, &suppression_cache)
}
fn process_metrics_pipeline(
&self,
metrics: &[FunctionMetrics],
test_only_functions: &HashSet<FunctionId>,
context: &FunctionAnalysisContext,
progress: Option<&indicatif::ProgressBar>,
) -> Vec<UnifiedDebtItem> {
use std::sync::atomic::{AtomicUsize, Ordering};
let total_metrics = metrics.len();
let processed_count = AtomicUsize::new(0);
let update_interval = (total_metrics / 100).max(1);
metrics
.par_iter()
.progress_with(
progress
.cloned()
.unwrap_or_else(indicatif::ProgressBar::hidden),
)
.flat_map(|metric| {
let result = self.process_single_metric(metric, test_only_functions, context);
let current = processed_count.fetch_add(1, Ordering::Relaxed) + 1;
if current % update_interval == 0 || current == total_metrics {
if let Some(manager) = crate::progress::ProgressManager::global() {
manager.tui_update_subtask(
5,
1,
crate::tui::app::StageStatus::Active,
Some((current, total_metrics)),
);
}
}
result
})
.collect()
}
fn process_single_metric(
&self,
metric: &FunctionMetrics,
test_only_functions: &HashSet<FunctionId>,
context: &FunctionAnalysisContext,
) -> Vec<UnifiedDebtItem> {
let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
let callee_count = self.call_graph.get_callees(&func_id).len();
if !predicates::should_process_metric(metric, test_only_functions, callee_count) {
return Vec::new();
}
self.metric_to_debt_items(metric, context)
}
fn metric_to_debt_items(
&self,
metric: &FunctionMetrics,
context: &FunctionAnalysisContext,
) -> Vec<UnifiedDebtItem> {
crate::builders::unified_analysis::create_debt_item_from_metric_with_aggregator(
metric,
context.call_graph,
context.coverage_data,
context.framework_exclusions,
context.function_pointer_used_functions,
context.debt_aggregator,
Some(context.data_flow_graph),
context.risk_analyzer,
context.project_path,
context.context_detector,
context.recommendation_engine,
)
}
pub fn execute_phase3_parallel(
&mut self,
metrics: &[FunctionMetrics],
coverage_data: Option<&LcovData>,
no_god_object: bool,
) -> Vec<(FileDebtItem, Vec<FunctionMetrics>)> {
let start = Instant::now();
let mut files_map: HashMap<PathBuf, Vec<&FunctionMetrics>> = HashMap::new();
for metric in metrics {
files_map
.entry(metric.file.clone())
.or_default()
.push(metric);
}
let total_files = files_map.len();
if let Some(manager) = crate::progress::ProgressManager::global() {
manager.tui_update_subtask(
5,
2,
crate::tui::app::StageStatus::Active,
Some((0, total_files)),
);
}
let processed_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let last_update = std::sync::Arc::new(std::sync::Mutex::new(Instant::now()));
let progress = indicatif::ProgressBar::hidden();
let mut file_data: Vec<(FileDebtItem, Vec<FunctionMetrics>)> = files_map
.par_iter()
.progress_with(progress.clone())
.filter_map(|(file_path, functions)| {
let result =
self.analyze_file_parallel(file_path, functions, coverage_data, no_god_object);
let current =
processed_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
if let Ok(mut last) = last_update.try_lock() {
if current % 10 == 0 || last.elapsed() > std::time::Duration::from_millis(100) {
if let Some(manager) = crate::progress::ProgressManager::global() {
manager.tui_update_subtask(
5,
2,
crate::tui::app::StageStatus::Active,
Some((current, total_files)),
);
}
*last = Instant::now();
}
}
result.map(|item| {
let raw_functions: Vec<FunctionMetrics> =
functions.iter().map(|&f| f.clone()).collect();
(item, raw_functions)
})
})
.collect();
file_data.sort_by(|a, b| a.0.metrics.path.cmp(&b.0.metrics.path));
self.timings.file_analysis = start.elapsed();
progress.finish_and_clear();
if let Some(manager) = crate::progress::ProgressManager::global() {
manager.tui_update_subtask(
5,
2,
crate::tui::app::StageStatus::Completed,
Some((total_files, total_files)),
);
}
file_data
}
fn analyze_file_parallel(
&self,
file_path: &Path,
functions: &[&FunctionMetrics],
coverage_data: Option<&LcovData>,
no_god_object: bool,
) -> Option<FileDebtItem> {
let functions_owned: Vec<FunctionMetrics> = functions.iter().map(|&f| f.clone()).collect();
let mut file_metrics =
file_analysis::aggregate_file_metrics(&functions_owned, coverage_data);
let cached_line_count = self.line_count_index.get(file_path).copied();
let estimated_lines = cached_line_count.unwrap_or_else(|| {
functions_owned.iter().map(|f| f.length).sum::<usize>()
});
let skip_god_object_analysis =
no_god_object || (estimated_lines < 500 && file_metrics.function_count < 20);
let extracted_file_data = self
.extracted_data
.as_ref()
.and_then(|data| data.get(file_path));
if let Some(extracted) = extracted_file_data {
file_metrics.total_lines = extracted.total_lines;
file_metrics.uncovered_lines =
((1.0 - file_metrics.coverage_percent) * extracted.total_lines as f64) as usize;
file_metrics.god_object_analysis = if skip_god_object_analysis {
None
} else {
crate::extraction::adapters::god_object::analyze_god_object(file_path, extracted)
};
} else {
let needs_file_read = cached_line_count.is_none() || !skip_god_object_analysis;
if needs_file_read {
if let Ok(content) = std::fs::read_to_string(file_path) {
let actual_line_count = content.lines().count();
file_metrics.total_lines = actual_line_count;
file_metrics.uncovered_lines =
((1.0 - file_metrics.coverage_percent) * actual_line_count as f64) as usize;
file_metrics.god_object_analysis = if skip_god_object_analysis {
None
} else {
let analysis_result =
file_analysis::analyze_god_object(&content, file_path, coverage_data);
let analyzed = analysis_result.unwrap_or(None);
if analyzed.as_ref().is_some_and(|a| a.is_god_object) {
analyzed
} else {
crate::organization::god_object::heuristics::fallback_with_preserved_analysis(
file_metrics.function_count,
actual_line_count,
file_metrics.total_complexity,
analyzed.as_ref(),
).or(analyzed)
}
};
} else {
file_metrics.god_object_analysis = if no_god_object {
None
} else {
self.analyze_god_object_with_io(file_path, coverage_data)
};
}
} else {
file_metrics.total_lines = cached_line_count.unwrap_or(estimated_lines);
file_metrics.uncovered_lines = ((1.0 - file_metrics.coverage_percent)
* file_metrics.total_lines as f64)
as usize;
file_metrics.god_object_analysis = None; }
}
file_metrics.function_scores = Vec::new();
use crate::analysis::FileContextDetector;
use crate::core::Language;
let language = Language::from_path(file_path);
let detector = FileContextDetector::new(language);
let file_context = detector.detect(file_path, &functions_owned);
let item = crate::priority::FileDebtItem::from_metrics(file_metrics, Some(&file_context));
let has_god_object = item
.metrics
.god_object_analysis
.as_ref()
.is_some_and(|analysis| analysis.is_god_object);
if file_analysis::should_include_file(item.score) || has_god_object {
Some(item)
} else {
None
}
}
fn analyze_god_object_with_io(
&self,
file_path: &Path,
coverage_data: Option<&LcovData>,
) -> Option<crate::organization::GodObjectAnalysis> {
let _span = debug_span!("analyze_god_object", path = %file_path.display()).entered();
let content = std::fs::read_to_string(file_path)
.map_err(|e| {
warn!(file = %file_path.display(), error = %e, "Failed to read file");
e
})
.ok()?;
file_analysis::analyze_god_object(&content, file_path, coverage_data)
.map_err(|e| {
warn!(file = %file_path.display(), error = %e, "Failed to analyze god object");
e
})
.ok()
.flatten() }
pub fn build(
mut self,
data_flow_graph: DataFlowGraph,
purity_analysis: HashMap<String, bool>,
items: Vec<UnifiedDebtItem>,
file_data: Vec<(FileDebtItem, Vec<FunctionMetrics>)>,
coverage_data: Option<&LcovData>,
) -> (UnifiedAnalysis, AnalysisPhaseTimings) {
let start = Instant::now();
let total_file_items = file_data.len();
let agg_progress = ProgressManager::global()
.map(|pm| pm.create_spinner("Aggregating analysis results"))
.unwrap_or_else(indicatif::ProgressBar::hidden);
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(
5,
3,
crate::tui::app::StageStatus::Active,
Some((0, total_file_items.max(1))),
);
}
let mut unified = UnifiedAnalysis::new((*self.call_graph).clone());
unified.data_flow_graph = data_flow_graph;
for (file_item, _) in &file_data {
if file_item.metrics.total_lines > 0 {
unified.register_analyzed_file(
file_item.metrics.path.clone(),
file_item.metrics.total_lines,
);
}
}
for (func_name, is_pure) in purity_analysis {
if let Some(item) = unified
.items
.iter_mut()
.find(|i| i.location.function == func_name)
{
item.is_pure = Some(is_pure);
}
}
for item in items {
unified.add_item(item);
}
for (index, (file_item, raw_functions)) in file_data.into_iter().enumerate() {
if let Some(ref god_analysis) = file_item.metrics.god_object_analysis {
if god_analysis.is_god_object {
let is_suppressed = std::fs::read_to_string(&file_item.metrics.path)
.ok()
.is_some_and(|content| {
is_god_object_suppressed_parallel(
god_analysis,
&content,
&file_item.metrics.path,
)
});
if is_suppressed {
let mut file_item = file_item;
file_item.metrics.god_object_analysis = None;
let file_item_with_deps =
enrich_file_item_with_dependencies(file_item, &unified.items);
unified.add_file_item(file_item_with_deps);
update_finalization_subtask(index + 1, total_file_items);
continue;
}
use crate::priority::god_object_aggregation::{
aggregate_coverage_from_raw_metrics, aggregate_from_raw_metrics,
aggregate_god_object_metrics, extract_member_functions,
};
let mut aggregated_metrics = aggregate_from_raw_metrics(&raw_functions);
if let Some(lcov) = coverage_data {
aggregated_metrics.weighted_coverage =
aggregate_coverage_from_raw_metrics(&raw_functions, lcov);
}
let member_functions =
extract_member_functions(unified.items.iter(), &file_item.metrics.path);
if !member_functions.is_empty() {
let item_metrics = aggregate_god_object_metrics(&member_functions);
aggregated_metrics.aggregated_contextual_risk = self
.risk_analyzer
.as_ref()
.and_then(|analyzer| {
crate::builders::unified_analysis::analyze_file_git_context(
&file_item.metrics.path,
analyzer,
&self.project_path,
)
})
.or(item_metrics.aggregated_contextual_risk); } else {
aggregated_metrics.aggregated_contextual_risk =
self.risk_analyzer.as_ref().and_then(|analyzer| {
crate::builders::unified_analysis::analyze_file_git_context(
&file_item.metrics.path,
analyzer,
&self.project_path,
)
});
}
let mut god_analysis = god_analysis.clone();
god_analysis.aggregated_entropy = aggregated_metrics.aggregated_entropy.clone();
god_analysis.aggregated_error_swallowing_count =
if aggregated_metrics.total_error_swallowing_count > 0 {
Some(aggregated_metrics.total_error_swallowing_count)
} else {
None
};
god_analysis.aggregated_error_swallowing_patterns =
if !aggregated_metrics.error_swallowing_patterns.is_empty() {
Some(aggregated_metrics.error_swallowing_patterns.clone())
} else {
None
};
let mut god_item =
crate::builders::unified_analysis::create_god_object_debt_item(
&file_item.metrics.path,
&file_item.metrics,
&god_analysis,
aggregated_metrics,
coverage_data,
Some(&self.call_graph),
);
use crate::priority::context::{generate_context_suggestion, ContextConfig};
let context_config = ContextConfig::default();
god_item.context_suggestion =
generate_context_suggestion(&god_item, &self.call_graph, &context_config);
unified.add_item(god_item);
}
}
let file_item_with_deps = enrich_file_item_with_dependencies(file_item, &unified.items);
unified.add_file_item(file_item_with_deps);
update_finalization_subtask(index + 1, total_file_items);
}
agg_progress.set_message("Sorting by priority and calculating impact");
unified.sort_by_priority();
unified.calculate_total_impact();
unified.has_coverage_data = coverage_data.is_some();
if let Some(lcov) = coverage_data {
unified.overall_coverage = Some(lcov.get_overall_coverage());
}
if let Some(manager) = ProgressManager::global() {
manager.tui_update_subtask(
5,
3,
crate::tui::app::StageStatus::Completed,
Some((total_file_items.max(1), total_file_items.max(1))),
);
}
agg_progress.finish_with_message(format!(
"Analysis complete ({} function items, {} file items)",
unified.items.len(),
unified.file_items.len()
));
self.timings.sorting = start.elapsed();
self.timings.total = self.timings.call_graph_building
+ self.timings.trait_resolution
+ self.timings.coverage_loading
+ self.timings.data_flow_creation
+ self.timings.purity_analysis
+ self.timings.test_detection
+ self.timings.debt_aggregation
+ self.timings.function_analysis
+ self.timings.file_analysis
+ self.timings.aggregation
+ self.timings.sorting;
if self.options.progress {
log::debug!("Total parallel analysis time: {:?}", self.timings.total);
log::debug!(
" - Call graph building: {:?}",
self.timings.call_graph_building
);
log::debug!(" - Trait resolution: {:?}", self.timings.trait_resolution);
log::debug!(" - Coverage loading: {:?}", self.timings.coverage_loading);
log::debug!(" - Data flow: {:?}", self.timings.data_flow_creation);
log::debug!(" - Purity: {:?}", self.timings.purity_analysis);
log::debug!(" - Test detection: {:?}", self.timings.test_detection);
log::debug!(" - Debt aggregation: {:?}", self.timings.debt_aggregation);
log::debug!(
" - Function analysis: {:?}",
self.timings.function_analysis
);
log::debug!(" - File analysis: {:?}", self.timings.file_analysis);
log::debug!(" - Sorting: {:?}", self.timings.sorting);
}
(unified, self.timings)
}
}
fn update_finalization_subtask(current: usize, total: usize) {
let Some(manager) = ProgressManager::global() else {
return;
};
let should_refresh = current == total || current == 1 || current % 10 == 0;
if should_refresh {
manager.tui_update_subtask(
5,
3,
crate::tui::app::StageStatus::Active,
Some((current, total.max(1))),
);
}
}
fn enrich_file_item_with_dependencies(
mut file_item: crate::priority::FileDebtItem,
unified_items: &im::Vector<crate::priority::UnifiedDebtItem>,
) -> crate::priority::FileDebtItem {
use crate::priority::god_object_aggregation::{
aggregate_dependency_metrics, extract_member_functions,
};
let member_functions = extract_member_functions(unified_items.iter(), &file_item.metrics.path);
let (callers, callees, afferent, efferent) = aggregate_dependency_metrics(&member_functions);
file_item.metrics.afferent_coupling = afferent;
file_item.metrics.efferent_coupling = efferent;
file_item.metrics.instability =
crate::output::unified::calculate_instability(afferent, efferent);
file_item.metrics.dependents = callers.into_iter().take(10).collect();
file_item.metrics.dependencies_list = callees.into_iter().take(10).collect();
file_item
}
pub trait ParallelAnalyzer {
fn analyze_parallel(
&self,
options: ParallelUnifiedAnalysisOptions,
) -> Result<UnifiedAnalysis, anyhow::Error>;
}