use std::collections::HashMap;
use crate::metrics::{CouplingMetrics, Distance, IntegrationStrength, ProjectMetrics, Volatility};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Low => write!(f, "Low"),
Severity::Medium => write!(f, "Medium"),
Severity::High => write!(f, "High"),
Severity::Critical => write!(f, "Critical"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IssueType {
GlobalComplexity,
CascadingChangeRisk,
InappropriateIntimacy,
HighEfferentCoupling,
HighAfferentCoupling,
UnnecessaryAbstraction,
CircularDependency,
ShallowModule,
PassThroughMethod,
HighCognitiveLoad,
GodModule,
PublicFieldExposure,
PrimitiveObsession,
}
impl std::fmt::Display for IssueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IssueType::GlobalComplexity => write!(f, "Global Complexity"),
IssueType::CascadingChangeRisk => write!(f, "Cascading Change Risk"),
IssueType::InappropriateIntimacy => write!(f, "Inappropriate Intimacy"),
IssueType::HighEfferentCoupling => write!(f, "High Efferent Coupling"),
IssueType::HighAfferentCoupling => write!(f, "High Afferent Coupling"),
IssueType::UnnecessaryAbstraction => write!(f, "Unnecessary Abstraction"),
IssueType::CircularDependency => write!(f, "Circular Dependency"),
IssueType::ShallowModule => write!(f, "Shallow Module"),
IssueType::PassThroughMethod => write!(f, "Pass-Through Method"),
IssueType::HighCognitiveLoad => write!(f, "High Cognitive Load"),
IssueType::GodModule => write!(f, "God Module"),
IssueType::PublicFieldExposure => write!(f, "Public Field Exposure"),
IssueType::PrimitiveObsession => write!(f, "Primitive Obsession"),
}
}
}
impl IssueType {
pub fn description(&self) -> &'static str {
match self {
IssueType::GlobalComplexity => {
"Strong coupling to distant components increases cognitive load and makes the system harder to understand and modify."
}
IssueType::CascadingChangeRisk => {
"Strongly coupling to volatile components means changes will cascade through the system, requiring updates in many places."
}
IssueType::InappropriateIntimacy => {
"Direct access to internal details (fields, private methods) across module boundaries violates encapsulation."
}
IssueType::HighEfferentCoupling => {
"A module depending on too many others is fragile and hard to test. Changes anywhere affect this module."
}
IssueType::HighAfferentCoupling => {
"A module that many others depend on is hard to change. Any modification risks breaking dependents."
}
IssueType::UnnecessaryAbstraction => {
"Using abstract interfaces for closely-related stable components may add complexity without benefit."
}
IssueType::CircularDependency => {
"Circular dependencies make it impossible to understand, test, or modify components in isolation."
}
IssueType::ShallowModule => {
"Interface complexity is close to implementation complexity. The module doesn't hide enough complexity behind a simple interface. (APOSD: Deep vs Shallow Modules)"
}
IssueType::PassThroughMethod => {
"Method only delegates to another method without adding significant functionality. Indicates unclear responsibility division. (APOSD: Pass-Through Methods)"
}
IssueType::HighCognitiveLoad => {
"Module requires too much knowledge to understand and modify. Too many public APIs, dependencies, or complex type signatures. (APOSD: Cognitive Load)"
}
IssueType::GodModule => {
"Module has too many responsibilities - too many functions, types, or implementations. Consider splitting into focused, cohesive modules. (SRP violation)"
}
IssueType::PublicFieldExposure => {
"Struct has public fields accessed from other modules. Consider using getter methods to reduce coupling and allow future implementation changes."
}
IssueType::PrimitiveObsession => {
"Function has many primitive parameters of the same type. Consider using newtype pattern (e.g., `struct UserId(u64)`) for type safety and clarity."
}
}
}
}
#[derive(Debug, Clone)]
pub struct CouplingIssue {
pub issue_type: IssueType,
pub severity: Severity,
pub source: String,
pub target: String,
pub description: String,
pub refactoring: RefactoringAction,
pub balance_score: f64,
}
#[derive(Debug, Clone)]
pub enum RefactoringAction {
IntroduceTrait {
suggested_name: String,
methods: Vec<String>,
},
MoveCloser { target_location: String },
ExtractAdapter {
adapter_name: String,
purpose: String,
},
SplitModule { suggested_modules: Vec<String> },
SimplifyAbstraction { direct_usage: String },
BreakCycle { suggested_direction: String },
StabilizeInterface { interface_name: String },
General { action: String },
AddGetters { fields: Vec<String> },
IntroduceNewtype {
suggested_name: String,
wrapped_type: String,
},
}
impl std::fmt::Display for RefactoringAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RefactoringAction::IntroduceTrait {
suggested_name,
methods,
} => {
write!(
f,
"Introduce trait `{}` with methods: {}",
suggested_name,
methods.join(", ")
)
}
RefactoringAction::MoveCloser { target_location } => {
write!(f, "Move component to `{}`", target_location)
}
RefactoringAction::ExtractAdapter {
adapter_name,
purpose,
} => {
write!(f, "Extract adapter `{}` to {}", adapter_name, purpose)
}
RefactoringAction::SplitModule { suggested_modules } => {
write!(f, "Split into modules: {}", suggested_modules.join(", "))
}
RefactoringAction::SimplifyAbstraction { direct_usage } => {
write!(f, "Replace with direct usage: {}", direct_usage)
}
RefactoringAction::BreakCycle {
suggested_direction,
} => {
write!(f, "Break cycle by {}", suggested_direction)
}
RefactoringAction::StabilizeInterface { interface_name } => {
write!(f, "Add stable interface `{}`", interface_name)
}
RefactoringAction::General { action } => {
write!(f, "{}", action)
}
RefactoringAction::AddGetters { fields } => {
write!(f, "Add getter methods for: {}", fields.join(", "))
}
RefactoringAction::IntroduceNewtype {
suggested_name,
wrapped_type,
} => {
write!(
f,
"Introduce newtype: `struct {}({});`",
suggested_name, wrapped_type
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct BalanceScore {
pub coupling: CouplingMetrics,
pub score: f64,
pub alignment: f64,
pub volatility_impact: f64,
pub interpretation: BalanceInterpretation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BalanceInterpretation {
Balanced,
Acceptable,
NeedsReview,
NeedsRefactoring,
Critical,
}
impl std::fmt::Display for BalanceInterpretation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BalanceInterpretation::Balanced => write!(f, "Balanced"),
BalanceInterpretation::Acceptable => write!(f, "Acceptable"),
BalanceInterpretation::NeedsReview => write!(f, "Needs Review"),
BalanceInterpretation::NeedsRefactoring => write!(f, "Needs Refactoring"),
BalanceInterpretation::Critical => write!(f, "Critical"),
}
}
}
impl BalanceScore {
pub fn calculate(coupling: &CouplingMetrics) -> Self {
let strength = coupling.strength_value();
let distance = coupling.distance_value();
let volatility = coupling.volatility_value();
let alignment = 1.0 - (strength - (1.0 - distance)).abs();
let volatility_penalty = volatility * strength;
let volatility_impact = 1.0 - volatility_penalty;
let score = alignment * volatility_impact;
let interpretation = match score {
s if s >= 0.8 => BalanceInterpretation::Balanced,
s if s >= 0.6 => BalanceInterpretation::Acceptable,
s if s >= 0.4 => BalanceInterpretation::NeedsReview,
s if s >= 0.2 => BalanceInterpretation::NeedsRefactoring,
_ => BalanceInterpretation::Critical,
};
Self {
coupling: coupling.clone(),
score,
alignment,
volatility_impact,
interpretation,
}
}
pub fn is_balanced(&self) -> bool {
matches!(
self.interpretation,
BalanceInterpretation::Balanced | BalanceInterpretation::Acceptable
)
}
pub fn needs_refactoring(&self) -> bool {
matches!(
self.interpretation,
BalanceInterpretation::NeedsRefactoring | BalanceInterpretation::Critical
)
}
}
#[derive(Debug, Clone)]
pub struct IssueThresholds {
pub strong_coupling: f64,
pub far_distance: f64,
pub high_volatility: f64,
pub max_dependencies: usize,
pub max_dependents: usize,
pub max_functions: usize,
pub max_types: usize,
pub max_impls: usize,
pub min_primitive_params: usize,
pub strict_mode: bool,
pub japanese: bool,
pub exclude_tests: bool,
pub prelude_module_count: usize,
}
impl Default for IssueThresholds {
fn default() -> Self {
Self {
strong_coupling: 0.75, far_distance: 0.5, high_volatility: 0.75, max_dependencies: 20, max_dependents: 30, max_functions: 30, max_types: 15, max_impls: 20, min_primitive_params: 3, strict_mode: true, japanese: false, exclude_tests: false, prelude_module_count: 0, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrateStability {
Fundamental,
Stable,
Infrastructure,
Normal,
}
pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
match base_name {
"std" | "core" | "alloc" => CrateStability::Fundamental,
"serde" | "serde_json" | "serde_yaml" | "toml" | "thiserror" | "anyhow" | "log" | "chrono" | "time" | "uuid" | "regex" | "lazy_static" | "once_cell" | "bytes" | "memchr" | "itertools" | "derive_more" | "strum" => CrateStability::Stable,
"tokio" | "async-std" | "smol" | "async-trait" | "futures" | "futures-util" | "tracing" | "tracing-subscriber" | "tracing-opentelemetry" | "opentelemetry" | "opentelemetry-otlp" | "opentelemetry_sdk" |
"hyper" | "reqwest" | "http" | "tonic" | "prost" | "sqlx" | "diesel" | "sea-orm" | "clap" | "structopt" => CrateStability::Infrastructure,
_ => CrateStability::Normal,
}
}
pub fn should_skip_crate(crate_name: &str) -> bool {
matches!(
classify_crate_stability(crate_name),
CrateStability::Fundamental
)
}
pub fn should_reduce_severity(crate_name: &str) -> bool {
matches!(
classify_crate_stability(crate_name),
CrateStability::Stable | CrateStability::Infrastructure
)
}
pub fn is_external_crate(target: &str, source: &str) -> bool {
let target_prefix = target.split("::").next().unwrap_or(target);
let source_prefix = source.split("::").next().unwrap_or(source);
if target_prefix == source_prefix {
return false;
}
let stability = classify_crate_stability(target);
matches!(
stability,
CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
)
}
pub fn identify_issues(coupling: &CouplingMetrics) -> Vec<CouplingIssue> {
identify_issues_with_thresholds(coupling, &IssueThresholds::default())
}
pub fn identify_issues_with_thresholds(
coupling: &CouplingMetrics,
_thresholds: &IssueThresholds,
) -> Vec<CouplingIssue> {
let mut issues = Vec::new();
if coupling.distance == Distance::DifferentCrate {
return issues;
}
let balance = BalanceScore::calculate(coupling);
if coupling.strength == IntegrationStrength::Intrusive
&& coupling.distance == Distance::DifferentModule
{
issues.push(CouplingIssue {
issue_type: IssueType::GlobalComplexity,
severity: Severity::Medium, source: coupling.source.clone(),
target: coupling.target.clone(),
description: format!(
"Intrusive coupling to {} across module boundary",
coupling.target,
),
refactoring: RefactoringAction::IntroduceTrait {
suggested_name: format!("{}Trait", extract_type_name(&coupling.target)),
methods: vec!["// Extract required methods".to_string()],
},
balance_score: balance.score,
});
}
if coupling.strength == IntegrationStrength::Intrusive
&& coupling.volatility == Volatility::High
{
issues.push(CouplingIssue {
issue_type: IssueType::CascadingChangeRisk,
severity: Severity::High,
source: coupling.source.clone(),
target: coupling.target.clone(),
description: format!(
"Intrusive coupling to frequently-changed component {}",
coupling.target,
),
refactoring: RefactoringAction::StabilizeInterface {
interface_name: format!("{}Interface", extract_type_name(&coupling.target)),
},
balance_score: balance.score,
});
}
if coupling.strength == IntegrationStrength::Intrusive
&& coupling.distance == Distance::DifferentModule
&& balance.score < 0.5
{
if !issues
.iter()
.any(|i| i.issue_type == IssueType::GlobalComplexity)
{
issues.push(CouplingIssue {
issue_type: IssueType::InappropriateIntimacy,
severity: Severity::Medium,
source: coupling.source.clone(),
target: coupling.target.clone(),
description: format!(
"Direct internal access to {} across module boundary",
coupling.target,
),
refactoring: RefactoringAction::IntroduceTrait {
suggested_name: format!("{}Api", extract_type_name(&coupling.target)),
methods: vec!["// Expose only necessary operations".to_string()],
},
balance_score: balance.score,
});
}
}
issues
}
pub fn analyze_project_balance(metrics: &ProjectMetrics) -> ProjectBalanceReport {
analyze_project_balance_with_thresholds(metrics, &IssueThresholds::default())
}
pub fn analyze_project_balance_with_thresholds(
metrics: &ProjectMetrics,
thresholds: &IssueThresholds,
) -> ProjectBalanceReport {
let thresholds = thresholds.clone();
let mut all_issues = Vec::new();
let mut internal_balance_scores: Vec<BalanceScore> = Vec::new();
let mut all_balance_scores: Vec<BalanceScore> = Vec::new();
for coupling in &metrics.couplings {
let score = BalanceScore::calculate(coupling);
all_balance_scores.push(score.clone());
if coupling.distance != Distance::DifferentCrate {
internal_balance_scores.push(score);
let issues = identify_issues_with_thresholds(coupling, &thresholds);
all_issues.extend(issues);
}
}
let module_issues = analyze_module_coupling(metrics, &thresholds);
all_issues.extend(module_issues);
let rust_issues = analyze_rust_patterns(metrics, &thresholds);
all_issues.extend(rust_issues);
if thresholds.strict_mode {
all_issues.retain(|issue| issue.severity >= Severity::Medium);
}
all_issues.sort_by(|a, b| {
b.severity
.cmp(&a.severity)
.then_with(|| a.balance_score.partial_cmp(&b.balance_score).unwrap())
});
let total_couplings = metrics.couplings.len();
let internal_couplings = internal_balance_scores.len();
let balanced_count = internal_balance_scores
.iter()
.filter(|s| s.is_balanced())
.count();
let needs_review = internal_balance_scores
.iter()
.filter(|s| s.interpretation == BalanceInterpretation::NeedsReview)
.count();
let needs_refactoring = internal_balance_scores
.iter()
.filter(|s| s.needs_refactoring())
.count();
let average_score = if internal_balance_scores.is_empty() {
1.0 } else {
internal_balance_scores.iter().map(|s| s.score).sum::<f64>()
/ internal_balance_scores.len() as f64
};
let mut issues_by_severity: HashMap<Severity, usize> = HashMap::new();
for issue in &all_issues {
*issues_by_severity.entry(issue.severity).or_insert(0) += 1;
}
let mut issues_by_type: HashMap<IssueType, usize> = HashMap::new();
for issue in &all_issues {
*issues_by_type.entry(issue.issue_type).or_insert(0) += 1;
}
let health_grade = calculate_health_grade(&issues_by_severity, internal_couplings);
ProjectBalanceReport {
total_couplings,
balanced_count,
needs_review,
needs_refactoring,
average_score,
health_grade,
issues_by_severity,
issues_by_type,
issues: all_issues,
top_priorities: Vec::new(), }
.with_top_priorities(5) }
fn analyze_module_coupling(
metrics: &ProjectMetrics,
thresholds: &IssueThresholds,
) -> Vec<CouplingIssue> {
let mut issues = Vec::new();
let mut efferent: HashMap<&str, usize> = HashMap::new();
let mut afferent: HashMap<&str, usize> = HashMap::new();
for coupling in &metrics.couplings {
if coupling.distance == Distance::DifferentCrate {
continue;
}
*efferent.entry(&coupling.source).or_insert(0) += 1;
*afferent.entry(&coupling.target).or_insert(0) += 1;
}
for (module, count) in &efferent {
if *count > thresholds.max_dependencies {
issues.push(CouplingIssue {
issue_type: IssueType::HighEfferentCoupling,
severity: if *count > thresholds.max_dependencies * 2 {
Severity::High
} else {
Severity::Medium
},
source: module.to_string(),
target: format!("{} dependencies", count),
description: format!(
"Module {} depends on {} other components (threshold: {})",
module, count, thresholds.max_dependencies
),
refactoring: RefactoringAction::SplitModule {
suggested_modules: vec![
format!("{}_core", module),
format!("{}_integration", module),
],
},
balance_score: 1.0
- (*count as f64 / (thresholds.max_dependencies * 3) as f64).min(1.0),
});
}
}
for (module, count) in &afferent {
if *count > thresholds.max_dependents {
issues.push(CouplingIssue {
issue_type: IssueType::HighAfferentCoupling,
severity: if *count > thresholds.max_dependents * 2 {
Severity::High
} else {
Severity::Medium
},
source: format!("{} dependents", count),
target: module.to_string(),
description: format!(
"Module {} is depended on by {} other components (threshold: {})",
module, count, thresholds.max_dependents
),
refactoring: RefactoringAction::IntroduceTrait {
suggested_name: format!("{}Interface", extract_type_name(module)),
methods: vec!["// Define stable public API".to_string()],
},
balance_score: 1.0
- (*count as f64 / (thresholds.max_dependents * 3) as f64).min(1.0),
});
}
}
issues
}
fn analyze_rust_patterns(
metrics: &ProjectMetrics,
thresholds: &IssueThresholds,
) -> Vec<CouplingIssue> {
let mut issues = Vec::new();
for (module_name, module) in &metrics.modules {
let func_count = if thresholds.exclude_tests {
module
.function_count()
.saturating_sub(module.test_function_count)
} else {
module.function_count()
};
let type_count = module.type_definitions.len();
let impl_count = module.trait_impl_count + module.inherent_impl_count;
let is_god_module = func_count > thresholds.max_functions
|| type_count > thresholds.max_types
|| impl_count > thresholds.max_impls;
if is_god_module {
issues.push(CouplingIssue {
issue_type: IssueType::GodModule,
severity: if func_count > thresholds.max_functions * 2
|| type_count > thresholds.max_types * 2
{
Severity::High
} else {
Severity::Medium
},
source: module_name.clone(),
target: format!(
"{} functions, {} types, {} impls",
func_count, type_count, impl_count
),
description: format!(
"Module {} has too many responsibilities (functions: {}/{}, types: {}/{}, impls: {}/{})",
module_name,
func_count, thresholds.max_functions,
type_count, thresholds.max_types,
impl_count, thresholds.max_impls,
),
refactoring: RefactoringAction::SplitModule {
suggested_modules: vec![
format!("{}_core", module_name),
format!("{}_helpers", module_name),
],
},
balance_score: 0.5,
});
}
for type_def in module.type_definitions.values() {
if type_def.public_field_count > 0
&& !type_def.is_trait
&& type_def.visibility == crate::metrics::Visibility::Public
{
issues.push(CouplingIssue {
issue_type: IssueType::PublicFieldExposure,
severity: Severity::Low,
source: format!("{}::{}", module_name, type_def.name),
target: format!("{} public fields", type_def.public_field_count),
description: format!(
"Type {} has {} public field(s). Consider using getter methods.",
type_def.name, type_def.public_field_count
),
refactoring: RefactoringAction::AddGetters {
fields: vec!["// Add getter methods".to_string()],
},
balance_score: 0.7,
});
}
}
for func_def in module.function_definitions.values() {
if func_def.primitive_param_count >= thresholds.min_primitive_params
&& func_def.param_count >= thresholds.min_primitive_params
{
let ratio = func_def.primitive_param_count as f64 / func_def.param_count as f64;
if ratio >= 0.6 {
issues.push(CouplingIssue {
issue_type: IssueType::PrimitiveObsession,
severity: Severity::Low,
source: format!("{}::{}", module_name, func_def.name),
target: format!(
"{}/{} primitive params",
func_def.primitive_param_count, func_def.param_count
),
description: format!(
"Function {} has {} primitive parameters. Consider newtype pattern.",
func_def.name, func_def.primitive_param_count
),
refactoring: RefactoringAction::IntroduceNewtype {
suggested_name: format!("{}Params", capitalize_first(&func_def.name)),
wrapped_type: "// Group related parameters".to_string(),
},
balance_score: 0.7,
});
}
}
}
}
issues
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthGrade {
S, A, B, C, D, F, }
impl std::fmt::Display for HealthGrade {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HealthGrade::S => write!(f, "S (Over-optimized! Real code has some issues. Ship it!)"),
HealthGrade::A => write!(f, "A (Well-balanced)"),
HealthGrade::B => write!(f, "B (Healthy)"),
HealthGrade::C => write!(f, "C (Room for improvement)"),
HealthGrade::D => write!(f, "D (Attention needed)"),
HealthGrade::F => write!(f, "F (Immediate action required)"),
}
}
}
fn calculate_health_grade(
issues_by_severity: &HashMap<Severity, usize>,
internal_couplings: usize,
) -> HealthGrade {
let critical = *issues_by_severity.get(&Severity::Critical).unwrap_or(&0);
let high = *issues_by_severity.get(&Severity::High).unwrap_or(&0);
let medium = *issues_by_severity.get(&Severity::Medium).unwrap_or(&0);
if internal_couplings == 0 {
return HealthGrade::B;
}
if critical > 3 {
return HealthGrade::F;
}
let high_density = high as f64 / internal_couplings as f64;
let medium_density = medium as f64 / internal_couplings as f64;
let total_issue_density = (critical + high + medium) as f64 / internal_couplings as f64;
if critical > 0 || high_density > 0.05 {
return HealthGrade::D;
}
if high > 0 || medium_density > 0.25 {
return HealthGrade::C;
}
if medium_density > 0.10 || total_issue_density > 0.15 {
return HealthGrade::B;
}
if high == 0 && medium_density <= 0.05 && internal_couplings >= 20 {
return HealthGrade::S;
}
if high == 0 && medium_density <= 0.10 && internal_couplings >= 10 {
return HealthGrade::A;
}
HealthGrade::B
}
#[derive(Debug)]
pub struct ProjectBalanceReport {
pub total_couplings: usize,
pub balanced_count: usize,
pub needs_review: usize,
pub needs_refactoring: usize,
pub average_score: f64,
pub health_grade: HealthGrade,
pub issues_by_severity: HashMap<Severity, usize>,
pub issues_by_type: HashMap<IssueType, usize>,
pub issues: Vec<CouplingIssue>,
pub top_priorities: Vec<CouplingIssue>,
}
impl ProjectBalanceReport {
fn with_top_priorities(mut self, n: usize) -> Self {
self.top_priorities = self.issues.iter().take(n).cloned().collect();
self
}
pub fn issues_grouped_by_type(&self) -> HashMap<IssueType, Vec<&CouplingIssue>> {
let mut grouped: HashMap<IssueType, Vec<&CouplingIssue>> = HashMap::new();
for issue in &self.issues {
grouped.entry(issue.issue_type).or_default().push(issue);
}
grouped
}
}
pub fn calculate_project_score(metrics: &ProjectMetrics) -> f64 {
let internal_scores: Vec<f64> = metrics
.couplings
.iter()
.filter(|c| c.distance != Distance::DifferentCrate)
.map(|c| BalanceScore::calculate(c).score)
.collect();
if internal_scores.is_empty() {
return 1.0; }
internal_scores.iter().sum::<f64>() / internal_scores.len() as f64
}
fn extract_type_name(path: &str) -> String {
path.split("::")
.last()
.unwrap_or(path)
.chars()
.enumerate()
.map(|(i, c)| if i == 0 { c.to_ascii_uppercase() } else { c })
.collect()
}
pub fn strength_label(strength: IntegrationStrength) -> &'static str {
match strength {
IntegrationStrength::Intrusive => "Intrusive",
IntegrationStrength::Functional => "Functional",
IntegrationStrength::Model => "Model",
IntegrationStrength::Contract => "Contract",
}
}
pub fn distance_label(distance: Distance) -> &'static str {
match distance {
Distance::SameFunction => "same function",
Distance::SameModule => "same module",
Distance::DifferentModule => "different module",
Distance::DifferentCrate => "external crate",
}
}
pub fn volatility_label(volatility: Volatility) -> &'static str {
match volatility {
Volatility::Low => "rarely",
Volatility::Medium => "sometimes",
Volatility::High => "frequently",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_coupling(
strength: IntegrationStrength,
distance: Distance,
volatility: Volatility,
) -> CouplingMetrics {
CouplingMetrics::new(
"source::module".to_string(),
"target::module".to_string(),
strength,
distance,
volatility,
)
}
#[test]
fn test_balance_ideal_close() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::SameModule,
Volatility::Low,
);
let score = BalanceScore::calculate(&coupling);
assert!(score.is_balanced(), "Score: {}", score.score);
}
#[test]
fn test_balance_ideal_far() {
let coupling = make_coupling(
IntegrationStrength::Contract,
Distance::DifferentCrate,
Volatility::Low,
);
let score = BalanceScore::calculate(&coupling);
assert!(score.is_balanced(), "Score: {}", score.score);
}
#[test]
fn test_balance_bad_global_complexity() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::DifferentCrate,
Volatility::Low,
);
let score = BalanceScore::calculate(&coupling);
assert!(
score.needs_refactoring(),
"Score: {}, should need refactoring",
score.score
);
}
#[test]
fn test_balance_bad_cascading() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::SameModule,
Volatility::High,
);
let score = BalanceScore::calculate(&coupling);
assert!(
!score.is_balanced(),
"Score: {}, should not be balanced due to volatility",
score.score
);
}
#[test]
fn test_identify_global_complexity() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::DifferentModule,
Volatility::Low,
);
let issues = identify_issues(&coupling);
assert!(
!issues.is_empty(),
"Should identify global complexity issue for internal cross-module coupling"
);
assert!(
issues
.iter()
.any(|i| i.issue_type == IssueType::GlobalComplexity)
);
}
#[test]
fn test_external_crates_are_skipped() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::DifferentCrate,
Volatility::Low,
);
let issues = identify_issues(&coupling);
assert!(
issues.is_empty(),
"External crate dependencies should be skipped"
);
}
#[test]
fn test_identify_cascading_change() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::SameModule,
Volatility::High,
);
let issues = identify_issues(&coupling);
assert!(
issues
.iter()
.any(|i| i.issue_type == IssueType::CascadingChangeRisk),
"Intrusive coupling + High volatility should detect CascadingChangeRisk"
);
}
#[test]
fn test_identify_inappropriate_intimacy() {
let coupling = make_coupling(
IntegrationStrength::Intrusive,
Distance::DifferentModule,
Volatility::Low,
);
let issues = identify_issues(&coupling);
assert!(
issues
.iter()
.any(|i| i.issue_type == IssueType::GlobalComplexity),
"Intrusive + DifferentModule should detect GlobalComplexity"
);
}
#[test]
fn test_no_issues_for_balanced() {
let coupling = make_coupling(
IntegrationStrength::Model,
Distance::DifferentModule,
Volatility::Low,
);
let issues = identify_issues(&coupling);
assert!(
issues.is_empty(),
"Model coupling should not generate issues"
);
}
#[test]
fn test_health_grade_calculation() {
let mut issues = HashMap::new();
assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::S);
assert_eq!(calculate_health_grade(&issues, 15), HealthGrade::A);
assert_eq!(calculate_health_grade(&issues, 0), HealthGrade::B);
issues.insert(Severity::High, 1);
assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
issues.clear();
issues.insert(Severity::High, 6); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
issues.clear();
issues.insert(Severity::Critical, 1);
assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::D);
issues.clear();
issues.insert(Severity::Critical, 4);
assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::F);
issues.clear();
issues.insert(Severity::Medium, 30); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::C);
issues.clear();
issues.insert(Severity::Medium, 20); assert_eq!(calculate_health_grade(&issues, 100), HealthGrade::B);
}
}