1use crate::balance::project::analyze_project_balance_with_thresholds;
4use crate::{CouplingIssue, Distance, IssueThresholds, ProjectMetrics, Severity};
5use serde::Serialize;
6use std::{
7 collections::{HashMap, HashSet},
8 io::{self, Write},
9};
10
11#[derive(Debug, Clone, Serialize)]
13pub struct Hotspot {
14 pub module: String,
16 pub score: u32,
18 pub issues: Vec<HotspotIssue>,
20 pub suggestion: String,
22 pub file_path: Option<String>,
24 pub in_cycle: bool,
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct HotspotIssue {
31 pub severity: String,
32 pub issue_type: String,
33 pub description: String,
34}
35
36pub fn get_issue_explanation(issue_type: &str) -> IssueExplanation {
42 match issue_type {
43 "High Efferent Coupling" => IssueExplanation {
44 what_it_means: "This module depends on too many other modules",
45 why_its_bad: vec![
46 "Changes elsewhere may break this module",
47 "Testing requires many mocks/stubs",
48 "Hard to understand in isolation",
49 ],
50 how_to_fix: "Split into smaller modules with clear responsibilities",
51 example: Some("e.g., Split main.rs into cli.rs, config.rs, runner.rs"),
52 },
53 "High Afferent Coupling" => IssueExplanation {
54 what_it_means: "Too many other modules depend on this one",
55 why_its_bad: vec![
56 "Changes here may break many other modules",
57 "Fear of changing leads to technical debt",
58 "Wide blast radius for bugs",
59 ],
60 how_to_fix: "Define a stable interface (trait) to hide implementation details",
61 example: Some("e.g., pub struct -> pub trait + impl for abstraction"),
62 },
63 "Circular Dependency" | "CircularDependency" => IssueExplanation {
64 what_it_means: "Modules depend on each other in a cycle (A -> B -> A)",
65 why_its_bad: vec![
66 "Can't understand one without the other",
67 "Unit testing is difficult (need both)",
68 "May cause compilation order issues",
69 ],
70 how_to_fix: "Extract shared types to a common module, or use traits to invert dependencies",
71 example: Some("e.g., A -> B -> A becomes A -> Common <- B"),
72 },
73 "Global Complexity" => IssueExplanation {
74 what_it_means: "Strong coupling to a distant module",
75 why_its_bad: vec![
76 "Hard to trace code flow",
77 "Changes have unpredictable effects",
78 "Module is not self-contained",
79 ],
80 how_to_fix: "Move the dependency closer, or use an interface for loose coupling",
81 example: None,
82 },
83 "Cascading Change Risk" => IssueExplanation {
84 what_it_means: "Strongly coupled to a frequently-changing module",
85 why_its_bad: vec![
86 "Every change there requires changes here",
87 "Bugs propagate through the chain",
88 "Constant rework needed",
89 ],
90 how_to_fix: "Depend on a stable interface instead of implementation",
91 example: None,
92 },
93 "Scattered External Coupling" => IssueExplanation {
94 what_it_means: "A third-party crate is used directly from many modules",
95 why_its_bad: vec![
96 "Crate API changes have a wide edit surface",
97 "Upgrade risk is spread across unrelated modules",
98 "Harder to replace or mock the dependency",
99 ],
100 how_to_fix: "Introduce a facade or wrapper module around the crate",
101 example: Some("e.g., reqwest calls go through http_client.rs"),
102 },
103 "Inappropriate Intimacy" | "InappropriateIntimacy" => IssueExplanation {
104 what_it_means: "Directly accessing another module's internal details",
105 why_its_bad: vec![
106 "Breaks encapsulation",
107 "Internal changes affect external code",
108 "Unclear module boundaries",
109 ],
110 how_to_fix: "Access through public methods or traits instead",
111 example: Some("e.g., foo.internal_field -> foo.get_value()"),
112 },
113 _ => IssueExplanation {
114 what_it_means: "A coupling-related issue was detected",
115 why_its_bad: vec![
116 "May reduce code maintainability",
117 "May increase change impact",
118 ],
119 how_to_fix: "Review the module dependencies",
120 example: None,
121 },
122 }
123}
124
125pub struct IssueExplanation {
127 pub what_it_means: &'static str,
129 pub why_its_bad: Vec<&'static str>,
131 pub how_to_fix: &'static str,
133 pub example: Option<&'static str>,
135}
136
137pub fn calculate_hotspots(
139 metrics: &ProjectMetrics,
140 thresholds: &IssueThresholds,
141 limit: usize,
142) -> Vec<Hotspot> {
143 let circular_deps = metrics.detect_circular_dependencies();
144 calculate_hotspots_with_cycles(metrics, thresholds, limit, &circular_deps)
145}
146
147pub(super) fn calculate_hotspots_with_cycles(
148 metrics: &ProjectMetrics,
149 thresholds: &IssueThresholds,
150 limit: usize,
151 circular_deps: &[Vec<String>],
152) -> Vec<Hotspot> {
153 let report = analyze_project_balance_with_thresholds(metrics, thresholds);
154 let names = crate::design::graph::DependencyGraph {
155 nodes: metrics.modules.keys().cloned().collect(),
156 ..Default::default()
157 };
158 let canonical = |name: &str| names.resolve(name).unwrap_or_else(|| name.to_string());
159 let cycle_modules: HashSet<String> = circular_deps
160 .iter()
161 .flatten()
162 .map(|name| canonical(name))
163 .collect();
164
165 let mut module_issues: HashMap<String, Vec<&CouplingIssue>> = HashMap::new();
167 for issue in &report.issues {
168 module_issues
169 .entry(canonical(&issue.source))
170 .or_default()
171 .push(issue);
172 }
173
174 let mut couplings_out: HashMap<String, usize> = HashMap::new();
176 let mut couplings_in: HashMap<String, usize> = HashMap::new();
177 for coupling in &metrics.couplings {
178 if coupling.distance != Distance::DifferentCrate {
179 *couplings_out
180 .entry(canonical(&coupling.source))
181 .or_default() += 1;
182 *couplings_in.entry(canonical(&coupling.target)).or_default() += 1;
183 }
184 }
185
186 let mut hotspots: Vec<Hotspot> = Vec::new();
188
189 for (module, issues) in &module_issues {
190 let mut score: u32 = 0;
191
192 for issue in issues {
194 score += match issue.severity {
195 Severity::Critical => 50,
196 Severity::High => 30,
197 Severity::Medium => 15,
198 Severity::Low => 5,
199 };
200 }
201
202 let in_cycle = cycle_modules.contains(module);
204 if in_cycle {
205 score += 40;
206 }
207
208 let out_count = couplings_out.get(module).copied().unwrap_or(0);
210 let in_count = couplings_in.get(module).copied().unwrap_or(0);
211 score += (out_count + in_count) as u32 * 2;
212
213 let primary_issue = issues.iter().max_by_key(|i| i.severity);
215 let suggestion = if in_cycle {
216 "Break circular dependency by extracting shared types or inverting with traits".into()
217 } else if let Some(issue) = primary_issue {
218 format!("{}", issue.refactoring)
219 } else {
220 "Review module coupling".into()
221 };
222
223 let file_path = metrics
225 .modules
226 .get(module)
227 .map(|m| m.path.display().to_string());
228
229 hotspots.push(Hotspot {
230 module: module.clone(),
231 score,
232 issues: issues
233 .iter()
234 .map(|i| HotspotIssue {
235 severity: format!("{}", i.severity),
236 issue_type: format!("{}", i.issue_type),
237 description: i.description.clone(),
238 })
239 .collect(),
240 suggestion,
241 file_path,
242 in_cycle,
243 });
244 }
245
246 for module in &cycle_modules {
248 if !module_issues.contains_key(module) {
249 let file_path = metrics
250 .modules
251 .get(module)
252 .map(|m| m.path.display().to_string());
253
254 hotspots.push(Hotspot {
255 module: module.clone(),
256 score: 40,
257 issues: vec![HotspotIssue {
258 severity: "Critical".into(),
259 issue_type: "CircularDependency".into(),
260 description: "Part of a circular dependency cycle".into(),
261 }],
262 suggestion:
263 "Break circular dependency by extracting shared types or inverting with traits"
264 .into(),
265 file_path,
266 in_cycle: true,
267 });
268 }
269 }
270
271 hotspots.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.module.cmp(&b.module)));
273 hotspots.truncate(limit);
274
275 hotspots
276}
277
278pub fn generate_hotspots_output<W: Write>(
280 metrics: &ProjectMetrics,
281 thresholds: &IssueThresholds,
282 limit: usize,
283 verbose: bool,
284 writer: &mut W,
285) -> io::Result<()> {
286 let hotspots = calculate_hotspots(metrics, thresholds, limit);
287
288 writeln!(writer, "Top {} Refactoring Targets", limit)?;
289 writeln!(
290 writer,
291 "═══════════════════════════════════════════════════════════"
292 )?;
293
294 if hotspots.is_empty() {
295 writeln!(writer)?;
296 writeln!(writer, "✅ No significant hotspots detected.")?;
297 writeln!(writer, " Your codebase has good coupling balance.")?;
298 return Ok(());
299 }
300
301 writeln!(writer)?;
302
303 for (i, hotspot) in hotspots.iter().enumerate() {
304 writeln!(
306 writer,
307 "#{} {} (Score: {})",
308 i + 1,
309 hotspot.module,
310 hotspot.score
311 )?;
312
313 if let Some(path) = &hotspot.file_path {
315 writeln!(writer, " 📁 {}", path)?;
316 }
317
318 for issue in &hotspot.issues {
320 let icon = match issue.severity.as_str() {
321 "Critical" => "🔴",
322 "High" => "🟠",
323 "Medium" => "🟡",
324 _ => "⚪",
325 };
326 writeln!(
327 writer,
328 " {} {}: {}",
329 icon, issue.severity, issue.issue_type
330 )?;
331
332 if verbose {
334 let explanation = get_issue_explanation(&issue.issue_type);
335 writeln!(writer)?;
336 writeln!(writer, " 💡 What it means:")?;
337 writeln!(writer, " {}", explanation.what_it_means)?;
338 writeln!(writer)?;
339 writeln!(writer, " ⚠️ Why it's a problem:")?;
340 for reason in &explanation.why_its_bad {
341 writeln!(writer, " • {}", reason)?;
342 }
343 writeln!(writer)?;
344 writeln!(writer, " 🔧 How to fix:")?;
345 writeln!(writer, " {}", explanation.how_to_fix)?;
346 if let Some(example) = explanation.example {
347 writeln!(writer, " {}", example)?;
348 }
349 writeln!(writer)?;
350 }
351 }
352
353 if !verbose {
355 writeln!(writer, " → Fix: {}", hotspot.suggestion)?;
356 }
357 writeln!(writer)?;
358 }
359
360 Ok(())
361}