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 _max_per_rule = 5;
224 let mut total_shown = 0;
225 let max_total = if self.max_issues_per_file > 0 {
226 self.max_issues_per_file
227 } else {
228 usize::MAX
229 };
230
231 let mut sorted_rules: Vec<_> = rule_groups.into_iter().collect();
233 sorted_rules.sort_by(|a, b| {
234 let severity_order = |s: &Severity| match s {
235 Severity::Nuclear => 3,
236 Severity::Spicy => 2,
237 Severity::Mild => 1,
238 };
239 let max_severity_a =
240 a.1.iter()
241 .map(|i| severity_order(&i.severity))
242 .max()
243 .unwrap_or(1);
244 let max_severity_b =
245 b.1.iter()
246 .map(|i| severity_order(&i.severity))
247 .max()
248 .unwrap_or(1);
249 max_severity_b.cmp(&max_severity_a)
250 });
251
252 for (rule_name, rule_issues) in sorted_rules {
253 if total_shown >= max_total {
254 break;
255 }
256
257 let rule_issues_len = rule_issues.len();
258
259 if rule_name.contains("naming") || rule_name.contains("single-letter") {
261 let bad_names: Vec<String> = rule_issues
263 .iter()
264 .filter_map(|issue| {
265 if let Some(start) = issue.message.find("'") {
266 issue.message[start + 1..].find("'").map(|end| {
267 issue.message[start + 1..start + 1 + end].to_string()
268 })
269 } else {
270 None
271 }
272 })
273 .take(5)
274 .collect();
275
276 let names_display = if bad_names.len() < rule_issues_len {
277 format!("{}, ...", bad_names.join(", "))
278 } else {
279 bad_names.join(", ")
280 };
281
282 let label = if self.i18n.lang == "zh-CN" {
283 "变量命名问题"
284 } else {
285 "Variable naming issues"
286 };
287
288 println!(
289 " 🏷️ {}: {} ({})",
290 label.bright_yellow().bold(),
291 rule_issues_len.to_string().bright_red().bold(),
292 names_display.bright_black()
293 );
294 total_shown += 1;
295 } else if rule_name.contains("duplication") {
296 let label = if self.i18n.lang == "zh-CN" {
297 "代码重复问题"
298 } else {
299 "Code duplication issues"
300 };
301
302 let instance_info = if let Some(first_issue) = rule_issues.first() {
304 if first_issue.message.contains("instances") {
305 let parts: Vec<&str> = first_issue.message.split_whitespace().collect();
306 if let Some(pos) = parts.iter().position(|&x| x == "instances") {
307 if pos > 0 {
308 format!("{} instances", parts[pos - 1])
309 } else {
310 "multiple instances".to_string()
311 }
312 } else {
313 if self.i18n.lang == "zh-CN" {
314 "多个代码块".to_string()
315 } else {
316 "multiple blocks".to_string()
317 }
318 }
319 } else {
320 if self.i18n.lang == "zh-CN" {
321 "多个代码块".to_string()
322 } else {
323 "multiple blocks".to_string()
324 }
325 }
326 } else {
327 if self.i18n.lang == "zh-CN" {
328 "多个代码块".to_string()
329 } else {
330 "multiple blocks".to_string()
331 }
332 };
333
334 println!(
335 " 🔄 {}: {} ({})",
336 label.bright_cyan().bold(),
337 rule_issues_len.to_string().bright_cyan().bold(),
338 instance_info.bright_black()
339 );
340 total_shown += 1;
341 } else if rule_name.contains("nesting") {
342 let label = if self.i18n.lang == "zh-CN" {
343 "嵌套深度问题"
344 } else {
345 "Nesting depth issues"
346 };
347
348 let depths: Vec<usize> = rule_issues
350 .iter()
351 .filter_map(|issue| {
352 if let Some(start) = issue.message.find("depth: ") {
353 let depth_str = &issue.message[start + 7..];
354 if let Some(end) = depth_str.find(')') {
355 depth_str[..end].parse().ok()
356 } else {
357 None
358 }
359 } else if let Some(start) = issue.message.find("深度: ") {
360 let depth_str = &issue.message[start + 6..];
361 if let Some(end) = depth_str.find(')') {
362 depth_str[..end].parse().ok()
363 } else {
364 None
365 }
366 } else {
367 None
368 }
369 })
370 .collect();
371
372 let depth_info = if !depths.is_empty() {
373 let min_depth = depths.iter().min().unwrap_or(&4);
374 let max_depth = depths.iter().max().unwrap_or(&8);
375 if min_depth == max_depth {
376 format!("depth {min_depth}")
377 } else {
378 format!("depth {min_depth}-{max_depth}")
379 }
380 } else {
381 if self.i18n.lang == "zh-CN" {
382 "深度嵌套".to_string()
383 } else {
384 "deep nesting".to_string()
385 }
386 };
387
388 println!(
389 " 📦 {}: {} ({})",
390 label.bright_magenta().bold(),
391 rule_issues_len.to_string().bright_magenta().bold(),
392 depth_info.bright_black()
393 );
394 total_shown += 1;
395 } else {
396 let display_name = match (self.i18n.lang.as_str(), rule_name.as_str()) {
398 ("zh-CN", "panic-abuse") => "panic 滥用",
399 ("zh-CN", "god-function") => "上帝函数",
400 ("zh-CN", "magic-number") => "魔法数字",
401 ("zh-CN", "todo-comment") => "TODO 注释",
402 ("zh-CN", "println-debugging") => "println 调试",
403 ("zh-CN", "string-abuse") => "String 滥用",
404 ("zh-CN", "vec-abuse") => "Vec 滥用",
405 ("zh-CN", "iterator-abuse") => "迭代器滥用",
406 ("zh-CN", "match-abuse") => "Match 滥用",
407 ("zh-CN", "hungarian-notation") => "匈牙利命名法",
408 ("zh-CN", "abbreviation-abuse") => "过度缩写",
409 ("zh-CN", "meaningless-naming") => "无意义命名",
410 ("zh-CN", "commented-code") => "被注释代码",
411 ("zh-CN", "dead-code") => "死代码",
412 _ => &rule_name.replace("-", " "),
413 };
414 println!(
415 " ⚠️ {}: {}",
416 display_name.bright_yellow().bold(),
417 rule_issues_len.to_string().bright_yellow().bold()
418 );
419 total_shown += 1;
420 }
421 }
422 println!();
423 }
424 }
425
426 fn print_footer(&self, _issues: &[CodeIssue]) {
427 println!();
428 println!("{}", self.i18n.get("suggestions").bright_cyan().bold());
429 println!("{}", "─".repeat(50).bright_black());
430
431 println!();
432 let footer_message = if self.savage_mode {
433 match self.i18n.lang.as_str() {
434 "zh-CN" => "记住:写垃圾代码容易,写好代码需要用心 💪".to_string(),
435 _ => "Remember: writing garbage code is easy, writing good code requires effort 💪"
436 .to_string(),
437 }
438 } else {
439 self.i18n.get("keep_improving")
440 };
441
442 let color = if self.savage_mode {
443 footer_message.bright_red().bold()
444 } else {
445 footer_message.bright_green().bold()
446 };
447
448 println!("{color}");
449 }
450
451 fn print_top_files(&self, issues: &[CodeIssue]) {
452 if self.top_files == 0 {
453 return;
454 }
455
456 let mut file_issue_counts: HashMap<String, usize> = HashMap::new();
457 for issue in issues {
458 let file_name = issue
459 .file_path
460 .file_name()
461 .unwrap_or_default()
462 .to_string_lossy()
463 .to_string();
464 *file_issue_counts.entry(file_name).or_insert(0) += 1;
465 }
466
467 let mut sorted_files: Vec<_> = file_issue_counts.into_iter().collect();
468 sorted_files.sort_by_key(|b| std::cmp::Reverse(b.1));
469
470 if !sorted_files.is_empty() {
471 println!("{}", self.i18n.get("top_files").bright_yellow().bold());
472 println!("{}", "─".repeat(50).bright_black());
473
474 for (i, (file_name, count)) in sorted_files.iter().take(self.top_files).enumerate() {
475 let rank = format!("{}.", i + 1);
476 println!(
477 " {} {} ({} issues)",
478 rank.bright_white(),
479 file_name.bright_blue(),
480 count.to_string().red()
481 );
482 }
483 println!();
484 }
485 }
486
487 fn print_detailed_analysis(&self, issues: &[CodeIssue]) {
488 println!(
489 "{}",
490 self.i18n.get("detailed_analysis").bright_magenta().bold()
491 );
492 println!("{}", "─".repeat(50).bright_black());
493
494 let mut rule_stats: HashMap<String, usize> = HashMap::new();
495 for issue in issues {
496 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
497 }
498
499 let rule_descriptions = match self.i18n.lang.as_str() {
500 "zh-CN" => [
501 ("terrible-naming", "糟糕的变量命名"),
502 ("single-letter-variable", "单字母变量"),
503 ("deep-nesting", "过度嵌套"),
504 ("long-function", "超长函数"),
505 ("unwrap-abuse", "unwrap() 滥用"),
506 ("unnecessary-clone", "不必要的 clone()"),
507 ]
508 .iter()
509 .cloned()
510 .collect::<HashMap<_, _>>(),
511 _ => [
512 ("terrible-naming", "Terrible variable naming"),
513 ("single-letter-variable", "Single letter variables"),
514 ("deep-nesting", "Deep nesting"),
515 ("long-function", "Long functions"),
516 ("unwrap-abuse", "unwrap() abuse"),
517 ("unnecessary-clone", "Unnecessary clone()"),
518 ]
519 .iter()
520 .cloned()
521 .collect::<HashMap<_, _>>(),
522 };
523
524 for (rule_name, count) in rule_stats {
525 let rule_name_str = rule_name.as_str();
526
527 let display_name = if self.i18n.lang == "zh-CN" {
529 match rule_name_str {
530 "terrible-naming" => "糟糕的变量命名",
531 "single-letter-variable" => "单字母变量",
532 "deep-nesting" => "过度嵌套",
533 "long-function" => "超长函数",
534 "unwrap-abuse" => "unwrap() 滥用",
535 "unnecessary-clone" => "不必要的 clone()",
536 "panic-abuse" => "panic 滥用",
537 "god-function" => "上帝函数",
538 "magic-number" => "魔法数字",
539 "todo-comment" => "TODO 注释",
540 "println-debugging" => "println 调试",
541 "string-abuse" => "String 滥用",
542 "vec-abuse" => "Vec 滥用",
543 "iterator-abuse" => "迭代器滥用",
544 "match-abuse" => "Match 滥用",
545 "hungarian-notation" => "匈牙利命名法",
546 "abbreviation-abuse" => "过度缩写",
547 "meaningless-naming" => "无意义命名",
548 "commented-code" => "被注释代码",
549 "dead-code" => "死代码",
550 "code-duplication" => "代码重复",
551 "macro-abuse" => "宏滥用",
552 _ => rule_name_str,
553 }
554 } else {
555 rule_descriptions
556 .get(rule_name_str)
557 .unwrap_or(&rule_name_str)
558 };
559
560 let issues_text = if self.i18n.lang == "zh-CN" {
561 "个问题"
562 } else {
563 "issues"
564 };
565
566 println!(
567 " 📌 {}: {} {}",
568 display_name.cyan(),
569 count.to_string().yellow(),
570 issues_text
571 );
572 }
573 println!();
574 }
575
576 fn print_markdown_report(&self, issues: &[CodeIssue], roasts: &RoastMap) {
577 let total = issues.len();
578 let nuclear = issues
579 .iter()
580 .filter(|i| matches!(i.severity, Severity::Nuclear))
581 .count();
582 let spicy = issues
583 .iter()
584 .filter(|i| matches!(i.severity, Severity::Spicy))
585 .count();
586 let mild = issues
587 .iter()
588 .filter(|i| matches!(i.severity, Severity::Mild))
589 .count();
590
591 println!("# {}", self.i18n.get("title"));
592 println!();
593 println!("## {}", self.i18n.get("statistics"));
594 println!();
595 println!("| Severity | Count | Description |");
596 println!("| --- | --- | --- |");
597 println!(
598 "| 🔥 Nuclear | {} | {} |",
599 nuclear,
600 self.i18n.get("nuclear_issues")
601 );
602 println!(
603 "| 🌶️ Spicy | {} | {} |",
604 spicy,
605 self.i18n.get("spicy_issues")
606 );
607 println!("| 😐 Mild | {} | {} |", mild, self.i18n.get("mild_issues"));
608 println!(
609 "| **Total** | **{}** | **{}** |",
610 total,
611 self.i18n.get("total")
612 );
613 println!();
614
615 if self.verbose {
616 println!("## {}", self.i18n.get("detailed_analysis"));
617 println!();
618
619 let mut rule_stats: HashMap<String, usize> = HashMap::new();
620 for issue in issues {
621 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
622 }
623
624 for (rule_name, count) in rule_stats {
625 println!("- **{}**: {} issues", rule_name, count);
626 }
627 println!();
628 }
629
630 println!("## Issues by File");
631 println!();
632
633 let mut file_groups: BTreeMap<String, Vec<&CodeIssue>> = BTreeMap::new();
634 for issue in issues {
635 let file_name = issue
636 .file_path
637 .file_name()
638 .unwrap_or_default()
639 .to_string_lossy()
640 .to_string();
641 file_groups.entry(file_name).or_default().push(issue);
642 }
643
644 for (file_name, file_issues) in file_groups {
645 println!("### 📁 {}", file_name);
646 println!();
647
648 let issues_to_show = if self.max_issues_per_file > 0 {
649 file_issues
650 .into_iter()
651 .take(self.max_issues_per_file)
652 .collect::<Vec<_>>()
653 } else {
654 file_issues
655 };
656
657 for issue in issues_to_show {
658 let severity_icon = match issue.severity {
659 Severity::Nuclear => "💥",
660 Severity::Spicy => "🌶️",
661 Severity::Mild => "😐",
662 };
663
664 let key = format!(
665 "{}:{}:{}",
666 issue.file_path.display(),
667 issue.line,
668 issue.rule_name
669 );
670 let message = roasts
671 .get(&key)
672 .cloned()
673 .unwrap_or_else(|| issue.message.clone());
674
675 println!(
676 "- {} **Line {}:{}** - {}",
677 severity_icon, issue.line, issue.column, message
678 );
679 }
680 println!();
681 }
682
683 println!("## {}", self.i18n.get("suggestions"));
684 println!();
685
686 println!();
687 }
688}