Skip to main content

cargo_coupling/cli_output/
hotspots.rs

1//! Refactoring priorities and their explanations.
2
3use 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/// A hotspot module that needs attention
12#[derive(Debug, Clone, Serialize)]
13pub struct Hotspot {
14    /// Module name
15    pub module: String,
16    /// Hotspot score (higher = more urgent)
17    pub score: u32,
18    /// Issues found in this module
19    pub issues: Vec<HotspotIssue>,
20    /// Suggested fix action
21    pub suggestion: String,
22    /// File path if available
23    pub file_path: Option<String>,
24    /// Whether this module is in a circular dependency
25    pub in_cycle: bool,
26}
27
28/// An issue contributing to a hotspot
29#[derive(Debug, Clone, Serialize)]
30pub struct HotspotIssue {
31    pub severity: String,
32    pub issue_type: String,
33    pub description: String,
34}
35
36// ============================================================================
37// Beginner-friendly explanations
38// ============================================================================
39
40/// Get a beginner-friendly explanation for an issue type
41pub 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
125/// Beginner-friendly explanation for an issue
126pub struct IssueExplanation {
127    /// What this issue means in simple terms
128    pub what_it_means: &'static str,
129    /// Why this is problematic
130    pub why_its_bad: Vec<&'static str>,
131    /// How to fix it
132    pub how_to_fix: &'static str,
133    /// Optional example
134    pub example: Option<&'static str>,
135}
136
137/// Calculate hotspots from project metrics
138pub 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    // Group issues by source module
166    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    // Calculate coupling counts per module
175    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    // Build hotspots
187    let mut hotspots: Vec<Hotspot> = Vec::new();
188
189    for (module, issues) in &module_issues {
190        let mut score: u32 = 0;
191
192        // Base score from issue count and severity
193        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        // Bonus for circular dependencies
203        let in_cycle = cycle_modules.contains(module);
204        if in_cycle {
205            score += 40;
206        }
207
208        // Bonus for high coupling count
209        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        // Determine primary issue type for suggestion
214        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        // Get file path
224        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    // Also add modules in cycles that don't have other issues
247    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    // Sort by score descending, then module name for deterministic ties.
272    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
278/// Generate hotspots output to writer
279pub 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        // Header with rank and score
305        writeln!(
306            writer,
307            "#{} {} (Score: {})",
308            i + 1,
309            hotspot.module,
310            hotspot.score
311        )?;
312
313        // File path if available
314        if let Some(path) = &hotspot.file_path {
315            writeln!(writer, "   📁 {}", path)?;
316        }
317
318        // Issues with optional verbose explanations
319        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            // Show beginner-friendly explanation in verbose mode
333            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        // Suggestion (only if not verbose, since verbose already shows how_to_fix)
354        if !verbose {
355            writeln!(writer, "   → Fix: {}", hotspot.suggestion)?;
356        }
357        writeln!(writer)?;
358    }
359
360    Ok(())
361}