garbage_code_hunter/autopsy/
mod.rs1use 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#[derive(Debug, Clone)]
13pub struct ContributingFactor {
14 pub name: &'static str,
15 pub description: String,
16 pub severity: &'static str,
17}
18
19#[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
29pub 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 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 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 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 let density = total as f64; 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 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}