garbage_code_hunter/autopsy/
mod.rs1use 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#[derive(Debug, Clone)]
14pub struct ContributingFactor {
15 pub name: &'static str,
16 pub description: String,
17 pub severity: &'static str,
18}
19
20#[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
30pub 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 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 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 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 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 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}