use super::format::round_ratio;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileDependencies {
pub afferent_coupling: usize,
pub efferent_coupling: usize,
pub instability: f64,
pub total_coupling: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub top_dependents: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub top_dependencies: Vec<String>,
pub coupling_classification: CouplingClassification,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CouplingClassification {
WellTestedCore,
StableFoundation,
UnstableHighCoupling,
StableCore,
UtilityModule,
ArchitecturalHub,
LeafModule,
Isolated,
HighlyCoupled,
}
impl CouplingClassification {
pub fn is_architectural_concern(&self) -> bool {
matches!(
self,
Self::UnstableHighCoupling | Self::ArchitecturalHub | Self::HighlyCoupled
)
}
pub fn is_stable_by_design(&self) -> bool {
matches!(
self,
Self::WellTestedCore | Self::StableFoundation | Self::StableCore
)
}
pub fn score_multiplier(&self) -> f64 {
match self {
Self::WellTestedCore => 0.2,
Self::StableFoundation => 0.5,
Self::StableCore => 0.6,
Self::LeafModule => 0.8,
Self::Isolated => 0.9,
Self::UtilityModule => 1.0,
Self::ArchitecturalHub => 1.0,
Self::HighlyCoupled => 1.2,
Self::UnstableHighCoupling => 1.5,
}
}
}
impl std::fmt::Display for CouplingClassification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CouplingClassification::WellTestedCore => write!(f, "Well-Tested Core"),
CouplingClassification::StableFoundation => write!(f, "Stable Foundation"),
CouplingClassification::UnstableHighCoupling => write!(f, "Unstable High Coupling"),
CouplingClassification::StableCore => write!(f, "Stable Core"),
CouplingClassification::UtilityModule => write!(f, "Utility Module"),
CouplingClassification::ArchitecturalHub => write!(f, "Architectural Hub"),
CouplingClassification::LeafModule => write!(f, "Leaf Module"),
CouplingClassification::Isolated => write!(f, "Isolated"),
CouplingClassification::HighlyCoupled => write!(f, "Highly Coupled"),
}
}
}
pub fn classify_coupling(afferent: usize, efferent: usize) -> CouplingClassification {
let total = afferent + efferent;
let instability = if total > 0 {
efferent as f64 / total as f64
} else {
0.0
};
if total > 15 {
return CouplingClassification::HighlyCoupled;
}
if total < 3 {
return CouplingClassification::Isolated;
}
if instability < 0.3 && afferent >= 3 {
return CouplingClassification::StableCore;
}
if instability > 0.7 && afferent <= 2 {
return CouplingClassification::LeafModule;
}
CouplingClassification::UtilityModule
}
pub fn calculate_instability(afferent: usize, efferent: usize) -> f64 {
let total = afferent + efferent;
if total > 0 {
efferent as f64 / total as f64
} else {
0.0
}
}
pub fn classify_coupling_pattern(
instability: f64,
production_caller_count: usize,
test_caller_count: usize,
callee_count: usize,
) -> CouplingClassification {
let total_callers = production_caller_count + test_caller_count;
let test_ratio = if total_callers > 0 {
test_caller_count as f64 / total_callers as f64
} else {
0.0
};
match (
instability,
total_callers,
test_ratio,
production_caller_count,
callee_count,
) {
(i, c, t, _, _) if i <= 0.35 && c > 5 && t > 0.7 => CouplingClassification::WellTestedCore,
(i, _, _, p, _) if i <= 0.35 && p > 10 => CouplingClassification::StableFoundation,
(i, c, _, _, _) if i <= 0.35 && c > 5 => CouplingClassification::StableCore,
(i, _, _, p, _) if i > 0.7 && p > 5 => CouplingClassification::UnstableHighCoupling,
(i, c, _, _, _) if i > 0.3 && i < 0.7 && c > 10 => CouplingClassification::ArchitecturalHub,
(_, c, _, _, callees) if c < 3 && callees > 5 => CouplingClassification::LeafModule,
(_, c, _, _, callees) if c < 3 && callees < 3 => CouplingClassification::Isolated,
_ => CouplingClassification::LeafModule,
}
}
pub fn calculate_architectural_dependency_factor(
production_upstream_count: usize,
test_upstream_count: usize,
downstream_count: usize,
) -> (f64, CouplingClassification) {
let incoming = production_upstream_count + test_upstream_count;
let instability = calculate_instability(incoming, downstream_count);
let classification = classify_coupling_pattern(
instability,
production_upstream_count,
test_upstream_count,
downstream_count,
);
let base_factor = if production_upstream_count == 0 {
0.0
} else {
(1.0 + production_upstream_count as f64).ln() / 1.5
};
let adjusted_factor = base_factor * classification.score_multiplier();
(adjusted_factor.min(10.0), classification)
}
pub fn build_file_dependencies(
metrics: &crate::priority::FileDebtMetrics,
) -> Option<FileDependencies> {
let has_coupling_data = metrics.afferent_coupling > 0
|| metrics.efferent_coupling > 0
|| !metrics.dependents.is_empty()
|| !metrics.dependencies_list.is_empty();
if !has_coupling_data {
return None;
}
let afferent = metrics.afferent_coupling;
let efferent = metrics.efferent_coupling;
Some(FileDependencies {
afferent_coupling: afferent,
efferent_coupling: efferent,
instability: round_ratio(metrics.instability),
total_coupling: afferent + efferent,
top_dependents: metrics.dependents.iter().take(5).cloned().collect(),
top_dependencies: metrics.dependencies_list.iter().take(5).cloned().collect(),
coupling_classification: classify_coupling(afferent, efferent),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_instability_balanced() {
let instability = calculate_instability(5, 5);
assert!((instability - 0.5).abs() < 0.01);
}
#[test]
fn test_calculate_instability_stable() {
let instability = calculate_instability(10, 0);
assert!((instability - 0.0).abs() < 0.01);
}
#[test]
fn test_calculate_instability_unstable() {
let instability = calculate_instability(0, 10);
assert!((instability - 1.0).abs() < 0.01);
}
#[test]
fn test_calculate_instability_zero_coupling() {
let instability = calculate_instability(0, 0);
assert!((instability - 0.0).abs() < 0.01);
}
#[test]
fn test_classify_coupling_stable_core() {
let classification = classify_coupling(8, 2); assert_eq!(classification, CouplingClassification::StableCore);
}
#[test]
fn test_classify_coupling_leaf_module() {
let classification = classify_coupling(1, 8); assert_eq!(classification, CouplingClassification::LeafModule);
}
#[test]
fn test_classify_coupling_utility_module() {
let classification = classify_coupling(5, 5);
assert_eq!(classification, CouplingClassification::UtilityModule);
}
#[test]
fn test_classify_coupling_isolated() {
let classification = classify_coupling(1, 1);
assert_eq!(classification, CouplingClassification::Isolated);
}
#[test]
fn test_classify_coupling_highly_coupled() {
let classification = classify_coupling(10, 10);
assert_eq!(classification, CouplingClassification::HighlyCoupled);
}
#[test]
fn test_coupling_classification_display() {
assert_eq!(
format!("{}", CouplingClassification::StableCore),
"Stable Core"
);
assert_eq!(
format!("{}", CouplingClassification::LeafModule),
"Leaf Module"
);
assert_eq!(format!("{}", CouplingClassification::Isolated), "Isolated");
assert_eq!(
format!("{}", CouplingClassification::HighlyCoupled),
"Highly Coupled"
);
}
#[test]
fn test_file_dependencies_serialization() {
let deps = FileDependencies {
afferent_coupling: 5,
efferent_coupling: 3,
instability: 0.375,
total_coupling: 8,
top_dependents: vec!["main.rs".to_string(), "lib.rs".to_string()],
top_dependencies: vec!["std".to_string()],
coupling_classification: CouplingClassification::UtilityModule,
};
let json = serde_json::to_string(&deps).unwrap();
assert!(json.contains("\"afferent_coupling\":5"));
assert!(json.contains("\"efferent_coupling\":3"));
assert!(json.contains("\"instability\":0.375"));
assert!(json.contains("\"total_coupling\":8"));
assert!(json.contains("\"top_dependents\":[\"main.rs\",\"lib.rs\"]"));
assert!(json.contains("\"top_dependencies\":[\"std\"]"));
assert!(json.contains("\"coupling_classification\":\"utility_module\""));
}
#[test]
fn test_file_dependencies_empty_lists_not_serialized() {
let deps = FileDependencies {
afferent_coupling: 0,
efferent_coupling: 0,
instability: 0.0,
total_coupling: 0,
top_dependents: vec![],
top_dependencies: vec![],
coupling_classification: CouplingClassification::Isolated,
};
let json = serde_json::to_string(&deps).unwrap();
assert!(!json.contains("\"top_dependents\""));
assert!(!json.contains("\"top_dependencies\""));
}
#[test]
fn test_well_tested_core_classification() {
let classification = classify_coupling_pattern(
0.2, 5, 85, 10, );
assert_eq!(classification, CouplingClassification::WellTestedCore);
}
#[test]
fn test_stable_foundation_classification() {
let classification = classify_coupling_pattern(
0.2, 15, 5, 10, );
assert_eq!(classification, CouplingClassification::StableFoundation);
}
#[test]
fn test_stable_core_classification_with_architecture() {
let classification = classify_coupling_pattern(
0.25, 6, 2, 5, );
assert_eq!(classification, CouplingClassification::StableCore);
}
#[test]
fn test_unstable_high_coupling_classification() {
let classification = classify_coupling_pattern(
0.8, 15, 5, 80, );
assert_eq!(classification, CouplingClassification::UnstableHighCoupling);
}
#[test]
fn test_architectural_hub_classification() {
let classification = classify_coupling_pattern(
0.5, 8, 5, 13, );
assert_eq!(classification, CouplingClassification::ArchitecturalHub);
}
#[test]
fn test_leaf_module_classification_with_architecture() {
let classification = classify_coupling_pattern(
0.9, 1, 1, 10, );
assert_eq!(classification, CouplingClassification::LeafModule);
}
#[test]
fn test_isolated_classification_with_architecture() {
let classification = classify_coupling_pattern(
0.5, 1, 0, 2, );
assert_eq!(classification, CouplingClassification::Isolated);
}
#[test]
fn test_score_multipliers() {
assert!(CouplingClassification::WellTestedCore.score_multiplier() < 0.5);
assert!(CouplingClassification::StableFoundation.score_multiplier() < 1.0);
assert!(CouplingClassification::StableCore.score_multiplier() < 1.0);
assert!(CouplingClassification::UnstableHighCoupling.score_multiplier() > 1.0);
assert!(CouplingClassification::HighlyCoupled.score_multiplier() > 1.0);
assert!((CouplingClassification::UtilityModule.score_multiplier() - 1.0).abs() < 0.01);
}
#[test]
fn test_is_stable_by_design() {
assert!(CouplingClassification::WellTestedCore.is_stable_by_design());
assert!(CouplingClassification::StableFoundation.is_stable_by_design());
assert!(CouplingClassification::StableCore.is_stable_by_design());
assert!(!CouplingClassification::UnstableHighCoupling.is_stable_by_design());
assert!(!CouplingClassification::LeafModule.is_stable_by_design());
assert!(!CouplingClassification::ArchitecturalHub.is_stable_by_design());
}
#[test]
fn test_is_architectural_concern() {
assert!(CouplingClassification::UnstableHighCoupling.is_architectural_concern());
assert!(CouplingClassification::ArchitecturalHub.is_architectural_concern());
assert!(CouplingClassification::HighlyCoupled.is_architectural_concern());
assert!(!CouplingClassification::WellTestedCore.is_architectural_concern());
assert!(!CouplingClassification::StableFoundation.is_architectural_concern());
assert!(!CouplingClassification::LeafModule.is_architectural_concern());
}
#[test]
fn test_architectural_dependency_factor_well_tested() {
let (factor, classification) = calculate_architectural_dependency_factor(
5, 85, 10, );
assert_eq!(classification, CouplingClassification::WellTestedCore);
assert!(factor < 1.0, "Well-tested core should have reduced factor");
}
#[test]
fn test_architectural_dependency_factor_unstable() {
let (factor, classification) = calculate_architectural_dependency_factor(
10, 2, 50, );
assert_eq!(classification, CouplingClassification::UnstableHighCoupling);
assert!(
factor > 2.0,
"Unstable high coupling should have increased factor"
);
}
#[test]
fn test_coupling_classification_new_display() {
assert_eq!(
format!("{}", CouplingClassification::WellTestedCore),
"Well-Tested Core"
);
assert_eq!(
format!("{}", CouplingClassification::StableFoundation),
"Stable Foundation"
);
assert_eq!(
format!("{}", CouplingClassification::UnstableHighCoupling),
"Unstable High Coupling"
);
assert_eq!(
format!("{}", CouplingClassification::ArchitecturalHub),
"Architectural Hub"
);
}
#[test]
fn test_overflow_rs_scenario() {
let instability = 0.26;
let production_callers = 5;
let test_callers = 85;
let callees = 35;
let classification =
classify_coupling_pattern(instability, production_callers, test_callers, callees);
assert_eq!(classification, CouplingClassification::WellTestedCore);
assert!(classification.is_stable_by_design());
assert!(classification.score_multiplier() < 0.5);
}
}