use crate::external::ExternalDependencyUsage;
use crate::{CouplingIssue, IssueType, RefactoringAction, Severity};
pub const SCATTERED_EXTERNAL_BREADTH_THRESHOLD: usize = 3;
#[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 detect_scattered_external_coupling(
dependencies: &[ExternalDependencyUsage],
) -> Vec<CouplingIssue> {
dependencies
.iter()
.filter(|dependency| dependency.breadth > SCATTERED_EXTERNAL_BREADTH_THRESHOLD)
.map(|dependency| {
let severity = scattered_severity(dependency.breadth);
CouplingIssue {
issue_type: IssueType::ScatteredExternalCoupling,
severity,
source: format!("{} internal modules", dependency.breadth),
target: dependency.crate_name.clone(),
description: format!(
"{} is directly used from {} internal modules ({} references). Third-party upgrade risk is spread across the codebase.",
dependency.crate_name, dependency.breadth, dependency.total_references
),
refactoring: RefactoringAction::General {
action: format!(
"Introduce a `{}` facade/wrapper module and route direct crate usage through it",
facade_module_name(&dependency.crate_name)
),
},
balance_score: scattered_balance_score(dependency.breadth),
}
})
.collect()
}
fn scattered_severity(breadth: usize) -> Severity {
if breadth >= 10 {
Severity::Critical
} else if breadth >= 6 {
Severity::High
} else {
Severity::Medium
}
}
fn scattered_balance_score(breadth: usize) -> f64 {
1.0 - (breadth as f64 / 12.0).min(1.0)
}
fn facade_module_name(crate_name: &str) -> String {
format!("{}_facade", crate_name.replace('-', "_"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scattered_coupling_is_flagged_above_threshold() {
let dependency = ExternalDependencyUsage {
crate_name: "reqwest".to_string(),
versions: vec![],
breadth: 4,
total_references: 9,
dominant_strength: "Functional".to_string(),
source_modules: vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
],
};
let issues = detect_scattered_external_coupling(&[dependency]);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].issue_type, IssueType::ScatteredExternalCoupling);
assert_eq!(issues[0].severity, Severity::Medium);
assert_eq!(issues[0].target, "reqwest");
assert!(format!("{}", issues[0].refactoring).contains("facade"));
}
}