cargo_coupling/balance/
external_crates.rs1use crate::external::ExternalDependencyUsage;
4use crate::{CouplingIssue, IssueType, RefactoringAction, Severity};
5
6pub const SCATTERED_EXTERNAL_BREADTH_THRESHOLD: usize = 3;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CrateStability {
12 Fundamental,
14 Stable,
16 Infrastructure,
18 Normal,
20}
21
22pub fn classify_crate_stability(crate_name: &str) -> CrateStability {
24 let base_name = crate_name.split("::").next().unwrap_or(crate_name).trim();
26
27 match base_name {
28 "std" | "core" | "alloc" => CrateStability::Fundamental,
30
31 "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,
43
44 "tokio" | "async-std" | "smol" | "async-trait" | "futures" | "futures-util" | "tracing" | "tracing-subscriber" | "tracing-opentelemetry" | "opentelemetry" | "opentelemetry-otlp" | "opentelemetry_sdk" |
51 "hyper" | "reqwest" | "http" | "tonic" | "prost" | "sqlx" | "diesel" | "sea-orm" | "clap" | "structopt" => CrateStability::Infrastructure,
56
57 _ => CrateStability::Normal,
59 }
60}
61
62pub fn should_skip_crate(crate_name: &str) -> bool {
64 matches!(
65 classify_crate_stability(crate_name),
66 CrateStability::Fundamental
67 )
68}
69
70pub fn should_reduce_severity(crate_name: &str) -> bool {
72 matches!(
73 classify_crate_stability(crate_name),
74 CrateStability::Stable | CrateStability::Infrastructure
75 )
76}
77
78pub fn is_external_crate(target: &str, source: &str) -> bool {
81 let target_prefix = target.split("::").next().unwrap_or(target);
86 let source_prefix = source.split("::").next().unwrap_or(source);
87
88 if target_prefix == source_prefix {
90 return false;
91 }
92
93 let stability = classify_crate_stability(target);
96 matches!(
97 stability,
98 CrateStability::Fundamental | CrateStability::Stable | CrateStability::Infrastructure
99 )
100}
101
102pub fn detect_scattered_external_coupling(
104 dependencies: &[ExternalDependencyUsage],
105) -> Vec<CouplingIssue> {
106 dependencies
107 .iter()
108 .filter(|dependency| dependency.breadth > SCATTERED_EXTERNAL_BREADTH_THRESHOLD)
109 .map(|dependency| {
110 let severity = scattered_severity(dependency.breadth);
111 CouplingIssue {
112 issue_type: IssueType::ScatteredExternalCoupling,
113 severity,
114 source: format!("{} internal modules", dependency.breadth),
115 target: dependency.crate_name.clone(),
116 description: format!(
117 "{} is directly used from {} internal modules ({} references). Third-party upgrade risk is spread across the codebase.",
118 dependency.crate_name, dependency.breadth, dependency.total_references
119 ),
120 refactoring: RefactoringAction::General {
121 action: format!(
122 "Introduce a `{}` facade/wrapper module and route direct crate usage through it",
123 facade_module_name(&dependency.crate_name)
124 ),
125 },
126 balance_score: scattered_balance_score(dependency.breadth),
127 }
128 })
129 .collect()
130}
131
132fn scattered_severity(breadth: usize) -> Severity {
133 if breadth >= 10 {
134 Severity::Critical
135 } else if breadth >= 6 {
136 Severity::High
137 } else {
138 Severity::Medium
139 }
140}
141
142fn scattered_balance_score(breadth: usize) -> f64 {
143 1.0 - (breadth as f64 / 12.0).min(1.0)
144}
145
146fn facade_module_name(crate_name: &str) -> String {
147 format!("{}_facade", crate_name.replace('-', "_"))
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn scattered_coupling_is_flagged_above_threshold() {
156 let dependency = ExternalDependencyUsage {
157 crate_name: "reqwest".to_string(),
158 versions: vec![],
159 breadth: 4,
160 total_references: 9,
161 dominant_strength: "Functional".to_string(),
162 source_modules: vec![
163 "a".to_string(),
164 "b".to_string(),
165 "c".to_string(),
166 "d".to_string(),
167 ],
168 };
169
170 let issues = detect_scattered_external_coupling(&[dependency]);
171
172 assert_eq!(issues.len(), 1);
173 assert_eq!(issues[0].issue_type, IssueType::ScatteredExternalCoupling);
174 assert_eq!(issues[0].severity, Severity::Medium);
175 assert_eq!(issues[0].target, "reqwest");
176 assert!(format!("{}", issues[0].refactoring).contains("facade"));
177 }
178}