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 anyhow::Result;
7use colored::Colorize;
8use std::collections::HashMap;
9use std::path::Path;
10
11/// A contributing factor to codebase death.
12#[derive(Debug, Clone)]
13pub struct ContributingFactor {
14    pub name: &'static str,
15    pub description: String,
16    pub severity: &'static str,
17}
18
19/// The autopsy report.
20#[derive(Debug, Clone)]
21pub struct AutopsyReport {
22    pub primary_cause: String,
23    pub secondary_cause: String,
24    pub contributing_factors: Vec<ContributingFactor>,
25    pub estimated_lifespan: String,
26    pub death_certificate: String,
27}
28
29/// Run autopsy analysis on a codebase.
30pub fn run(path: &Path, format: &OutputFormat, lang: &str) -> Result<String> {
31    let analyzer = CodeAnalyzer::new(&[], lang);
32    let issues = analyzer.analyze_path(path);
33    let report = generate_autopsy(&issues);
34
35    let output = match format {
36        OutputFormat::Terminal => display_terminal(&report, lang),
37        OutputFormat::Json => display_json(&report),
38    };
39
40    Ok(output)
41}
42
43fn generate_autopsy(issues: &[CodeIssue]) -> AutopsyReport {
44    let mut category_counts: HashMap<&str, usize> = HashMap::new();
45
46    for issue in issues {
47        let cat = categorize(&issue.rule_name);
48        *category_counts.entry(cat).or_insert(0) += 1;
49    }
50
51    // Find primary and secondary causes
52    let mut sorted: Vec<_> = category_counts.iter().collect();
53    sorted.sort_by(|a, b| b.1.cmp(a.1));
54
55    let primary = sorted.first().map(|(k, _)| **k).unwrap_or("Unknown");
56    let secondary = sorted.get(1).map(|(k, _)| **k).unwrap_or("Unknown");
57
58    let total = issues.len();
59
60    // Contributing factors
61    let mut factors = Vec::new();
62
63    if total > 50 {
64        factors.push(ContributingFactor {
65            name: "Accumulated Technical Debt",
66            description: format!("{} issues found — debt has been accumulating", total),
67            severity: "Critical",
68        });
69    }
70
71    // Check for deadline-driven patterns
72    let quick_fix_count = issues
73        .iter()
74        .filter(|i| i.rule_name.to_lowercase().contains("unwrap"))
75        .count();
76    if quick_fix_count > 10 {
77        factors.push(ContributingFactor {
78            name: "Deadline Pressure",
79            description: format!(
80                "{} unwrap() calls suggest 'ship now, fix never' mentality",
81                quick_fix_count
82            ),
83            severity: "High",
84        });
85    }
86
87    let naming_count = issues
88        .iter()
89        .filter(|i| {
90            let r = i.rule_name.to_lowercase();
91            r.contains("name") || r.contains("single_letter")
92        })
93        .count();
94    if naming_count > 5 {
95        factors.push(ContributingFactor {
96            name: "Rapid Prototyping Syndrome",
97            description: format!(
98                "{} naming issues suggest code was written in a hurry",
99                naming_count
100            ),
101            severity: "Medium",
102        });
103    }
104
105    if factors.is_empty() {
106        factors.push(ContributingFactor {
107            name: "Natural Aging",
108            description: "Code naturally degrades over time without maintenance".to_string(),
109            severity: "Low",
110        });
111    }
112
113    // Estimate lifespan
114    let density = total as f64; // rough estimate
115    let lifespan = if density > 100.0 {
116        "3 months — code is on life support".to_string()
117    } else if density > 50.0 {
118        "6 months — symptoms are treatable".to_string()
119    } else if density > 20.0 {
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    let lower = rule_name.to_lowercase();
158    if lower.contains("duplicat") || lower.contains("copy") {
159        "Copy-Paste Driven Development"
160    } else if lower.contains("unwrap") {
161        "Uncontrolled unwrap() Proliferation"
162    } else if lower.contains("nest") || lower.contains("complex") {
163        "Pyramid of Doom"
164    } else if lower.contains("name")
165        || lower.contains("single_letter")
166        || lower.contains("meaningless")
167    {
168        "Naming Atrophy"
169    } else if lower.contains("magic") {
170        "Magic Number Syndrome"
171    } else if lower.contains("long") {
172        "Function Obesity"
173    } else {
174        "Chronic Code Smell"
175    }
176}
177
178fn display_terminal(report: &AutopsyReport, lang: &str) -> String {
179    let mut out = String::new();
180
181    out.push_str(&format!(
182        "\n{}\n",
183        t(
184            lang,
185            "\u{2620}\u{fe0f} 代码尸检报告",
186            "\u{2620}\u{fe0f} Code Autopsy Report"
187        )
188        .bold()
189    ));
190    out.push_str(&format!("{}\n\n", "\u{2501}".repeat(40)));
191
192    out.push_str(&format!(
193        "{}\n",
194        t(
195            lang,
196            "\u{1f480} 代码库死亡的根本原因",
197            "\u{1f480} Root Cause of Codebase Death"
198        )
199        .bold()
200    ));
201    out.push_str(&format!(
202        "  {}\n    {}\n\n",
203        t(lang, "主要原因:", "Primary Cause:"),
204        report.primary_cause.red().bold()
205    ));
206    out.push_str(&format!(
207        "  {}\n    {}\n\n",
208        t(lang, "次要原因:", "Secondary Cause:"),
209        report.secondary_cause.yellow()
210    ));
211
212    out.push_str(&format!(
213        "{}\n",
214        t(lang, "\u{1f4a7} 促成因素", "\u{1f4a7} Contributing Factors").bold()
215    ));
216    for factor in &report.contributing_factors {
217        let sev = match factor.severity {
218            "Critical" => factor.severity.red().bold(),
219            "High" => factor.severity.red(),
220            "Medium" => factor.severity.yellow(),
221            _ => factor.severity.green(),
222        };
223        out.push_str(&format!(
224            "  \u{2022} [{}] {}: {}\n",
225            sev,
226            factor.name.bold(),
227            factor.description
228        ));
229    }
230    out.push('\n');
231
232    out.push_str(&format!(
233        "{}\n",
234        t(lang, "\u{1f52e} 预估寿命", "\u{1f52e} Estimated Lifespan").bold()
235    ));
236    out.push_str(&format!("  {}\n\n", report.estimated_lifespan));
237
238    out.push_str(&format!(
239        "{}\n",
240        t(lang, "\u{1f4dc} 死亡证明", "\u{1f4dc} Death Certificate").bold()
241    ));
242    out.push_str(&format!("  \"{}\"\n", report.death_certificate.italic()));
243
244    out
245}
246
247fn display_json(report: &AutopsyReport) -> String {
248    serde_json::json!({
249        "primary_cause": report.primary_cause,
250        "secondary_cause": report.secondary_cause,
251        "contributing_factors": report.contributing_factors.iter().map(|f| {
252            serde_json::json!({
253                "name": f.name,
254                "description": f.description,
255                "severity": f.severity,
256            })
257        }).collect::<Vec<_>>(),
258        "estimated_lifespan": report.estimated_lifespan,
259        "death_certificate": report.death_certificate,
260    })
261    .to_string()
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_categorize() {
270        assert_eq!(
271            categorize("unwrap_abuse"),
272            "Uncontrolled unwrap() Proliferation"
273        );
274        assert_eq!(categorize("deep_nesting"), "Pyramid of Doom");
275        assert_eq!(
276            categorize("code_duplication"),
277            "Copy-Paste Driven Development"
278        );
279    }
280
281    #[test]
282    fn test_run_on_current_dir() {
283        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "en-US");
284        assert!(result.is_ok());
285    }
286
287    #[test]
288    fn test_run_on_current_dir_chinese() {
289        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "zh-CN");
290        assert!(result.is_ok());
291    }
292
293    #[test]
294    fn test_run_json_format() {
295        let result = run(std::path::Path::new("."), &OutputFormat::Json, "en-US");
296        assert!(result.is_ok());
297        let json = result.unwrap();
298        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
299        assert!(parsed["primary_cause"].is_string());
300    }
301}