use super::{
env::{AnalysisEnv, ProgressReporter},
guards::*,
state::{AnalysisPhase, AnalysisState},
};
use crate::priority::UnifiedAnalysisUtils;
use anyhow::{Context, Result};
pub fn build_call_graph<Env: AnalysisEnv>(state: &mut AnalysisState, env: &mut Env) -> Result<()> {
debug_assert!(can_start_call_graph(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::CallGraphBuilding.display_name());
state.transition_to(AnalysisPhase::CallGraphBuilding);
let metrics = state
.results
.metrics
.as_ref()
.expect("guard ensures metrics exist");
let mut call_graph = crate::builders::call_graph::build_initial_call_graph(metrics);
let (framework_exclusions, function_pointer_used_functions) = if state.config.parallel {
let (graph, exclusions, used_funcs) =
crate::builders::parallel_call_graph::build_call_graph_parallel(
&state.config.project_path,
call_graph.clone(),
if state.config.jobs == 0 {
None
} else {
Some(state.config.jobs)
},
|_progress| {
},
)
.context("Failed to build parallel call graph")?;
call_graph = graph;
(exclusions, used_funcs)
} else {
crate::builders::call_graph::process_rust_files_for_call_graph(
&state.config.project_path,
&mut call_graph,
state.config.verbose_macro_warnings,
state.config.show_macro_stats,
|progress| {
env.phase_progress(progress.current as f64 / progress.total.max(1) as f64 * 0.3);
},
)
.context("Failed to process Rust files for call graph")?
};
{
use crate::analysis::call_graph::TraitRegistry;
let trait_registry = TraitRegistry::new();
trait_registry.detect_common_trait_patterns(&mut call_graph);
}
state.results.call_graph = Some(call_graph);
state.results.framework_exclusions = Some(framework_exclusions);
state.results.function_pointer_used_functions = Some(function_pointer_used_functions);
state.transition_to(AnalysisPhase::CallGraphComplete);
env.phase_complete();
Ok(())
}
pub fn load_coverage<Env: AnalysisEnv>(state: &mut AnalysisState, env: &mut Env) -> Result<()> {
debug_assert!(can_start_coverage(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::CoverageLoading.display_name());
state.transition_to(AnalysisPhase::CoverageLoading);
let coverage_path = state
.config
.coverage_file
.as_ref()
.expect("guard ensures coverage_file exists");
let coverage_data =
crate::risk::lcov::parse_lcov_file_with_callback(coverage_path, |progress| {
use crate::risk::lcov::CoverageProgress;
match progress {
CoverageProgress::Initializing => env.phase_progress(0.1),
CoverageProgress::Parsing { current, total } => {
env.phase_progress(0.1 + (current as f64 / total.max(1) as f64) * 0.7)
}
CoverageProgress::ComputingStats => env.phase_progress(0.9),
CoverageProgress::Complete => env.phase_progress(1.0),
}
})
.context("Failed to parse LCOV file")?;
state.results.coverage = Some(coverage_data);
state.transition_to(AnalysisPhase::CoverageComplete);
env.phase_complete();
Ok(())
}
pub fn skip_coverage(state: &mut AnalysisState) {
debug_assert!(can_skip_coverage(state), "Guard should be checked first");
state.results.coverage = None;
state.transition_to(AnalysisPhase::CoverageComplete);
}
pub fn analyze_purity<Env: ProgressReporter>(
state: &mut AnalysisState,
env: &mut Env,
) -> Result<()> {
debug_assert!(can_start_purity(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::PurityAnalyzing.display_name());
state.transition_to(AnalysisPhase::PurityAnalyzing);
let metrics = state
.results
.metrics
.as_ref()
.expect("guard ensures metrics exist");
let call_graph = state
.results
.call_graph
.as_ref()
.expect("guard ensures call_graph exists");
let enriched_metrics = crate::analyzers::call_graph_integration::populate_call_graph_data(
metrics.clone(),
call_graph,
);
env.phase_progress(0.3);
use crate::analysis::call_graph::{
CrossModuleTracker, FrameworkPatternDetector, FunctionPointerTracker, RustCallGraph,
TraitRegistry,
};
use crate::analysis::purity_analysis::PurityAnalyzer;
use crate::analysis::purity_propagation::{PurityCallGraphAdapter, PurityPropagator};
use crate::priority::call_graph::FunctionId;
let rust_graph = RustCallGraph {
base_graph: call_graph.clone(),
trait_registry: TraitRegistry::new(),
function_pointer_tracker: FunctionPointerTracker::new(),
framework_patterns: FrameworkPatternDetector::new(),
cross_module_tracker: CrossModuleTracker::new(),
};
let adapter = PurityCallGraphAdapter::from_rust_graph(rust_graph);
let purity_analyzer = PurityAnalyzer::new();
let mut propagator = PurityPropagator::new(adapter, purity_analyzer);
env.phase_progress(0.5);
if let Err(e) = propagator.propagate(&enriched_metrics) {
log::debug!("Purity propagation skipped (external deps): {}", e);
state.results.enriched_metrics = Some(enriched_metrics);
state.transition_to(AnalysisPhase::PurityComplete);
env.phase_complete();
return Ok(());
}
env.phase_progress(0.8);
let final_metrics: Vec<_> = enriched_metrics
.iter()
.map(|metric| {
let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
if let Some(result) = propagator.get_result(&func_id) {
let mut updated = metric.clone();
updated.is_pure = Some(
result.level == crate::analysis::purity_analysis::PurityLevel::StrictlyPure,
);
updated.purity_confidence = Some(result.confidence as f32);
updated.purity_reason = Some(format!("{:?}", result.reason));
updated
} else {
metric.clone()
}
})
.collect();
state.results.enriched_metrics = Some(final_metrics);
state.transition_to(AnalysisPhase::PurityComplete);
env.phase_complete();
Ok(())
}
pub fn load_context<Env: AnalysisEnv>(state: &mut AnalysisState, env: &mut Env) -> Result<()> {
debug_assert!(can_start_context(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::ContextLoading.display_name());
state.transition_to(AnalysisPhase::ContextLoading);
let context_aggregator = crate::utils::risk_analyzer::build_context_aggregator(
&state.config.project_path,
state.config.enable_context,
state.config.context_providers.clone(),
state.config.disable_context.clone(),
);
if let Some(aggregator) = context_aggregator {
let debt_score = 50.0; let debt_threshold = 100.0;
let risk_analyzer = crate::risk::RiskAnalyzer::default()
.with_debt_context(debt_score, debt_threshold)
.with_context_aggregator(aggregator);
state.results.risk_analyzer = Some(risk_analyzer);
}
state.transition_to(AnalysisPhase::ContextComplete);
env.phase_complete();
Ok(())
}
pub fn skip_context(state: &mut AnalysisState) {
debug_assert!(can_skip_context(state), "Guard should be checked first");
state.results.risk_analyzer = None;
state.transition_to(AnalysisPhase::ContextComplete);
}
pub fn compute_scores<Env: ProgressReporter>(
state: &mut AnalysisState,
env: &mut Env,
) -> Result<()> {
debug_assert!(can_start_scoring(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::ScoringInProgress.display_name());
state.transition_to(AnalysisPhase::ScoringInProgress);
let call_graph = state
.results
.call_graph
.as_ref()
.expect("guard ensures call_graph exists");
let enriched_metrics = state
.results
.enriched_metrics
.as_ref()
.expect("guard ensures enriched_metrics exist");
let framework_exclusions = state
.results
.framework_exclusions
.clone()
.unwrap_or_default();
let function_pointer_used = state.results.function_pointer_used_functions.clone();
let unified = crate::builders::unified_analysis::create_unified_analysis_with_exclusions(
enriched_metrics,
call_graph,
state.results.coverage.as_ref(),
&framework_exclusions,
function_pointer_used.as_ref(),
None, state.config.no_aggregation,
state.config.aggregation_method.clone(),
state.config.min_problematic,
state.config.no_god_object,
chrono::Utc::now(),
);
env.phase_progress(0.9);
state.results.unified_analysis = Some(unified);
state.transition_to(AnalysisPhase::ScoringComplete);
env.phase_complete();
Ok(())
}
pub fn filter_and_rank<Env: ProgressReporter>(
state: &mut AnalysisState,
env: &mut Env,
) -> Result<()> {
debug_assert!(can_start_filtering(state), "Guard should be checked first");
env.phase_starting(AnalysisPhase::FilteringInProgress.display_name());
state.transition_to(AnalysisPhase::FilteringInProgress);
if let Some(ref mut unified) = state.results.unified_analysis {
unified.sort_by_priority();
unified.calculate_total_impact();
}
state.transition_to(AnalysisPhase::Complete);
env.phase_complete();
Ok(())
}
pub struct WorkflowRunner<Env> {
state: AnalysisState,
env: Env,
}
impl<Env: AnalysisEnv> WorkflowRunner<Env> {
pub fn new(state: AnalysisState, env: Env) -> Self {
Self { state, env }
}
pub fn state(&self) -> &AnalysisState {
&self.state
}
pub fn state_mut(&mut self) -> &mut AnalysisState {
&mut self.state
}
pub fn into_state(self) -> AnalysisState {
self.state
}
pub fn step(&mut self) -> Result<bool> {
if can_start_call_graph(&self.state) {
build_call_graph(&mut self.state, &mut self.env)?;
return Ok(true);
}
if can_start_coverage(&self.state) {
load_coverage(&mut self.state, &mut self.env)?;
return Ok(true);
}
if can_skip_coverage(&self.state) {
skip_coverage(&mut self.state);
return Ok(true);
}
if can_start_purity(&self.state) {
analyze_purity(&mut self.state, &mut self.env)?;
return Ok(true);
}
if can_start_context(&self.state) {
load_context(&mut self.state, &mut self.env)?;
return Ok(true);
}
if can_skip_context(&self.state) {
skip_context(&mut self.state);
return Ok(true);
}
if can_start_scoring(&self.state) {
compute_scores(&mut self.state, &mut self.env)?;
return Ok(true);
}
if can_start_filtering(&self.state) {
filter_and_rank(&mut self.state, &mut self.env)?;
return Ok(true);
}
Ok(false)
}
pub fn run(mut self) -> Result<AnalysisState> {
while !self.state.is_complete() {
if !self.step()? {
return Err(anyhow::anyhow!(
"Workflow stuck at phase {:?} - missing prerequisites",
self.state.phase
));
}
}
Ok(self.state)
}
}
pub fn run_analysis<Env: AnalysisEnv>(state: AnalysisState, env: Env) -> Result<AnalysisState> {
WorkflowRunner::new(state, env).run()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::workflow::env::MockAnalysisEnv;
use crate::analysis::workflow::state::AnalysisConfig;
use crate::core::FunctionMetrics;
use std::path::PathBuf;
fn create_test_config() -> AnalysisConfig {
AnalysisConfig {
project_path: PathBuf::from("."),
..Default::default()
}
}
fn create_test_metrics() -> Vec<FunctionMetrics> {
vec![FunctionMetrics::new(
"test_fn".to_string(),
PathBuf::from("test.rs"),
1,
)]
}
#[test]
fn test_workflow_runner_creation() {
let config = create_test_config();
let state = AnalysisState::with_metrics(config, create_test_metrics());
let env = MockAnalysisEnv::new();
let runner = WorkflowRunner::new(state, env);
assert_eq!(runner.state().phase, AnalysisPhase::Initialized);
}
#[test]
fn test_skip_coverage_action() {
let config = create_test_config();
let mut state = AnalysisState::with_metrics(config, create_test_metrics());
state.phase = AnalysisPhase::CallGraphComplete;
state.results.call_graph = Some(crate::priority::call_graph::CallGraph::new());
assert!(can_skip_coverage(&state));
skip_coverage(&mut state);
assert_eq!(state.phase, AnalysisPhase::CoverageComplete);
assert!(state.results.coverage.is_none());
}
#[test]
fn test_skip_context_action() {
let config = create_test_config();
let mut state = AnalysisState::with_metrics(config, create_test_metrics());
state.phase = AnalysisPhase::PurityComplete;
state.results.enriched_metrics = Some(create_test_metrics());
assert!(can_skip_context(&state));
skip_context(&mut state);
assert_eq!(state.phase, AnalysisPhase::ContextComplete);
assert!(state.results.risk_analyzer.is_none());
}
#[test]
fn test_workflow_step_no_metrics() {
let config = create_test_config();
let state = AnalysisState::new(config); let env = MockAnalysisEnv::new();
let mut runner = WorkflowRunner::new(state, env);
let stepped = runner.step().unwrap();
assert!(!stepped);
}
}