use super::cohesion::CohesionSummary;
use super::file_item::FileDebtItemOutput;
use super::func_item::FunctionDebtItemOutput;
use crate::priority::DebtItem;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedOutput {
pub format_version: String,
pub metadata: OutputMetadata,
pub summary: DebtSummary,
pub items: Vec<UnifiedDebtItemOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputMetadata {
pub debtmap_version: String,
pub generated_at: String,
pub project_root: Option<PathBuf>,
pub analysis_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DebtSummary {
pub total_items: usize,
pub total_debt_score: f64,
pub debt_density: f64,
pub total_loc: usize,
pub by_type: TypeBreakdown,
pub by_category: std::collections::HashMap<String, usize>,
pub score_distribution: ScoreDistribution,
#[serde(skip_serializing_if = "Option::is_none")]
pub cohesion: Option<CohesionSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeBreakdown {
#[serde(rename = "File")]
pub file: usize,
#[serde(rename = "Function")]
pub function: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScoreDistribution {
pub critical: usize, pub high: usize, pub medium: usize, pub low: usize, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum UnifiedDebtItemOutput {
File(Box<FileDebtItemOutput>),
Function(Box<FunctionDebtItemOutput>),
}
impl UnifiedDebtItemOutput {
#[cfg(debug_assertions)]
pub fn assert_invariants(&self) {
match self {
UnifiedDebtItemOutput::File(f) => f.assert_invariants(),
UnifiedDebtItemOutput::Function(f) => f.assert_invariants(),
}
}
#[cfg(not(debug_assertions))]
#[inline]
pub fn assert_invariants(&self) {}
pub fn score(&self) -> f64 {
match self {
UnifiedDebtItemOutput::File(f) => f.score,
UnifiedDebtItemOutput::Function(f) => f.score,
}
}
pub fn from_debt_item(item: &DebtItem, include_scoring_details: bool) -> Self {
Self::from_debt_item_with_call_graph(item, include_scoring_details, None)
}
pub fn from_debt_item_with_call_graph(
item: &DebtItem,
include_scoring_details: bool,
call_graph: Option<&crate::priority::CallGraph>,
) -> Self {
use super::cohesion::build_cohesion_output;
match item {
DebtItem::File(file_item) => {
let cohesion = call_graph.and_then(|cg| {
crate::organization::calculate_file_cohesion(&file_item.metrics.path, cg)
.map(|r| build_cohesion_output(&r))
});
UnifiedDebtItemOutput::File(Box::new(
FileDebtItemOutput::from_file_item_with_cohesion(
file_item,
include_scoring_details,
cohesion,
),
))
}
DebtItem::Function(func_item) => UnifiedDebtItemOutput::Function(Box::new(
FunctionDebtItemOutput::from_function_item(func_item, include_scoring_details),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::unified::dependencies::Dependencies;
use crate::output::unified::file_item::{FileImpactOutput, FileMetricsOutput};
use crate::output::unified::func_item::{FunctionImpactOutput, FunctionMetricsOutput};
use crate::output::unified::location::UnifiedLocation;
use crate::output::unified::priority::Priority;
use crate::priority::{DebtType, FunctionRole};
fn create_test_function_item(
file: &str,
line: usize,
function: &str,
score: f64,
) -> UnifiedDebtItemOutput {
UnifiedDebtItemOutput::Function(Box::new(FunctionDebtItemOutput {
score,
category: "TestCategory".to_string(),
priority: Priority::from_score(score),
location: UnifiedLocation {
file: file.to_string(),
line: Some(line),
function: Some(function.to_string()),
file_context_label: None,
},
metrics: FunctionMetricsOutput {
cyclomatic_complexity: 10,
cognitive_complexity: 15,
length: 50,
nesting_depth: 3,
coverage: Some(0.8),
uncovered_lines: None,
entropy_score: None,
..Default::default()
},
debt_type: DebtType::ComplexityHotspot {
cyclomatic: 10,
cognitive: 15,
},
function_role: FunctionRole::PureLogic,
purity_analysis: None,
dependencies: Dependencies {
upstream_count: 0,
downstream_count: 0,
upstream_callers: vec![],
downstream_callees: vec![],
upstream_production_callers: vec![],
upstream_test_callers: vec![],
production_blast_radius: 0,
..Default::default()
},
impact: FunctionImpactOutput {
coverage_improvement: 0.0,
complexity_reduction: 0.0,
risk_reduction: 0.0,
},
scoring_details: None,
adjusted_complexity: None,
complexity_pattern: None,
pattern_type: None,
pattern_confidence: None,
pattern_details: None,
context: None,
git_history: None,
}))
}
fn create_test_file_item(file: &str, score: f64) -> UnifiedDebtItemOutput {
UnifiedDebtItemOutput::File(Box::new(FileDebtItemOutput {
score,
category: "Architecture".to_string(),
priority: Priority::from_score(score),
location: UnifiedLocation {
file: file.to_string(),
line: None,
function: None,
file_context_label: None,
},
metrics: FileMetricsOutput {
lines: 500,
functions: 20,
classes: 1,
avg_complexity: 8.0,
max_complexity: 15,
total_complexity: 160,
coverage: 0.7,
uncovered_lines: 150,
distribution: None,
},
debt_type: None,
god_object_indicators: None,
dependencies: None,
anti_patterns: None,
cohesion: None,
impact: FileImpactOutput {
complexity_reduction: 10.0,
maintainability_improvement: 0.2,
test_effort: 5.0,
},
scoring_details: None,
}))
}
#[test]
fn test_unified_debt_item_output_score() {
let func_item = create_test_function_item("a.rs", 10, "foo", 75.5);
let file_item = create_test_file_item("b.rs", 42.0);
assert_eq!(func_item.score(), 75.5);
assert_eq!(file_item.score(), 42.0);
}
}