1mod display;
2
3use colored::*;
4use std::collections::{BTreeMap, HashMap};
5
6use crate::analyzer::{CodeIssue, Severity};
7use crate::i18n::I18n;
8use crate::llm::{RoastMap, RoastProvider};
9use crate::scoring::{CodeQualityScore, CodeScorer};
10
11pub struct Reporter {
12 harsh_mode: bool,
13 savage_mode: bool,
14 verbose: bool,
15 top_files: usize,
16 max_issues_per_file: usize,
17 summary_only: bool,
18 markdown: bool,
19 i18n: I18n,
20 roast_provider: Box<dyn RoastProvider>,
21}
22
23impl Reporter {
24 #[allow(clippy::too_many_arguments)]
25 pub fn new(
26 harsh_mode: bool,
27 savage_mode: bool,
28 verbose: bool,
29 top_files: usize,
30 max_issues_per_file: usize,
31 summary_only: bool,
32 markdown: bool,
33 lang: &str,
34 roast_provider: Box<dyn RoastProvider>,
35 ) -> Self {
36 Self {
37 harsh_mode,
38 savage_mode,
39 verbose,
40 top_files,
41 max_issues_per_file,
42 summary_only,
43 markdown,
44 i18n: I18n::new(lang),
45 roast_provider,
46 }
47 }
48
49 pub fn report_with_metrics(
50 &self,
51 mut issues: Vec<CodeIssue>,
52 file_count: usize,
53 total_lines: usize,
54 ) {
55 let scorer = CodeScorer::new();
57 let quality_score = scorer.calculate_score(&issues, file_count, total_lines);
58
59 if issues.is_empty() {
60 self.print_clean_code_message_with_score(&quality_score);
61 return;
62 }
63
64 issues.sort_by(|a, b| {
66 let severity_order = |s: &Severity| match s {
67 Severity::Nuclear => 3,
68 Severity::Spicy => 2,
69 Severity::Mild => 1,
70 };
71 severity_order(&b.severity).cmp(&severity_order(&a.severity))
72 });
73
74 if self.harsh_mode {
76 issues.retain(|issue| matches!(issue.severity, Severity::Nuclear | Severity::Spicy));
77 }
78
79 let roasts = self
81 .roast_provider
82 .generate_roasts(&issues, &self.i18n.lang);
83
84 if self.markdown {
85 self.print_markdown_report(&issues, &roasts);
86 } else {
87 if !self.summary_only {
88 self.print_header(&issues);
89 self.print_quality_score(&quality_score);
90 if self.verbose {
91 self.print_detailed_analysis(&issues);
92 }
93 self.print_top_files(&issues);
94 self.print_issues(&issues);
95 }
96 self.print_summary_with_score(&issues, &quality_score);
97 if !self.summary_only {
98 self.print_footer(&issues);
99 }
100 }
101 }
102
103 fn print_clean_code_message_with_score(&self, quality_score: &CodeQualityScore) {
104 if self.markdown {
105 println!("# {}", self.i18n.get("title"));
106 println!();
107 println!("## 🏆 代码质量评分");
108 println!();
109 println!(
110 "**评分**: {:.1}/100 {}",
111 quality_score.total_score,
112 quality_score.quality_level.emoji()
113 );
114 println!(
115 "**等级**: {}",
116 quality_score.quality_level.description(&self.i18n.lang)
117 );
118 println!();
119 println!("{}", self.i18n.get("clean_code"));
120 println!();
121 println!("{}", self.i18n.get("clean_code_warning"));
122 } else {
123 println!("{}", self.i18n.get("clean_code").bright_green().bold());
124 println!();
125 println!(
126 "{} 代码质量评分: {:.1}/100 {}",
127 "🏆".bright_yellow(),
128 quality_score.total_score.to_string().bright_green().bold(),
129 quality_score.quality_level.emoji()
130 );
131 println!(
132 "{} 质量等级: {}",
133 "📊".bright_blue(),
134 quality_score
135 .quality_level
136 .description(&self.i18n.lang)
137 .bright_green()
138 .bold()
139 );
140 println!("{}", self.i18n.get("clean_code_warning").yellow());
141 }
142 }
143
144 fn print_header(&self, issues: &[CodeIssue]) {
145 let total = issues.len();
146 let nuclear = issues
147 .iter()
148 .filter(|i| matches!(i.severity, Severity::Nuclear))
149 .count();
150 let spicy = issues
151 .iter()
152 .filter(|i| matches!(i.severity, Severity::Spicy))
153 .count();
154 let mild = issues
155 .iter()
156 .filter(|i| matches!(i.severity, Severity::Mild))
157 .count();
158
159 println!("{}", self.i18n.get("title").bright_red().bold());
160 println!("{}", self.i18n.get("preparing").yellow());
161 println!();
162
163 println!("{}", self.i18n.get("report_title").bright_red().bold());
164 println!("{}", "─".repeat(50).bright_black());
165
166 if self.savage_mode {
167 println!("{}", self.i18n.get("found_issues").red().bold());
168 } else {
169 println!("{}", self.i18n.get("found_issues").yellow());
170 }
171
172 println!();
173 println!("{}", self.i18n.get("statistics"));
174 println!(
175 " {} {}",
176 nuclear.to_string().red().bold(),
177 self.i18n.get("nuclear_issues")
178 );
179 println!(
180 " {} {}",
181 spicy.to_string().yellow().bold(),
182 self.i18n.get("spicy_issues")
183 );
184 println!(
185 " {} {}",
186 mild.to_string().blue().bold(),
187 self.i18n.get("mild_issues")
188 );
189 println!(
190 " {} {}",
191 total.to_string().bright_white().bold(),
192 self.i18n.get("total")
193 );
194 println!();
195 }
196
197 fn print_issues(&self, issues: &[CodeIssue]) {
198 let mut file_groups: HashMap<String, Vec<&CodeIssue>> = HashMap::new();
199
200 for issue in issues {
201 let file_name = issue
202 .file_path
203 .file_name()
204 .unwrap_or_default()
205 .to_string_lossy()
206 .to_string();
207 file_groups.entry(file_name).or_default().push(issue);
208 }
209
210 for (file_name, file_issues) in file_groups {
211 println!("{} {}", "📁".bright_blue(), file_name.bright_blue().bold());
212
213 let mut rule_groups: BTreeMap<String, Vec<&CodeIssue>> = BTreeMap::new();
215 for issue in &file_issues {
216 rule_groups
217 .entry(issue.rule_name.clone())
218 .or_default()
219 .push(issue);
220 }
221
222 let mut total_shown = 0;
224 let max_total = if self.max_issues_per_file > 0 {
225 self.max_issues_per_file
226 } else {
227 usize::MAX
228 };
229
230 let mut sorted_rules: Vec<_> = rule_groups.into_iter().collect();
232 sorted_rules.sort_by(|a, b| {
233 let severity_order = |s: &Severity| match s {
234 Severity::Nuclear => 3,
235 Severity::Spicy => 2,
236 Severity::Mild => 1,
237 };
238 let max_severity_a =
239 a.1.iter()
240 .map(|i| severity_order(&i.severity))
241 .max()
242 .unwrap_or(1);
243 let max_severity_b =
244 b.1.iter()
245 .map(|i| severity_order(&i.severity))
246 .max()
247 .unwrap_or(1);
248 max_severity_b.cmp(&max_severity_a)
249 });
250
251 for (rule_name, rule_issues) in sorted_rules {
252 if total_shown >= max_total {
253 break;
254 }
255
256 let rule_issues_len = rule_issues.len();
257
258 if rule_name.contains("naming") || rule_name.contains("single-letter") {
260 let bad_names: Vec<String> = rule_issues
262 .iter()
263 .filter_map(|issue| {
264 if let Some(start) = issue.message.find("'") {
265 issue.message[start + 1..].find("'").map(|end| {
266 issue.message[start + 1..start + 1 + end].to_string()
267 })
268 } else {
269 None
270 }
271 })
272 .take(5)
273 .collect();
274
275 let names_display = if bad_names.len() < rule_issues_len {
276 format!("{}, ...", bad_names.join(", "))
277 } else {
278 bad_names.join(", ")
279 };
280
281 let label = if self.i18n.lang == "zh-CN" {
282 "变量命名问题"
283 } else {
284 "Variable naming issues"
285 };
286
287 println!(
288 " 🏷️ {}: {} ({})",
289 label.bright_yellow().bold(),
290 rule_issues_len.to_string().bright_red().bold(),
291 names_display.bright_black()
292 );
293 total_shown += 1;
294 } else if rule_name.contains("duplication") {
295 let label = if self.i18n.lang == "zh-CN" {
296 "代码重复问题"
297 } else {
298 "Code duplication issues"
299 };
300
301 let instance_info = if let Some(first_issue) = rule_issues.first() {
303 if first_issue.message.contains("instances") {
304 let parts: Vec<&str> = first_issue.message.split_whitespace().collect();
305 if let Some(pos) = parts.iter().position(|&x| x == "instances") {
306 if pos > 0 {
307 format!("{} instances", parts[pos - 1])
308 } else {
309 "multiple instances".to_string()
310 }
311 } else {
312 if self.i18n.lang == "zh-CN" {
313 "多个代码块".to_string()
314 } else {
315 "multiple blocks".to_string()
316 }
317 }
318 } else {
319 if self.i18n.lang == "zh-CN" {
320 "多个代码块".to_string()
321 } else {
322 "multiple blocks".to_string()
323 }
324 }
325 } else {
326 if self.i18n.lang == "zh-CN" {
327 "多个代码块".to_string()
328 } else {
329 "multiple blocks".to_string()
330 }
331 };
332
333 println!(
334 " 🔄 {}: {} ({})",
335 label.bright_cyan().bold(),
336 rule_issues_len.to_string().bright_cyan().bold(),
337 instance_info.bright_black()
338 );
339 total_shown += 1;
340 } else if rule_name.contains("nesting") {
341 let label = if self.i18n.lang == "zh-CN" {
342 "嵌套深度问题"
343 } else {
344 "Nesting depth issues"
345 };
346
347 let depths: Vec<usize> = rule_issues
349 .iter()
350 .filter_map(|issue| {
351 if let Some(start) = issue.message.find("depth: ") {
352 let depth_str = &issue.message[start + 7..];
353 if let Some(end) = depth_str.find(')') {
354 depth_str[..end].parse().ok()
355 } else {
356 None
357 }
358 } else if let Some(start) = issue.message.find("深度: ") {
359 let depth_str = &issue.message[start + 6..];
360 if let Some(end) = depth_str.find(')') {
361 depth_str[..end].parse().ok()
362 } else {
363 None
364 }
365 } else {
366 None
367 }
368 })
369 .collect();
370
371 let depth_info = if !depths.is_empty() {
372 let min_depth = depths.iter().min().unwrap_or(&4);
373 let max_depth = depths.iter().max().unwrap_or(&8);
374 if min_depth == max_depth {
375 format!("depth {min_depth}")
376 } else {
377 format!("depth {min_depth}-{max_depth}")
378 }
379 } else {
380 if self.i18n.lang == "zh-CN" {
381 "深度嵌套".to_string()
382 } else {
383 "deep nesting".to_string()
384 }
385 };
386
387 println!(
388 " 📦 {}: {} ({})",
389 label.bright_magenta().bold(),
390 rule_issues_len.to_string().bright_magenta().bold(),
391 depth_info.bright_black()
392 );
393 total_shown += 1;
394 } else {
395 let display_name = match (self.i18n.lang.as_str(), rule_name.as_str()) {
397 ("zh-CN", "panic-abuse") => "panic 滥用",
398 ("zh-CN", "god-function") => "上帝函数",
399 ("zh-CN", "magic-number") => "魔法数字",
400 ("zh-CN", "todo-comment") => "TODO 注释",
401 ("zh-CN", "println-debugging") => "println 调试",
402 ("zh-CN", "string-abuse") => "String 滥用",
403 ("zh-CN", "vec-abuse") => "Vec 滥用",
404 ("zh-CN", "iterator-abuse") => "迭代器滥用",
405 ("zh-CN", "match-abuse") => "Match 滥用",
406 ("zh-CN", "hungarian-notation") => "匈牙利命名法",
407 ("zh-CN", "abbreviation-abuse") => "过度缩写",
408 ("zh-CN", "meaningless-naming") => "无意义命名",
409 ("zh-CN", "commented-code") => "被注释代码",
410 ("zh-CN", "dead-code") => "死代码",
411 _ => &rule_name.replace("-", " "),
412 };
413 println!(
414 " ⚠️ {}: {}",
415 display_name.bright_yellow().bold(),
416 rule_issues_len.to_string().bright_yellow().bold()
417 );
418 total_shown += 1;
419 }
420 }
421 println!();
422 }
423 }
424
425 fn print_footer(&self, _issues: &[CodeIssue]) {
426 println!();
427 println!("{}", self.i18n.get("suggestions").bright_cyan().bold());
428 println!("{}", "─".repeat(50).bright_black());
429
430 println!();
431 let footer_message = if self.savage_mode {
432 match self.i18n.lang.as_str() {
433 "zh-CN" => "记住:写垃圾代码容易,写好代码需要用心 💪".to_string(),
434 _ => "Remember: writing garbage code is easy, writing good code requires effort 💪"
435 .to_string(),
436 }
437 } else {
438 self.i18n.get("keep_improving")
439 };
440
441 let color = if self.savage_mode {
442 footer_message.bright_red().bold()
443 } else {
444 footer_message.bright_green().bold()
445 };
446
447 println!("{color}");
448 }
449
450 fn print_top_files(&self, issues: &[CodeIssue]) {
451 if self.top_files == 0 {
452 return;
453 }
454
455 let mut file_issue_counts: HashMap<String, usize> = HashMap::new();
456 for issue in issues {
457 let file_name = issue
458 .file_path
459 .file_name()
460 .unwrap_or_default()
461 .to_string_lossy()
462 .to_string();
463 *file_issue_counts.entry(file_name).or_insert(0) += 1;
464 }
465
466 let mut sorted_files: Vec<_> = file_issue_counts.into_iter().collect();
467 sorted_files.sort_by_key(|b| std::cmp::Reverse(b.1));
468
469 if !sorted_files.is_empty() {
470 println!("{}", self.i18n.get("top_files").bright_yellow().bold());
471 println!("{}", "─".repeat(50).bright_black());
472
473 for (i, (file_name, count)) in sorted_files.iter().take(self.top_files).enumerate() {
474 let rank = format!("{}.", i + 1);
475 println!(
476 " {} {} ({} issues)",
477 rank.bright_white(),
478 file_name.bright_blue(),
479 count.to_string().red()
480 );
481 }
482 println!();
483 }
484 }
485
486 fn print_detailed_analysis(&self, issues: &[CodeIssue]) {
487 println!(
488 "{}",
489 self.i18n.get("detailed_analysis").bright_magenta().bold()
490 );
491 println!("{}", "─".repeat(50).bright_black());
492
493 let mut rule_stats: HashMap<String, usize> = HashMap::new();
494 for issue in issues {
495 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
496 }
497
498 let rule_descriptions = match self.i18n.lang.as_str() {
499 "zh-CN" => [
500 ("terrible-naming", "糟糕的变量命名"),
501 ("single-letter-variable", "单字母变量"),
502 ("deep-nesting", "过度嵌套"),
503 ("long-function", "超长函数"),
504 ("unwrap-abuse", "unwrap() 滥用"),
505 ("unnecessary-clone", "不必要的 clone()"),
506 ]
507 .iter()
508 .cloned()
509 .collect::<HashMap<_, _>>(),
510 _ => [
511 ("terrible-naming", "Terrible variable naming"),
512 ("single-letter-variable", "Single letter variables"),
513 ("deep-nesting", "Deep nesting"),
514 ("long-function", "Long functions"),
515 ("unwrap-abuse", "unwrap() abuse"),
516 ("unnecessary-clone", "Unnecessary clone()"),
517 ]
518 .iter()
519 .cloned()
520 .collect::<HashMap<_, _>>(),
521 };
522
523 for (rule_name, count) in rule_stats {
524 let rule_name_str = rule_name.as_str();
525
526 let display_name = if self.i18n.lang == "zh-CN" {
528 match rule_name_str {
529 "terrible-naming" => "糟糕的变量命名",
530 "single-letter-variable" => "单字母变量",
531 "deep-nesting" => "过度嵌套",
532 "long-function" => "超长函数",
533 "unwrap-abuse" => "unwrap() 滥用",
534 "unnecessary-clone" => "不必要的 clone()",
535 "panic-abuse" => "panic 滥用",
536 "god-function" => "上帝函数",
537 "magic-number" => "魔法数字",
538 "todo-comment" => "TODO 注释",
539 "println-debugging" => "println 调试",
540 "string-abuse" => "String 滥用",
541 "vec-abuse" => "Vec 滥用",
542 "iterator-abuse" => "迭代器滥用",
543 "match-abuse" => "Match 滥用",
544 "hungarian-notation" => "匈牙利命名法",
545 "abbreviation-abuse" => "过度缩写",
546 "meaningless-naming" => "无意义命名",
547 "commented-code" => "被注释代码",
548 "dead-code" => "死代码",
549 "code-duplication" => "代码重复",
550 "macro-abuse" => "宏滥用",
551 _ => rule_name_str,
552 }
553 } else {
554 rule_descriptions
555 .get(rule_name_str)
556 .unwrap_or(&rule_name_str)
557 };
558
559 let issues_text = if self.i18n.lang == "zh-CN" {
560 "个问题"
561 } else {
562 "issues"
563 };
564
565 println!(
566 " 📌 {}: {} {}",
567 display_name.cyan(),
568 count.to_string().yellow(),
569 issues_text
570 );
571 }
572 println!();
573 }
574
575 fn print_markdown_report(&self, issues: &[CodeIssue], roasts: &RoastMap) {
576 let total = issues.len();
577 let nuclear = issues
578 .iter()
579 .filter(|i| matches!(i.severity, Severity::Nuclear))
580 .count();
581 let spicy = issues
582 .iter()
583 .filter(|i| matches!(i.severity, Severity::Spicy))
584 .count();
585 let mild = issues
586 .iter()
587 .filter(|i| matches!(i.severity, Severity::Mild))
588 .count();
589
590 println!("# {}", self.i18n.get("title"));
591 println!();
592 println!("## {}", self.i18n.get("statistics"));
593 println!();
594 println!("| Severity | Count | Description |");
595 println!("| --- | --- | --- |");
596 println!(
597 "| 🔥 Nuclear | {} | {} |",
598 nuclear,
599 self.i18n.get("nuclear_issues")
600 );
601 println!(
602 "| 🌶️ Spicy | {} | {} |",
603 spicy,
604 self.i18n.get("spicy_issues")
605 );
606 println!("| 😐 Mild | {} | {} |", mild, self.i18n.get("mild_issues"));
607 println!(
608 "| **Total** | **{}** | **{}** |",
609 total,
610 self.i18n.get("total")
611 );
612 println!();
613
614 if self.verbose {
615 println!("## {}", self.i18n.get("detailed_analysis"));
616 println!();
617
618 let mut rule_stats: HashMap<String, usize> = HashMap::new();
619 for issue in issues {
620 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
621 }
622
623 for (rule_name, count) in rule_stats {
624 println!("- **{}**: {} issues", rule_name, count);
625 }
626 println!();
627 }
628
629 println!("## Issues by File");
630 println!();
631
632 let mut file_groups: BTreeMap<String, Vec<&CodeIssue>> = BTreeMap::new();
633 for issue in issues {
634 let file_name = issue
635 .file_path
636 .file_name()
637 .unwrap_or_default()
638 .to_string_lossy()
639 .to_string();
640 file_groups.entry(file_name).or_default().push(issue);
641 }
642
643 for (file_name, file_issues) in file_groups {
644 println!("### 📁 {}", file_name);
645 println!();
646
647 let issues_to_show = if self.max_issues_per_file > 0 {
648 file_issues
649 .into_iter()
650 .take(self.max_issues_per_file)
651 .collect::<Vec<_>>()
652 } else {
653 file_issues
654 };
655
656 for issue in issues_to_show {
657 let severity_icon = match issue.severity {
658 Severity::Nuclear => "💥",
659 Severity::Spicy => "🌶️",
660 Severity::Mild => "😐",
661 };
662
663 let key = format!(
664 "{}:{}:{}",
665 issue.file_path.display(),
666 issue.line,
667 issue.rule_name
668 );
669 let message = roasts
670 .get(&key)
671 .cloned()
672 .unwrap_or_else(|| issue.message.clone());
673
674 println!(
675 "- {} **Line {}:{}** - {}",
676 severity_icon, issue.line, issue.column, message
677 );
678 }
679 println!();
680 }
681
682 println!("## {}", self.i18n.get("suggestions"));
683 println!();
684
685 println!();
686 }
687}