use crate::core::FunctionMetrics;
use crate::priority::call_graph::CallGraph;
use crate::risk::lcov::LcovData;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AnalysisPhase {
#[default]
Initialized,
CallGraphBuilding,
CallGraphComplete,
CoverageLoading,
CoverageComplete,
PurityAnalyzing,
PurityComplete,
ContextLoading,
ContextComplete,
ScoringInProgress,
ScoringComplete,
FilteringInProgress,
Complete,
}
impl AnalysisPhase {
pub fn is_final(&self) -> bool {
matches!(self, AnalysisPhase::Complete)
}
pub fn display_name(&self) -> &'static str {
match self {
AnalysisPhase::Initialized => "Initialized",
AnalysisPhase::CallGraphBuilding => "Building call graph",
AnalysisPhase::CallGraphComplete => "Call graph complete",
AnalysisPhase::CoverageLoading => "Loading coverage data",
AnalysisPhase::CoverageComplete => "Coverage complete",
AnalysisPhase::PurityAnalyzing => "Analyzing function purity",
AnalysisPhase::PurityComplete => "Purity analysis complete",
AnalysisPhase::ContextLoading => "Loading project context",
AnalysisPhase::ContextComplete => "Context complete",
AnalysisPhase::ScoringInProgress => "Computing debt scores",
AnalysisPhase::ScoringComplete => "Scoring complete",
AnalysisPhase::FilteringInProgress => "Filtering and ranking",
AnalysisPhase::Complete => "Complete",
}
}
pub fn tui_stage_index(&self) -> Option<usize> {
match self {
AnalysisPhase::CallGraphBuilding | AnalysisPhase::CallGraphComplete => Some(1),
AnalysisPhase::CoverageLoading | AnalysisPhase::CoverageComplete => Some(2),
AnalysisPhase::PurityAnalyzing | AnalysisPhase::PurityComplete => Some(3),
AnalysisPhase::ContextLoading | AnalysisPhase::ContextComplete => Some(4),
AnalysisPhase::ScoringInProgress
| AnalysisPhase::ScoringComplete
| AnalysisPhase::FilteringInProgress => Some(5),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnalysisConfig {
pub project_path: PathBuf,
pub coverage_file: Option<PathBuf>,
pub enable_context: bool,
pub context_providers: Option<Vec<String>>,
pub disable_context: Option<Vec<String>>,
pub parallel: bool,
pub jobs: usize,
pub multi_pass: bool,
pub show_attribution: bool,
pub no_aggregation: bool,
pub aggregation_method: Option<String>,
pub min_problematic: Option<usize>,
pub no_god_object: bool,
pub suppress_coverage_tip: bool,
pub verbose_macro_warnings: bool,
pub show_macro_stats: bool,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct AnalysisResults {
pub metrics: Option<Vec<FunctionMetrics>>,
#[serde(skip)]
pub call_graph: Option<CallGraph>,
#[serde(skip)]
pub framework_exclusions: Option<HashSet<crate::priority::call_graph::FunctionId>>,
#[serde(skip)]
pub function_pointer_used_functions: Option<HashSet<crate::priority::call_graph::FunctionId>>,
#[serde(skip)]
pub coverage: Option<LcovData>,
pub enriched_metrics: Option<Vec<FunctionMetrics>>,
#[serde(skip)]
pub risk_analyzer: Option<crate::risk::RiskAnalyzer>,
#[serde(skip)]
pub unified_analysis: Option<crate::priority::UnifiedAnalysis>,
}
impl std::fmt::Debug for AnalysisResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AnalysisResults")
.field("metrics", &self.metrics.as_ref().map(|m| m.len()))
.field("call_graph", &self.call_graph.is_some())
.field(
"framework_exclusions",
&self.framework_exclusions.as_ref().map(|s| s.len()),
)
.field(
"function_pointer_used_functions",
&self
.function_pointer_used_functions
.as_ref()
.map(|s| s.len()),
)
.field("coverage", &self.coverage.is_some())
.field(
"enriched_metrics",
&self.enriched_metrics.as_ref().map(|m| m.len()),
)
.field("risk_analyzer", &self.risk_analyzer.is_some())
.field("unified_analysis", &self.unified_analysis.is_some())
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisState {
pub phase: AnalysisPhase,
pub config: AnalysisConfig,
#[serde(skip)]
pub results: AnalysisResults,
pub last_transition: Option<chrono::DateTime<chrono::Utc>>,
}
impl AnalysisState {
pub fn new(config: AnalysisConfig) -> Self {
Self {
phase: AnalysisPhase::Initialized,
config,
results: AnalysisResults::default(),
last_transition: Some(chrono::Utc::now()),
}
}
pub fn with_metrics(config: AnalysisConfig, metrics: Vec<FunctionMetrics>) -> Self {
let mut state = Self::new(config);
state.results.metrics = Some(metrics);
state
}
pub fn transition_to(&mut self, new_phase: AnalysisPhase) {
self.phase = new_phase;
self.last_transition = Some(chrono::Utc::now());
}
pub fn is_complete(&self) -> bool {
self.phase.is_final()
}
pub fn progress_percent(&self) -> f64 {
match self.phase {
AnalysisPhase::Initialized => 0.0,
AnalysisPhase::CallGraphBuilding => 0.10,
AnalysisPhase::CallGraphComplete => 0.29,
AnalysisPhase::CoverageLoading => 0.35,
AnalysisPhase::CoverageComplete => 0.43,
AnalysisPhase::PurityAnalyzing => 0.50,
AnalysisPhase::PurityComplete => 0.57,
AnalysisPhase::ContextLoading => 0.65,
AnalysisPhase::ContextComplete => 0.83,
AnalysisPhase::ScoringInProgress => 0.90,
AnalysisPhase::ScoringComplete => 0.95,
AnalysisPhase::FilteringInProgress => 0.98,
AnalysisPhase::Complete => 1.0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_is_final() {
assert!(!AnalysisPhase::Initialized.is_final());
assert!(!AnalysisPhase::CallGraphBuilding.is_final());
assert!(!AnalysisPhase::PurityComplete.is_final());
assert!(AnalysisPhase::Complete.is_final());
}
#[test]
fn test_state_new() {
let config = AnalysisConfig {
project_path: PathBuf::from("src"),
..Default::default()
};
let state = AnalysisState::new(config);
assert_eq!(state.phase, AnalysisPhase::Initialized);
assert!(!state.is_complete());
assert!(state.last_transition.is_some());
}
#[test]
fn test_state_with_metrics() {
let config = AnalysisConfig::default();
let metrics = vec![FunctionMetrics::new(
"test".to_string(),
PathBuf::from("test.rs"),
1,
)];
let state = AnalysisState::with_metrics(config, metrics);
assert!(state.results.metrics.is_some());
assert_eq!(state.results.metrics.as_ref().unwrap().len(), 1);
}
#[test]
fn test_state_transition() {
let mut state = AnalysisState::new(AnalysisConfig::default());
let initial_time = state.last_transition;
std::thread::sleep(std::time::Duration::from_millis(10));
state.transition_to(AnalysisPhase::CallGraphBuilding);
assert_eq!(state.phase, AnalysisPhase::CallGraphBuilding);
assert!(state.last_transition > initial_time);
}
#[test]
fn test_progress_percent_ordering() {
let phases = [
AnalysisPhase::Initialized,
AnalysisPhase::CallGraphBuilding,
AnalysisPhase::CallGraphComplete,
AnalysisPhase::CoverageLoading,
AnalysisPhase::CoverageComplete,
AnalysisPhase::PurityAnalyzing,
AnalysisPhase::PurityComplete,
AnalysisPhase::ContextLoading,
AnalysisPhase::ContextComplete,
AnalysisPhase::ScoringInProgress,
AnalysisPhase::ScoringComplete,
AnalysisPhase::FilteringInProgress,
AnalysisPhase::Complete,
];
let mut prev_progress = -1.0;
for phase in phases {
let mut state = AnalysisState::new(AnalysisConfig::default());
state.phase = phase;
let current_progress = state.progress_percent();
assert!(
current_progress >= prev_progress,
"Progress should increase monotonically: {:?} ({}) < {}",
phase,
current_progress,
prev_progress
);
prev_progress = current_progress;
}
}
#[test]
fn test_tui_stage_indices() {
assert_eq!(AnalysisPhase::CallGraphBuilding.tui_stage_index(), Some(1));
assert_eq!(AnalysisPhase::CoverageLoading.tui_stage_index(), Some(2));
assert_eq!(AnalysisPhase::PurityAnalyzing.tui_stage_index(), Some(3));
assert_eq!(AnalysisPhase::ContextLoading.tui_stage_index(), Some(4));
assert_eq!(AnalysisPhase::ScoringInProgress.tui_stage_index(), Some(5));
assert_eq!(AnalysisPhase::Initialized.tui_stage_index(), None);
assert_eq!(AnalysisPhase::Complete.tui_stage_index(), None);
}
}