Skip to main content

garbage_code_hunter/autopsy/
mod.rs

1//! Code Autopsy — root cause analysis of codebase problems.
2
3use crate::analyzer::{CodeAnalyzer, CodeIssue};
4use crate::common::i18n_ext::t;
5use crate::common::OutputFormat;
6use crate::signals::{classify_rule, StyleSignal};
7use anyhow::Result;
8use colored::Colorize;
9use std::collections::HashMap;
10use std::path::Path;
11
12/// A contributing factor to codebase death.
13#[derive(Debug, Clone)]
14pub struct ContributingFactor {
15    pub name: &'static str,
16    pub description: String,
17    pub severity: &'static str,
18}
19
20/// The autopsy report.
21#[derive(Debug, Clone)]
22pub struct AutopsyReport {
23    pub primary_cause: String,
24    pub secondary_cause: String,
25    pub contributing_factors: Vec<ContributingFactor>,
26    pub estimated_lifespan: String,
27    pub death_certificate: String,
28}
29
30/// Run autopsy analysis on a codebase.
31pub fn run(path: &Path, format: &OutputFormat, lang: &str) -> Result<String> {
32    let analyzer = CodeAnalyzer::new(&[], lang);
33    let issues = analyzer.analyze_path(path);
34    let report = generate_autopsy(&issues);
35
36    let output = match format {
37        OutputFormat::Terminal => display_terminal(&report, lang),
38        OutputFormat::Json => display_json(&report),
39    };
40
41    Ok(output)
42}
43
44fn generate_autopsy(issues: &[CodeIssue]) -> AutopsyReport {
45    let mut category_counts: HashMap<&str, usize> = HashMap::new();
46
47    for issue in issues {
48        let cat = categorize(&issue.rule_name);
49        *category_counts.entry(cat).or_insert(0) += 1;
50    }
51
52    // Find primary and secondary causes
53    let mut sorted: Vec<_> = category_counts.iter().collect();
54    sorted.sort_by(|a, b| b.1.cmp(a.1));
55
56    let primary = sorted.first().map(|(k, _)| **k).unwrap_or("Unknown");
57    let secondary = sorted.get(1).map(|(k, _)| **k).unwrap_or("Unknown");
58
59    let total = issues.len();
60
61    // Contributing factors
62    let mut factors = Vec::new();
63
64    if total > 50 {
65        factors.push(ContributingFactor {
66            name: "Accumulated Technical Debt",
67            description: format!("{} issues found — debt has been accumulating", total),
68            severity: "Critical",
69        });
70    }
71
72    // Check for deadline-driven patterns
73    let quick_fix_count = issues
74        .iter()
75        .filter(|i| i.rule_name.to_lowercase().contains("unwrap"))
76        .count();
77    if quick_fix_count > 10 {
78        factors.push(ContributingFactor {
79            name: "Deadline Pressure",
80            description: format!(
81                "{} unwrap() calls suggest 'ship now, fix never' mentality",
82                quick_fix_count
83            ),
84            severity: "High",
85        });
86    }
87
88    let naming_count = issues
89        .iter()
90        .filter(|i| {
91            let r = i.rule_name.to_lowercase();
92            r.contains("name") || r.contains("single_letter")
93        })
94        .count();
95    if naming_count > 5 {
96        factors.push(ContributingFactor {
97            name: "Rapid Prototyping Syndrome",
98            description: format!(
99                "{} naming issues suggest code was written in a hurry",
100                naming_count
101            ),
102            severity: "Medium",
103        });
104    }
105
106    if factors.is_empty() {
107        factors.push(ContributingFactor {
108            name: "Natural Aging",
109            description: "Code naturally degrades over time without maintenance".to_string(),
110            severity: "Low",
111        });
112    }
113
114    // Estimate lifespan based on issue count
115    let lifespan = if total > 200 {
116        "3 months — code is on life support".to_string()
117    } else if total > 80 {
118        "6 months — symptoms are treatable".to_string()
119    } else if total > 30 {
120        "12 months — early intervention recommended".to_string()
121    } else {
122        "24+ months — prognosis is good".to_string()
123    };
124
125    // Death certificate quote
126    let certificate = match primary {
127        "Copy-Paste Driven Development" => {
128            "The codebase was pronounced dead after 47 identical implementations \
129             of the same function were found across 12 files."
130                .to_string()
131        }
132        "Uncontrolled unwrap() Proliferation" => "Cause of death: acute panic attack. \
133             The code believed everything would always work. It was wrong."
134            .to_string(),
135        "Pyramid of Doom" => "Cause of death: suffocation under layers of nested code. \
136             The indentation level exceeded human comprehension."
137            .to_string(),
138        _ => {
139            format!(
140                "Cause of death: {} — the codebase could not withstand \
141                 the accumulated weight of its own technical debt.",
142                primary
143            )
144        }
145    };
146
147    AutopsyReport {
148        primary_cause: primary.to_string(),
149        secondary_cause: secondary.to_string(),
150        contributing_factors: factors,
151        estimated_lifespan: lifespan,
152        death_certificate: certificate,
153    }
154}
155
156fn categorize(rule_name: &str) -> &'static str {
157    match classify_rule(rule_name) {
158        StyleSignal::Duplication => "Copy-Paste Driven Development",
159        StyleSignal::PanicAddiction => "Uncontrolled unwrap() Proliferation",
160        StyleSignal::NamingChaos => "Naming Atrophy",
161        StyleSignal::NestedHell => "Pyramid of Doom",
162        StyleSignal::CodeSmells => "Magic Number Syndrome",
163        StyleSignal::OverEngineering => "Function Obesity",
164        StyleSignal::HotfixCulture => "Chronic Code Smell",
165        StyleSignal::LegacyCode => "Cemetary of Dead Code",
166        StyleSignal::TodoMountain => "Unfinished Symphony",
167        StyleSignal::LineCountSmell => "File Bloat",
168    }
169}
170
171fn display_terminal(report: &AutopsyReport, lang: &str) -> String {
172    let mut out = String::new();
173
174    out.push_str(&format!(
175        "\n{}\n",
176        t(
177            lang,
178            "\u{2620}\u{fe0f} 代码尸检报告",
179            "\u{2620}\u{fe0f} Code Autopsy Report"
180        )
181        .bold()
182    ));
183    out.push_str(&format!("{}\n\n", "\u{2501}".repeat(40)));
184
185    out.push_str(&format!(
186        "{}\n",
187        t(
188            lang,
189            "\u{1f480} 代码库死亡的根本原因",
190            "\u{1f480} Root Cause of Codebase Death"
191        )
192        .bold()
193    ));
194    out.push_str(&format!(
195        "  {}\n    {}\n\n",
196        t(lang, "主要原因:", "Primary Cause:"),
197        report.primary_cause.red().bold()
198    ));
199    out.push_str(&format!(
200        "  {}\n    {}\n\n",
201        t(lang, "次要原因:", "Secondary Cause:"),
202        report.secondary_cause.yellow()
203    ));
204
205    out.push_str(&format!(
206        "{}\n",
207        t(lang, "\u{1f4a7} 促成因素", "\u{1f4a7} Contributing Factors").bold()
208    ));
209    for factor in &report.contributing_factors {
210        let sev = match factor.severity {
211            "Critical" => factor.severity.red().bold(),
212            "High" => factor.severity.red(),
213            "Medium" => factor.severity.yellow(),
214            _ => factor.severity.green(),
215        };
216        out.push_str(&format!(
217            "  \u{2022} [{}] {}: {}\n",
218            sev,
219            factor.name.bold(),
220            factor.description
221        ));
222    }
223    out.push('\n');
224
225    out.push_str(&format!(
226        "{}\n",
227        t(lang, "\u{1f52e} 预估寿命", "\u{1f52e} Estimated Lifespan").bold()
228    ));
229    out.push_str(&format!("  {}\n\n", report.estimated_lifespan));
230
231    out.push_str(&format!(
232        "{}\n",
233        t(lang, "\u{1f4dc} 死亡证明", "\u{1f4dc} Death Certificate").bold()
234    ));
235    out.push_str(&format!("  \"{}\"\n", report.death_certificate.italic()));
236
237    out
238}
239
240fn display_json(report: &AutopsyReport) -> String {
241    serde_json::json!({
242        "primary_cause": report.primary_cause,
243        "secondary_cause": report.secondary_cause,
244        "contributing_factors": report.contributing_factors.iter().map(|f| {
245            serde_json::json!({
246                "name": f.name,
247                "description": f.description,
248                "severity": f.severity,
249            })
250        }).collect::<Vec<_>>(),
251        "estimated_lifespan": report.estimated_lifespan,
252        "death_certificate": report.death_certificate,
253    })
254    .to_string()
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_categorize() {
263        assert_eq!(
264            categorize("unwrap-abuse"),
265            "Uncontrolled unwrap() Proliferation"
266        );
267        assert_eq!(categorize("deep-nesting"), "Pyramid of Doom");
268        assert_eq!(
269            categorize("code-duplication"),
270            "Copy-Paste Driven Development"
271        );
272    }
273
274    #[test]
275    fn test_run_on_current_dir() {
276        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "en-US");
277        assert!(result.is_ok());
278    }
279
280    #[test]
281    fn test_run_on_current_dir_chinese() {
282        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "zh-CN");
283        assert!(result.is_ok());
284    }
285
286    #[test]
287    fn test_run_json_format() {
288        let result = run(std::path::Path::new("."), &OutputFormat::Json, "en-US");
289        assert!(result.is_ok());
290        let json = result.unwrap();
291        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
292        assert!(parsed["primary_cause"].is_string());
293    }
294}