use super::anti_patterns::{build_anti_patterns, AntiPatternOutput};
use super::cohesion::CohesionOutput;
use super::coupling::{build_file_dependencies, FileDependencies};
use super::format::{assert_ratio_invariants, assert_score_invariants};
use super::format::{round_ratio, round_score};
use super::location::UnifiedLocation;
use super::priority::{assert_priority_invariants, Priority};
use crate::priority::{DebtType, FileDebtItem};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDebtItemOutput {
pub score: f64,
pub category: String,
pub priority: Priority,
pub location: UnifiedLocation,
pub metrics: FileMetricsOutput,
#[serde(skip_serializing_if = "Option::is_none")]
pub debt_type: Option<DebtType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub god_object_indicators: Option<crate::priority::GodObjectIndicators>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<FileDependencies>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anti_patterns: Option<AntiPatternOutput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cohesion: Option<CohesionOutput>,
pub impact: FileImpactOutput,
#[serde(skip_serializing_if = "Option::is_none")]
pub scoring_details: Option<FileScoringDetails>,
}
impl FileDebtItemOutput {
#[cfg(debug_assertions)]
pub fn assert_invariants(&self) {
assert_score_invariants(self.score, "file.score");
assert_priority_invariants(&self.priority, self.score);
assert_ratio_invariants(self.metrics.coverage, "file.metrics.coverage");
if let Some(ref cohesion) = self.cohesion {
assert_ratio_invariants(cohesion.score, "file.cohesion.score");
}
if let Some(ref deps) = self.dependencies {
assert_ratio_invariants(deps.instability, "file.dependencies.instability");
}
}
#[cfg(not(debug_assertions))]
#[inline]
pub fn assert_invariants(&self) {}
pub fn from_file_item_with_cohesion(
item: &FileDebtItem,
include_scoring_details: bool,
cohesion: Option<CohesionOutput>,
) -> Self {
let score = item.score;
let dependencies = build_file_dependencies(&item.metrics);
let anti_patterns = build_anti_patterns(&item.metrics);
let rounded_score = round_score(score);
let rounded_coverage = round_ratio(item.metrics.coverage_percent);
let rounded_avg_complexity = round_score(item.metrics.avg_complexity);
let cohesion = cohesion.map(|mut c| {
c.score = round_ratio(c.score);
c
});
let debt_type = derive_file_debt_type(item);
FileDebtItemOutput {
score: rounded_score,
category: categorize_file_debt(item),
priority: Priority::from_score(rounded_score),
location: UnifiedLocation {
file: item.metrics.path.to_string_lossy().to_string(),
line: None,
function: None,
file_context_label: None, },
metrics: FileMetricsOutput {
lines: item.metrics.total_lines,
functions: item.metrics.function_count,
classes: item.metrics.class_count,
avg_complexity: rounded_avg_complexity,
max_complexity: item.metrics.max_complexity,
total_complexity: item.metrics.total_complexity,
coverage: rounded_coverage,
uncovered_lines: item.metrics.uncovered_lines,
distribution: None, },
debt_type,
god_object_indicators: item.metrics.god_object_analysis.clone().map(|a| a.into()),
dependencies,
anti_patterns,
cohesion,
impact: FileImpactOutput {
complexity_reduction: round_ratio(item.impact.complexity_reduction),
maintainability_improvement: round_ratio(item.impact.maintainability_improvement),
test_effort: round_ratio(item.impact.test_effort),
},
scoring_details: if include_scoring_details {
Some(calculate_file_scoring_details(item))
} else {
None
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetricsOutput {
pub lines: usize,
pub functions: usize,
pub classes: usize,
pub avg_complexity: f64,
pub max_complexity: u32,
pub total_complexity: u32,
pub coverage: f64,
pub uncovered_lines: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub distribution: Option<DistributionMetricsOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionMetricsOutput {
pub function_count: usize,
pub max_function_complexity: u32,
pub avg_function_complexity: f64,
pub median_complexity: u32,
pub exceeding_threshold: usize,
pub distribution_type: String,
pub classification_explanation: String,
pub production_loc: usize,
pub test_loc: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileImpactOutput {
pub complexity_reduction: f64,
pub maintainability_improvement: f64,
pub test_effort: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileScoringDetails {
pub file_size_score: f64,
pub function_count_score: f64,
pub complexity_score: f64,
pub coverage_penalty: f64,
}
fn categorize_file_debt(_item: &FileDebtItem) -> String {
"Architecture".to_string()
}
fn derive_file_debt_type(item: &FileDebtItem) -> Option<DebtType> {
if let Some(ref analysis) = item.metrics.god_object_analysis {
if analysis.is_god_object {
return Some(DebtType::GodObject {
methods: analysis.method_count as u32,
fields: if analysis.field_count > 0 {
Some(analysis.field_count as u32)
} else {
None
},
responsibilities: analysis.responsibility_count as u32,
god_object_score: analysis.god_object_score,
lines: analysis.lines_of_code as u32,
});
}
}
let is_large_file = item.metrics.total_lines > 2000;
let has_many_functions = item.metrics.function_count > 50;
if is_large_file || has_many_functions {
let god_object_score = (item.metrics.function_count as f64 / 50.0).min(2.0) * 100.0;
return Some(DebtType::GodObject {
methods: item.metrics.function_count as u32,
fields: None,
responsibilities: 0,
god_object_score,
lines: item.metrics.total_lines as u32,
});
}
None
}
fn calculate_file_scoring_details(item: &FileDebtItem) -> FileScoringDetails {
let file_size_score = (item.metrics.total_lines as f64 / 100.0).min(50.0);
let function_count_score = (item.metrics.function_count as f64 / 2.0).min(30.0);
let complexity_score = (item.metrics.avg_complexity * 2.0).min(20.0);
let coverage_penalty = (1.0 - item.metrics.coverage_percent) * 20.0;
FileScoringDetails {
file_size_score,
function_count_score,
complexity_score,
coverage_penalty,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_debt_item_serialization_roundtrip() {
use super::super::cohesion::CohesionClassification;
let item = FileDebtItemOutput {
score: 75.25,
category: "Architecture".to_string(),
priority: Priority::from_score(75.25),
location: UnifiedLocation {
file: "big_file.rs".to_string(),
line: None,
function: None,
file_context_label: None,
},
metrics: FileMetricsOutput {
lines: 500,
functions: 25,
classes: 0,
avg_complexity: 8.5,
max_complexity: 15,
total_complexity: 212,
coverage: 0.65,
uncovered_lines: 175,
distribution: None,
},
debt_type: Some(DebtType::GodObject {
methods: 25,
fields: None,
responsibilities: 5,
god_object_score: 75.25,
lines: 500,
}),
god_object_indicators: None,
dependencies: None,
anti_patterns: None,
cohesion: Some(CohesionOutput {
score: 0.45,
internal_calls: 10,
external_calls: 15,
classification: CohesionClassification::Medium,
functions_analyzed: 25,
}),
impact: FileImpactOutput {
complexity_reduction: 0.3,
maintainability_improvement: 0.4,
test_effort: 0.5,
},
scoring_details: None,
};
let json = serde_json::to_string(&item).unwrap();
let deserialized: FileDebtItemOutput = serde_json::from_str(&json).unwrap();
assert_eq!(item.score, deserialized.score);
assert!(matches!(deserialized.priority, Priority::High));
assert_eq!(item.metrics.coverage, deserialized.metrics.coverage);
}
}