garbage_code_hunter/radar/
mod.rs1use crate::analyzer::{CodeAnalyzer, CodeIssue};
4use crate::common::i18n_ext::t;
5use crate::common::OutputFormat;
6use anyhow::Result;
7use std::collections::HashMap;
8use std::path::Path;
9
10#[derive(Debug, Clone)]
12pub struct RadarData {
13 pub complexity: f64,
14 pub duplication: f64,
15 pub naming: f64,
16 pub panic_risk: f64,
17 pub dependency_hell: f64,
18 pub legacy_smell: f64,
19}
20
21impl RadarData {
22 pub fn dimensions(&self) -> [(&str, f64); 6] {
23 [
24 ("Complexity", self.complexity),
25 ("Duplication", self.duplication),
26 ("Naming", self.naming),
27 ("Panic Risk", self.panic_risk),
28 ("Dep Hell", self.dependency_hell),
29 ("Legacy Smell", self.legacy_smell),
30 ]
31 }
32}
33
34pub fn run(
36 path: &Path,
37 format: &OutputFormat,
38 lang: &str,
39 output_path: Option<&Path>,
40) -> Result<String> {
41 let analyzer = CodeAnalyzer::new(&[], lang);
42 let issues = analyzer.analyze_path(path);
43 let data = analyze_dimensions(&issues);
44
45 match format {
46 OutputFormat::Json => Ok(display_json(&data)),
47 OutputFormat::Terminal => {
48 let svg = generate_svg(&data);
49 if let Some(out_path) = output_path {
50 std::fs::write(out_path, &svg)?;
51 Ok(format!(
52 "\n Radar chart written to {}\n",
53 out_path.display()
54 ))
55 } else {
56 Ok(display_terminal(&data, lang))
57 }
58 }
59 }
60}
61
62fn analyze_dimensions(issues: &[CodeIssue]) -> RadarData {
63 let mut counts: HashMap<&str, f64> = HashMap::new();
64
65 for issue in issues {
66 let cat = categorize(&issue.rule_name);
67 *counts.entry(cat).or_insert(0.0) += 1.0;
68 }
69
70 let normalize = |key: &str| -> f64 {
72 let count = counts.get(key).copied().unwrap_or(0.0);
73 (count * 5.0).min(100.0)
74 };
75
76 RadarData {
77 complexity: normalize("complexity"),
78 duplication: normalize("duplication"),
79 naming: normalize("naming"),
80 panic_risk: normalize("panic_risk"),
81 dependency_hell: normalize("dependency_hell"),
82 legacy_smell: normalize("legacy_smell"),
83 }
84}
85
86fn categorize(rule_name: &str) -> &'static str {
87 let lower = rule_name.to_lowercase();
88 if lower.contains("nest") || lower.contains("complex") || lower.contains("long") {
89 "complexity"
90 } else if lower.contains("duplicat") || lower.contains("copy") {
91 "duplication"
92 } else if lower.contains("name")
93 || lower.contains("single_letter")
94 || lower.contains("meaningless")
95 {
96 "naming"
97 } else if lower.contains("unwrap") {
98 "panic_risk"
99 } else {
100 "legacy_smell"
101 }
102}
103
104fn generate_svg(data: &RadarData) -> String {
106 let dims = data.dimensions();
107 let center_x: f64 = 150.0;
108 let center_y: f64 = 150.0;
109 let radius: f64 = 120.0;
110 let n = dims.len() as f64;
111 let angle_step = 2.0 * std::f64::consts::PI / n;
112
113 let mut svg = String::new();
114 svg.push_str("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"300\" height=\"340\" viewBox=\"0 0 300 340\">\n");
115 svg.push_str(" <style>\n");
116 svg.push_str(" text { font-family: sans-serif; font-size: 11px; fill: #333; }\n");
117 svg.push_str(" .title { font-size: 14px; font-weight: bold; fill: #111; }\n");
118 svg.push_str(" </style>\n");
119
120 svg.push_str(" <text x=\"150\" y=\"20\" text-anchor=\"middle\" class=\"title\">Code Smell Radar</text>\n");
122
123 for i in 1..=4 {
125 let r = radius * i as f64 / 4.0;
126 svg.push_str(&format!(
127 " <circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"none\" stroke=\"#ddd\" stroke-width=\"0.5\"/>\n",
128 center_x, center_y, r
129 ));
130 }
131
132 for (i, (label, _)) in dims.iter().enumerate() {
134 let angle = angle_step * i as f64 - std::f64::consts::PI / 2.0;
135 let x2 = center_x + radius * angle.cos();
136 let y2 = center_y + radius * angle.sin();
137 svg.push_str(&format!(
138 " <line x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"#ccc\" stroke-width=\"0.5\"/>\n",
139 center_x, center_y, x2, y2
140 ));
141
142 let label_r = radius + 18.0;
144 let lx = center_x + label_r * angle.cos();
145 let ly = center_y + label_r * angle.sin();
146 let anchor = if lx < center_x - 10.0 {
147 "end"
148 } else if lx > center_x + 10.0 {
149 "start"
150 } else {
151 "middle"
152 };
153 svg.push_str(&format!(
154 " <text x=\"{}\" y=\"{}\" text-anchor=\"{}\">{}</text>\n",
155 lx, ly, anchor, label
156 ));
157 }
158
159 let mut points = Vec::new();
161 for (i, (_, value)) in dims.iter().enumerate() {
162 let angle = angle_step * i as f64 - std::f64::consts::PI / 2.0;
163 let r = radius * (value / 100.0);
164 let x = center_x + r * angle.cos();
165 let y = center_y + r * angle.sin();
166 points.push(format!("{},{}", x, y));
167 }
168 svg.push_str(&format!(
169 " <polygon points=\"{}\" fill=\"rgba(255,99,71,0.25)\" stroke=\"#e74c3c\" stroke-width=\"2\"/>\n",
170 points.join(" ")
171 ));
172
173 for (i, (_, value)) in dims.iter().enumerate() {
175 let angle = angle_step * i as f64 - std::f64::consts::PI / 2.0;
176 let r = radius * (value / 100.0);
177 let x = center_x + r * angle.cos();
178 let y = center_y + r * angle.sin();
179 svg.push_str(&format!(
180 " <circle cx=\"{}\" cy=\"{}\" r=\"3\" fill=\"#e74c3c\"/>\n",
181 x, y
182 ));
183 svg.push_str(&format!(
185 " <text x=\"{}\" y=\"{}\" text-anchor=\"middle\" font-size=\"9\" fill=\"#666\">{:.0}</text>\n",
186 x, y - 8.0, value
187 ));
188 }
189
190 svg.push_str(" <text x=\"150\" y=\"330\" text-anchor=\"middle\" font-size=\"10\" fill=\"#666\">Higher = worse smell</text>\n");
192
193 svg.push_str("</svg>");
194 svg
195}
196
197fn display_terminal(data: &RadarData, lang: &str) -> String {
198 let mut out = String::new();
199 out.push_str(&format!(
200 "\n{}\n",
201 t(lang, "\u{1f4e1} 代码气味雷达", "\u{1f4e1} Code Smell Radar").bold()
202 ));
203 out.push_str(&format!("{}\n\n", "\u{2501}".repeat(40)));
204
205 for (label, value) in data.dimensions() {
206 let bar_len = (value / 5.0) as usize;
207 let bar: String = "\u{2588}".repeat(bar_len);
208 let bar_colored = if value >= 70.0 {
209 bar.red()
210 } else if value >= 40.0 {
211 bar.yellow()
212 } else {
213 bar.green()
214 };
215 out.push_str(&format!(" {:<16} {:>5.0} {}\n", label, value, bar_colored));
216 }
217
218 out.push_str(&format!(
219 "\n {}\n",
220 t(
221 lang,
222 "使用 --output <file.svg> 生成雷达图 SVG",
223 "Use --output <file.svg> to generate radar chart SVG"
224 )
225 ));
226
227 out
228}
229
230fn display_json(data: &RadarData) -> String {
231 serde_json::json!({
232 "complexity": data.complexity,
233 "duplication": data.duplication,
234 "naming": data.naming,
235 "panic_risk": data.panic_risk,
236 "dependency_hell": data.dependency_hell,
237 "legacy_smell": data.legacy_smell,
238 })
239 .to_string()
240}
241
242use colored::Colorize;
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_categorize() {
250 assert_eq!(categorize("deep_nesting"), "complexity");
251 assert_eq!(categorize("unwrap_abuse"), "panic_risk");
252 assert_eq!(categorize("code_duplication"), "duplication");
253 }
254
255 #[test]
256 fn test_generate_svg() {
257 let data = RadarData {
258 complexity: 50.0,
259 duplication: 30.0,
260 naming: 70.0,
261 panic_risk: 20.0,
262 dependency_hell: 10.0,
263 legacy_smell: 40.0,
264 };
265 let svg = generate_svg(&data);
266 assert!(svg.contains("<svg"));
267 assert!(svg.contains("</svg>"));
268 assert!(svg.contains("polygon"));
269 }
270
271 #[test]
272 fn test_display_terminal() {
273 let data = RadarData {
274 complexity: 50.0,
275 duplication: 30.0,
276 naming: 70.0,
277 panic_risk: 20.0,
278 dependency_hell: 10.0,
279 legacy_smell: 40.0,
280 };
281 let out = display_terminal(&data, "en-US");
282 assert!(out.contains("Code Smell Radar"));
283 }
284
285 #[test]
286 fn test_display_terminal_chinese() {
287 let data = RadarData {
288 complexity: 50.0,
289 duplication: 30.0,
290 naming: 70.0,
291 panic_risk: 20.0,
292 dependency_hell: 10.0,
293 legacy_smell: 40.0,
294 };
295 let out = display_terminal(&data, "zh-CN");
296 assert!(out.contains("代码气味雷达"));
297 }
298
299 #[test]
300 fn test_generate_svg_all_zeros() {
301 let data = RadarData {
302 complexity: 0.0,
303 duplication: 0.0,
304 naming: 0.0,
305 panic_risk: 0.0,
306 dependency_hell: 0.0,
307 legacy_smell: 0.0,
308 };
309 let svg = generate_svg(&data);
310 assert!(svg.contains("<svg"));
311 assert!(svg.contains("</svg>"));
312 }
313
314 #[test]
315 fn test_generate_svg_all_max() {
316 let data = RadarData {
317 complexity: 100.0,
318 duplication: 100.0,
319 naming: 100.0,
320 panic_risk: 100.0,
321 dependency_hell: 100.0,
322 legacy_smell: 100.0,
323 };
324 let svg = generate_svg(&data);
325 assert!(svg.contains("polygon"));
326 }
327
328 #[test]
329 fn test_display_json() {
330 let data = RadarData {
331 complexity: 50.0,
332 duplication: 30.0,
333 naming: 70.0,
334 panic_risk: 20.0,
335 dependency_hell: 10.0,
336 legacy_smell: 40.0,
337 };
338 let json = display_json(&data);
339 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
340 assert_eq!(parsed["complexity"], 50.0);
341 }
342}