use crate::{
analysis::ContextDetector,
builders::unified_analysis_phases::phases::scoring::{
SuppressionContextCache, build_suppression_context_cache,
},
core::FunctionMetrics,
data_flow::DataFlowGraph,
extraction::ExtractedFileData,
priority::{
UnifiedAnalysis, UnifiedAnalysisUtils, UnifiedDebtItem,
call_graph::{CallGraph, FunctionId},
debt_aggregator::DebtAggregator,
file_metrics::FileDebtItem,
scoring::ContextRecommendationEngine,
},
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};
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 clone_function_metrics(functions: &[&FunctionMetrics]) -> Vec<FunctionMetrics> {
functions.iter().map(|&function| function.clone()).collect()
}
fn should_emit_file_item(item: &FileDebtItem) -> bool {
let has_god_object = item
.metrics
.god_object_analysis
.as_ref()
.is_some_and(|analysis| analysis.is_god_object);
crate::builders::unified_analysis_phases::phases::file_analysis::should_include_file(item.score)
|| has_god_object
}
#[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,
file_line_counts: &'a HashMap<PathBuf, usize>,
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() && Self::is_test_function(func_id) {
test_roots.insert(func_id.clone());
}
}
test_roots
}
fn is_test_function(func_id: &FunctionId) -> bool {
let file = func_id.file.to_string_lossy();
func_id.name.starts_with("test_")
|| func_id.name.contains("::test")
|| file.contains("/tests/")
|| file.contains("_test.rs")
}
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 self.reaches_non_test_root(current, &mut visited, &mut queue) {
return false;
}
}
true
}
fn reaches_non_test_root(
&self,
current: FunctionId,
visited: &mut HashSet<FunctionId>,
queue: &mut Vec<FunctionId>,
) -> bool {
if !visited.insert(current.clone()) {
return false;
}
let callers = self.call_graph.get_callers(¤t);
if callers.is_empty() {
return !self.test_roots.contains(¤t);
}
Self::enqueue_unvisited_callers(callers, visited, queue);
false
}
fn enqueue_unvisited_callers(
callers: Vec<FunctionId>,
visited: &HashSet<FunctionId>,
queue: &mut Vec<FunctionId>,
) {
for caller in callers
.into_iter()
.filter(|caller| !visited.contains(caller))
{
queue.push(caller);
}
}
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();
progress.set_message("Preparing shared data-flow facts...");
let data_flow = crate::builders::unified_analysis_phases::phases::preparation::build_data_flow_graph(
&metrics,
&call_graph,
extracted_data.as_deref(),
);
timings.lock().data_flow_creation = start.elapsed();
*result.lock() = Some(data_flow);
progress.finish_with_message("Data-flow preparation complete");
});
}
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 =
crate::builders::unified_analysis_phases::phases::scoring::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 test_funcs = crate::builders::unified_analysis_phases::phases::call_graph::find_test_only_functions(&call_graph);
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 debt_aggregator =
crate::builders::unified_analysis_phases::phases::scoring::setup_debt_aggregator(
&metrics,
debt_items.as_deref(),
);
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,
file_line_counts: &self.line_count_index,
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)
&& 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> {
if !crate::builders::unified_analysis_phases::phases::call_graph::should_process_metric(
metric,
&self.call_graph,
test_only_functions,
) {
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.file_line_counts,
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()
&& (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 = clone_function_metrics(functions);
let extracted = self
.extracted_data
.as_ref()
.and_then(|data| data.get(file_path));
let file_content = std::fs::read_to_string(file_path).ok();
let mut processed =
crate::builders::unified_analysis_phases::phases::file_analysis::process_file_metrics_with_facts(
file_path.to_path_buf(),
functions_owned,
crate::builders::unified_analysis_phases::phases::file_analysis::FileAnalysisFacts {
content: file_content.as_deref(),
extracted,
line_count: self.line_count_index.get(file_path).copied(),
},
coverage_data,
no_god_object,
&self.project_path,
);
processed.file_metrics.function_scores.clear();
let item =
crate::builders::unified_analysis_phases::phases::file_analysis::create_file_debt_item(
processed.file_metrics,
Some(&processed.file_context),
);
if should_emit_file_item(&item) {
Some(item)
} else {
None
}
}
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 = create_final_aggregation_progress(total_file_items);
let mut unified = self.initialize_unified_analysis(data_flow_graph, &file_data);
apply_purity_analysis(&mut unified, purity_analysis);
add_unified_items(&mut unified, items);
self.add_finalized_file_items(&mut unified, file_data, coverage_data);
agg_progress.set_message("Sorting by priority and calculating impact");
finalize_unified_analysis(&mut unified, coverage_data);
complete_finalization_subtask(total_file_items);
finish_aggregation_progress(&agg_progress, &unified);
self.record_final_timing(start.elapsed());
self.log_timing_summary();
(unified, self.timings)
}
fn initialize_unified_analysis(
&self,
data_flow_graph: DataFlowGraph,
_file_data: &[(FileDebtItem, Vec<FunctionMetrics>)],
) -> UnifiedAnalysis {
let mut unified = UnifiedAnalysis::new((*self.call_graph).clone());
unified.data_flow_graph = data_flow_graph;
register_analyzed_files(&mut unified, &self.line_count_index);
unified
}
fn add_finalized_file_items(
&self,
unified: &mut UnifiedAnalysis,
file_data: Vec<(FileDebtItem, Vec<FunctionMetrics>)>,
coverage_data: Option<&LcovData>,
) {
let total_file_items = file_data.len();
for (index, (file_item, raw_functions)) in file_data.into_iter().enumerate() {
let finalized = crate::builders::unified_analysis::finalize_file_item(
unified,
file_item,
&raw_functions,
coverage_data,
self.risk_analyzer.as_ref(),
&self.project_path,
&self.call_graph,
);
unified.add_file_item(finalized);
update_finalization_subtask(index + 1, total_file_items);
}
}
fn record_final_timing(&mut self, elapsed: Duration) {
self.timings.sorting = elapsed;
self.timings.total = total_analysis_duration(&self.timings);
}
fn log_timing_summary(&self) {
if !self.options.progress {
return;
}
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);
}
}
fn create_final_aggregation_progress(total_file_items: usize) -> indicatif::ProgressBar {
let 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))),
);
}
progress
}
fn register_analyzed_files(
unified: &mut UnifiedAnalysis,
line_count_index: &HashMap<PathBuf, usize>,
) {
for (path, line_count) in line_count_index {
if *line_count > 0 {
unified.register_analyzed_file(path.clone(), *line_count);
}
}
}
fn apply_purity_analysis(unified: &mut UnifiedAnalysis, purity_analysis: HashMap<String, bool>) {
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);
}
}
}
fn add_unified_items(unified: &mut UnifiedAnalysis, items: Vec<UnifiedDebtItem>) {
for item in items {
unified.add_item(item);
}
}
fn finalize_unified_analysis(unified: &mut UnifiedAnalysis, coverage_data: Option<&LcovData>) {
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());
}
}
fn complete_finalization_subtask(total_file_items: usize) {
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))),
);
}
}
fn finish_aggregation_progress(progress: &indicatif::ProgressBar, unified: &UnifiedAnalysis) {
progress.finish_with_message(format!(
"Analysis complete ({} function items, {} file items)",
unified.items.len(),
unified.file_items.len()
));
}
fn total_analysis_duration(timings: &AnalysisPhaseTimings) -> Duration {
timings.call_graph_building
+ timings.trait_resolution
+ timings.coverage_loading
+ timings.data_flow_creation
+ timings.purity_analysis
+ timings.test_detection
+ timings.debt_aggregation
+ timings.function_analysis
+ timings.file_analysis
+ timings.aggregation
+ timings.sorting
}
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))),
);
}
}
pub trait ParallelAnalyzer {
fn analyze_parallel(
&self,
options: ParallelUnifiedAnalysisOptions,
) -> Result<UnifiedAnalysis, anyhow::Error>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::priority::call_graph::CallType;
fn function_id(file: &str, name: &str, line: usize) -> FunctionId {
FunctionId::new(PathBuf::from(file), name.to_string(), line)
}
fn graph_with_functions(functions: &[FunctionId]) -> CallGraph {
let mut graph = CallGraph::new();
for func in functions.iter() {
graph.add_function(func.clone(), false, false, 1, 10);
}
graph
}
#[test]
fn test_only_detector_marks_helper_called_only_by_tests() {
let test = function_id("tests/integration.rs", "test_parses_input", 10);
let helper = function_id("src/parser.rs", "build_fixture", 20);
let mut graph = graph_with_functions(&[test.clone(), helper.clone()]);
graph.add_call_parts(test, helper.clone(), CallType::Direct);
let detector = OptimizedTestDetector::new(Arc::new(graph));
assert!(detector.is_test_only(&helper));
}
#[test]
fn test_only_detector_rejects_helper_reachable_from_production_root() {
let test = function_id("tests/integration.rs", "test_parses_input", 10);
let production = function_id("src/main.rs", "main", 1);
let helper = function_id("src/parser.rs", "build_fixture", 20);
let mut graph = graph_with_functions(&[test.clone(), production.clone(), helper.clone()]);
graph.add_call_parts(test, helper.clone(), CallType::Direct);
graph.add_call_parts(production, helper.clone(), CallType::Direct);
let detector = OptimizedTestDetector::new(Arc::new(graph));
assert!(!detector.is_test_only(&helper));
}
#[test]
fn parallel_phase_uses_canonical_test_only_classification() {
let attributed_test = function_id("src/parser.rs", "checks_input", 10);
let helper = function_id("src/parser.rs", "build_fixture", 20);
let mut graph = CallGraph::new();
graph.add_function(attributed_test.clone(), false, true, 2, 5);
graph.add_function(helper.clone(), false, false, 5, 20);
graph.add_call_parts(attributed_test, helper.clone(), CallType::Direct);
let expected = graph.find_test_only_functions().into_iter().collect();
let mut builder = ParallelUnifiedAnalysisBuilder::new(
graph,
ParallelUnifiedAnalysisOptions {
progress: false,
..ParallelUnifiedAnalysisOptions::default()
},
);
let (_, _, actual, _) = builder.execute_phase1_parallel(&[], None);
assert!(actual.contains(&helper));
assert_eq!(actual, expected);
}
}