1use colored::*;
2use std::collections::HashMap;
3
4use crate::analyzer::{CodeIssue, Severity};
5use crate::i18n::I18n;
6use crate::scoring::{CodeScorer, CodeQualityScore};
7
8pub struct Reporter {
9 harsh_mode: bool,
10 savage_mode: bool,
11 verbose: bool,
12 top_files: usize,
13 max_issues_per_file: usize,
14 summary_only: bool,
15 markdown: bool,
16 i18n: I18n,
17}
18
19impl Reporter {
20 pub fn new(
21 harsh_mode: bool,
22 savage_mode: bool,
23 verbose: bool,
24 top_files: usize,
25 max_issues_per_file: usize,
26 summary_only: bool,
27 markdown: bool,
28 lang: &str,
29 ) -> Self {
30 Self {
31 harsh_mode,
32 savage_mode,
33 verbose,
34 top_files,
35 max_issues_per_file,
36 summary_only,
37 markdown,
38 i18n: I18n::new(lang),
39 }
40 }
41
42 #[allow(dead_code)]
43 pub fn report(&self, issues: Vec<CodeIssue>) {
44 self.report_with_metrics(issues, 1, 100);
45 }
46
47 pub fn report_with_metrics(&self, mut issues: Vec<CodeIssue>, file_count: usize, total_lines: usize) {
48 let scorer = CodeScorer::new();
50 let quality_score = scorer.calculate_score(&issues, file_count, total_lines);
51
52 if issues.is_empty() {
53 self.print_clean_code_message_with_score(&quality_score);
54 return;
55 }
56
57 issues.sort_by(|a, b| {
59 let severity_order = |s: &Severity| match s {
60 Severity::Nuclear => 3,
61 Severity::Spicy => 2,
62 Severity::Mild => 1,
63 };
64 severity_order(&b.severity).cmp(&severity_order(&a.severity))
65 });
66
67 if self.harsh_mode {
69 issues.retain(|issue| matches!(issue.severity, Severity::Nuclear | Severity::Spicy));
70 }
71
72 if self.markdown {
73 self.print_markdown_report(&issues);
74 } else {
75 if !self.summary_only {
76 self.print_header(&issues);
77 self.print_quality_score(&quality_score);
78 if self.verbose {
79 self.print_detailed_analysis(&issues);
80 }
81 self.print_top_files(&issues);
82 self.print_issues(&issues);
83 }
84 self.print_summary_with_score(&issues, &quality_score);
85 if !self.summary_only {
86 self.print_footer(&issues);
87 }
88 }
89 }
90
91 #[allow(dead_code)]
92 fn print_clean_code_message(&self) {
93 if self.markdown {
94 println!("# {}", self.i18n.get("title"));
95 println!();
96 println!("{}", self.i18n.get("clean_code"));
97 println!();
98 println!("{}", self.i18n.get("clean_code_warning"));
99 } else {
100 println!("{}", self.i18n.get("clean_code").bright_green().bold());
101 println!("{}", self.i18n.get("clean_code_warning").yellow());
102 }
103 }
104
105 fn print_clean_code_message_with_score(&self, quality_score: &CodeQualityScore) {
106 if self.markdown {
107 println!("# {}", self.i18n.get("title"));
108 println!();
109 println!("## 🏆 代码质量评分");
110 println!();
111 println!("**评分**: {:.1}/100 {}", quality_score.total_score, quality_score.quality_level.emoji());
112 println!("**等级**: {}", quality_score.quality_level.description(&self.i18n.lang));
113 println!();
114 println!("{}", self.i18n.get("clean_code"));
115 println!();
116 println!("{}", self.i18n.get("clean_code_warning"));
117 } else {
118 println!("{}", self.i18n.get("clean_code").bright_green().bold());
119 println!();
120 println!("{} 代码质量评分: {:.1}/100 {}",
121 "🏆".bright_yellow(),
122 quality_score.total_score.to_string().bright_green().bold(),
123 quality_score.quality_level.emoji()
124 );
125 println!("{} 质量等级: {}",
126 "📊".bright_blue(),
127 quality_score.quality_level.description(&self.i18n.lang).bright_green().bold()
128 );
129 println!("{}", self.i18n.get("clean_code_warning").yellow());
130 }
131 }
132
133 fn print_quality_score(&self, quality_score: &CodeQualityScore) {
134 println!("{}", "🏆 代码质量评分".bright_yellow().bold());
135 println!("{}", "─".repeat(50).bright_black());
136
137 let score_color = match quality_score.quality_level {
138 crate::scoring::QualityLevel::Excellent => quality_score.total_score.to_string().bright_green().bold(),
139 crate::scoring::QualityLevel::Good => quality_score.total_score.to_string().green(),
140 crate::scoring::QualityLevel::Average => quality_score.total_score.to_string().yellow(),
141 crate::scoring::QualityLevel::Poor => quality_score.total_score.to_string().red(),
142 crate::scoring::QualityLevel::Terrible => quality_score.total_score.to_string().bright_red().bold(),
143 };
144
145 println!(" 📊 总分: {:.1}/100 {}",
146 score_color,
147 quality_score.quality_level.emoji()
148 );
149 println!(" 🎯 等级: {}",
150 quality_score.quality_level.description(&self.i18n.lang).bright_white().bold()
151 );
152
153 if quality_score.total_lines > 0 {
154 println!(" 📏 代码行数: {}", quality_score.total_lines.to_string().cyan());
155 println!(" 📁 文件数量: {}", quality_score.file_count.to_string().cyan());
156 println!(" 🔍 问题密度: {:.2} 问题/千行", quality_score.issue_density.to_string().cyan());
157 }
158
159 if quality_score.severity_distribution.nuclear > 0 ||
161 quality_score.severity_distribution.spicy > 0 ||
162 quality_score.severity_distribution.mild > 0 {
163 println!();
164 println!(" 🎭 问题分布:");
165 if quality_score.severity_distribution.nuclear > 0 {
166 println!(" 💥 核弹级: {}", quality_score.severity_distribution.nuclear.to_string().red().bold());
167 }
168 if quality_score.severity_distribution.spicy > 0 {
169 println!(" 🌶️ 严重: {}", quality_score.severity_distribution.spicy.to_string().yellow());
170 }
171 if quality_score.severity_distribution.mild > 0 {
172 println!(" 😐 轻微: {}", quality_score.severity_distribution.mild.to_string().blue());
173 }
174 }
175
176 if !quality_score.category_scores.is_empty() && self.verbose {
178 println!();
179 println!(" 📋 分类得分:");
180 let mut sorted_categories: Vec<_> = quality_score.category_scores.iter().collect();
181 sorted_categories.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
182
183 for (category, score) in sorted_categories.iter().take(5) {
184 let category_name = match category.as_str() {
185 "naming" => "命名规范",
186 "complexity" => "复杂度",
187 "rust-basics" => "Rust基础",
188 "advanced-rust" => "高级特性",
189 "rust-features" => "Rust功能",
190 "structure" => "代码结构",
191 _ => category,
192 };
193 println!(" {} {:.1}", category_name.cyan(), score.to_string().yellow());
194 }
195 }
196
197 println!();
198 }
199
200 fn print_header(&self, issues: &[CodeIssue]) {
201 let total = issues.len();
202 let nuclear = issues
203 .iter()
204 .filter(|i| matches!(i.severity, Severity::Nuclear))
205 .count();
206 let spicy = issues
207 .iter()
208 .filter(|i| matches!(i.severity, Severity::Spicy))
209 .count();
210 let mild = issues
211 .iter()
212 .filter(|i| matches!(i.severity, Severity::Mild))
213 .count();
214
215 println!("{}", self.i18n.get("title").bright_red().bold());
216 println!("{}", self.i18n.get("preparing").yellow());
217 println!();
218
219 println!("{}", self.i18n.get("report_title").bright_red().bold());
220 println!("{}", "─".repeat(50).bright_black());
221
222 if self.savage_mode {
223 println!("{}", self.i18n.get("found_issues").red().bold());
224 } else {
225 println!("{}", self.i18n.get("found_issues").yellow());
226 }
227
228 println!();
229 println!("{}", self.i18n.get("statistics"));
230 println!(
231 " {} {}",
232 nuclear.to_string().red().bold(),
233 self.i18n.get("nuclear_issues")
234 );
235 println!(
236 " {} {}",
237 spicy.to_string().yellow().bold(),
238 self.i18n.get("spicy_issues")
239 );
240 println!(
241 " {} {}",
242 mild.to_string().blue().bold(),
243 self.i18n.get("mild_issues")
244 );
245 println!(
246 " {} {}",
247 total.to_string().bright_white().bold(),
248 self.i18n.get("total")
249 );
250 println!();
251 }
252
253 fn print_issues(&self, issues: &[CodeIssue]) {
254 let mut file_groups: HashMap<String, Vec<&CodeIssue>> = HashMap::new();
255
256 for issue in issues {
257 let file_name = issue
258 .file_path
259 .file_name()
260 .unwrap_or_default()
261 .to_string_lossy()
262 .to_string();
263 file_groups.entry(file_name).or_default().push(issue);
264 }
265
266 for (file_name, file_issues) in file_groups {
267 println!("{} {}", "📁".bright_blue(), file_name.bright_blue().bold());
268
269 let issues_to_show = if self.max_issues_per_file > 0 {
270 file_issues
271 .into_iter()
272 .take(self.max_issues_per_file)
273 .collect::<Vec<_>>()
274 } else {
275 file_issues
276 };
277
278 for issue in issues_to_show {
279 self.print_issue(issue);
280 }
281 println!();
282 }
283 }
284
285 fn print_issue(&self, issue: &CodeIssue) {
286 let severity_icon = match issue.severity {
287 Severity::Nuclear => "💥",
288 Severity::Spicy => "🌶️",
289 Severity::Mild => "😐",
290 };
291
292 let line_info = format!("{}:{}", issue.line, issue.column).bright_black();
293
294
295 let messages = self.i18n.get_roast_messages(&issue.rule_name);
296 let message = if !messages.is_empty() {
297 messages[issue.line % messages.len()].clone()
298 } else {
299 issue.message.clone()
300 };
301
302
303 let final_message = if self.savage_mode {
304 self.make_message_savage(&message)
305 } else {
306 message
307 };
308
309 let colored_message = match issue.severity {
310 Severity::Nuclear => final_message.red().bold(),
311 Severity::Spicy => final_message.yellow(),
312 Severity::Mild => final_message.blue(),
313 };
314
315 println!(" {} {} {}", severity_icon, line_info, colored_message);
316 }
317
318 fn make_message_savage(&self, message: &str) -> String {
319 let savage_prefixes = vec![
320 "🔥 严重警告:",
321 "💀 代码死刑:",
322 "🗑️ 垃圾警报:",
323 "😱 恐怖发现:",
324 "🤮 令人作呕:",
325 ];
326
327 let prefix = savage_prefixes[message.len() % savage_prefixes.len()];
328 format!("{} {}", prefix, message)
329 }
330
331 fn print_summary_with_score(&self, issues: &[CodeIssue], quality_score: &CodeQualityScore) {
332 let _nuclear_count = issues
333 .iter()
334 .filter(|i| matches!(i.severity, Severity::Nuclear))
335 .count();
336 let total_count = issues.len();
337
338 println!("{}", self.i18n.get("summary").bright_white().bold());
339 println!("{}", "─".repeat(50).bright_black());
340
341 let score_summary = match quality_score.quality_level {
343 crate::scoring::QualityLevel::Excellent => {
344 match self.i18n.lang.as_str() {
345 "zh-CN" => format!("🏆 代码质量优秀!评分: {:.1}/100", quality_score.total_score),
346 _ => format!("🏆 Excellent code quality! Score: {:.1}/100", quality_score.total_score),
347 }
348 },
349 crate::scoring::QualityLevel::Good => {
350 match self.i18n.lang.as_str() {
351 "zh-CN" => format!("👍 代码质量良好,评分: {:.1}/100", quality_score.total_score),
352 _ => format!("👍 Good code quality, Score: {:.1}/100", quality_score.total_score),
353 }
354 },
355 crate::scoring::QualityLevel::Average => {
356 match self.i18n.lang.as_str() {
357 "zh-CN" => format!("😐 代码质量一般,评分: {:.1}/100,还有改进空间", quality_score.total_score),
358 _ => format!("😐 Average code quality, Score: {:.1}/100, room for improvement", quality_score.total_score),
359 }
360 },
361 crate::scoring::QualityLevel::Poor => {
362 match self.i18n.lang.as_str() {
363 "zh-CN" => format!("😟 代码质量较差,评分: {:.1}/100,建议重构", quality_score.total_score),
364 _ => format!("😟 Poor code quality, Score: {:.1}/100, refactoring recommended", quality_score.total_score),
365 }
366 },
367 crate::scoring::QualityLevel::Terrible => {
368 match self.i18n.lang.as_str() {
369 "zh-CN" => format!("💀 代码质量糟糕,评分: {:.1}/100,急需重写", quality_score.total_score),
370 _ => format!("💀 Terrible code quality, Score: {:.1}/100, rewrite urgently needed", quality_score.total_score),
371 }
372 },
373 };
374
375 let score_color = match quality_score.quality_level {
376 crate::scoring::QualityLevel::Excellent => score_summary.bright_green().bold(),
377 crate::scoring::QualityLevel::Good => score_summary.green(),
378 crate::scoring::QualityLevel::Average => score_summary.yellow(),
379 crate::scoring::QualityLevel::Poor => score_summary.red(),
380 crate::scoring::QualityLevel::Terrible => score_summary.bright_red().bold(),
381 };
382
383 println!("{}", score_color);
384 println!();
385
386 let _nuclear_count = issues
388 .iter()
389 .filter(|i| matches!(i.severity, Severity::Nuclear))
390 .count();
391 let _total_count = issues.len();
392
393 println!("{}", self.i18n.get("summary").bright_white().bold());
394 println!("{}", "─".repeat(50).bright_black());
395
396 let summary_message = if _nuclear_count > 0 {
397 if self.savage_mode {
398 match self.i18n.lang.as_str() {
399 "zh-CN" => "你的代码质量堪忧,建议重新学习编程基础 💀".to_string(),
400 _ => "Your code quality is concerning, suggest learning programming basics again 💀".to_string(),
401 }
402 } else {
403 match self.i18n.lang.as_str() {
404 "zh-CN" => "发现了一些严重问题,建议优先修复核弹级问题 🔥".to_string(),
405 _ => "Found some serious issues, suggest fixing nuclear problems first 🔥"
406 .to_string(),
407 }
408 }
409 } else if total_count > 10 {
410 match self.i18n.lang.as_str() {
411 "zh-CN" => "问题有点多,建议分批修复 📝".to_string(),
412 _ => "Quite a few issues, suggest fixing them in batches 📝".to_string(),
413 }
414 } else {
415 match self.i18n.lang.as_str() {
416 "zh-CN" => "问题不多,稍微改进一下就好了 👍".to_string(),
417 _ => "Not many issues, just need some minor improvements 👍".to_string(),
418 }
419 };
420
421 let color = if _nuclear_count > 0 {
422 summary_message.red().bold()
423 } else if _total_count > 10 {
424 summary_message.yellow()
425 } else {
426 summary_message.green()
427 };
428
429 println!("{}", color);
430 }
431
432 fn print_footer(&self, issues: &[CodeIssue]) {
433 println!();
434 println!("{}", self.i18n.get("suggestions").bright_cyan().bold());
435 println!("{}", "─".repeat(50).bright_black());
436
437 let rule_names: Vec<String> = issues
438 .iter()
439 .map(|issue| issue.rule_name.clone())
440 .collect::<std::collections::HashSet<_>>()
441 .into_iter()
442 .collect();
443
444 let suggestions = self.i18n.get_suggestions(&rule_names);
445 for suggestion in suggestions {
446 println!(" {}", suggestion.cyan());
447 }
448
449 println!();
450 let footer_message = if self.savage_mode {
451 match self.i18n.lang.as_str() {
452 "zh-CN" => "记住:写垃圾代码容易,写好代码需要用心 💪".to_string(),
453 _ => "Remember: writing garbage code is easy, writing good code requires effort 💪"
454 .to_string(),
455 }
456 } else {
457 self.i18n.get("keep_improving")
458 };
459
460 let color = if self.savage_mode {
461 footer_message.bright_red().bold()
462 } else {
463 footer_message.bright_green().bold()
464 };
465
466 println!("{}", color);
467 }
468
469 fn print_top_files(&self, issues: &[CodeIssue]) {
470 if self.top_files == 0 {
471 return;
472 }
473
474 let mut file_issue_counts: HashMap<String, usize> = HashMap::new();
475 for issue in issues {
476 let file_name = issue
477 .file_path
478 .file_name()
479 .unwrap_or_default()
480 .to_string_lossy()
481 .to_string();
482 *file_issue_counts.entry(file_name).or_insert(0) += 1;
483 }
484
485 let mut sorted_files: Vec<_> = file_issue_counts.into_iter().collect();
486 sorted_files.sort_by(|a, b| b.1.cmp(&a.1));
487
488 if !sorted_files.is_empty() {
489 println!("{}", self.i18n.get("top_files").bright_yellow().bold());
490 println!("{}", "─".repeat(50).bright_black());
491
492 for (i, (file_name, count)) in sorted_files.iter().take(self.top_files).enumerate() {
493 let rank = format!("{}.", i + 1);
494 println!(
495 " {} {} ({} issues)",
496 rank.bright_white(),
497 file_name.bright_blue(),
498 count.to_string().red()
499 );
500 }
501 println!();
502 }
503 }
504
505 fn print_detailed_analysis(&self, issues: &[CodeIssue]) {
506 println!(
507 "{}",
508 self.i18n.get("detailed_analysis").bright_magenta().bold()
509 );
510 println!("{}", "─".repeat(50).bright_black());
511
512 let mut rule_stats: HashMap<String, usize> = HashMap::new();
513 for issue in issues {
514 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
515 }
516
517 let rule_descriptions = match self.i18n.lang.as_str() {
518 "zh-CN" => [
519 ("terrible-naming", "糟糕的变量命名"),
520 ("single-letter-variable", "单字母变量"),
521 ("deep-nesting", "过度嵌套"),
522 ("long-function", "超长函数"),
523 ("unwrap-abuse", "unwrap() 滥用"),
524 ("unnecessary-clone", "不必要的 clone()"),
525 ]
526 .iter()
527 .cloned()
528 .collect::<HashMap<_, _>>(),
529 _ => [
530 ("terrible-naming", "Terrible variable naming"),
531 ("single-letter-variable", "Single letter variables"),
532 ("deep-nesting", "Deep nesting"),
533 ("long-function", "Long functions"),
534 ("unwrap-abuse", "unwrap() abuse"),
535 ("unnecessary-clone", "Unnecessary clone()"),
536 ]
537 .iter()
538 .cloned()
539 .collect::<HashMap<_, _>>(),
540 };
541
542 for (rule_name, count) in rule_stats {
543 let rule_name_str = rule_name.as_str();
544 let description = rule_descriptions
545 .get(rule_name_str)
546 .unwrap_or(&rule_name_str);
547 println!(
548 " 📌 {}: {} issues",
549 description.cyan(),
550 count.to_string().yellow()
551 );
552 }
553 println!();
554 }
555
556 fn print_markdown_report(&self, issues: &[CodeIssue]) {
557 let total = issues.len();
558 let nuclear = issues
559 .iter()
560 .filter(|i| matches!(i.severity, Severity::Nuclear))
561 .count();
562 let spicy = issues
563 .iter()
564 .filter(|i| matches!(i.severity, Severity::Spicy))
565 .count();
566 let mild = issues
567 .iter()
568 .filter(|i| matches!(i.severity, Severity::Mild))
569 .count();
570
571 println!("# {}", self.i18n.get("title"));
572 println!();
573 println!("## {}", self.i18n.get("statistics"));
574 println!();
575 println!("| Severity | Count | Description |");
576 println!("| --- | --- | --- |");
577 println!(
578 "| 🔥 Nuclear | {} | {} |",
579 nuclear,
580 self.i18n.get("nuclear_issues")
581 );
582 println!(
583 "| 🌶️ Spicy | {} | {} |",
584 spicy,
585 self.i18n.get("spicy_issues")
586 );
587 println!("| 😐 Mild | {} | {} |", mild, self.i18n.get("mild_issues"));
588 println!(
589 "| **Total** | **{}** | **{}** |",
590 total,
591 self.i18n.get("total")
592 );
593 println!();
594
595 if self.verbose {
596 println!("## {}", self.i18n.get("detailed_analysis"));
597 println!();
598
599 let mut rule_stats: HashMap<String, usize> = HashMap::new();
600 for issue in issues {
601 *rule_stats.entry(issue.rule_name.clone()).or_insert(0) += 1;
602 }
603
604 for (rule_name, count) in rule_stats {
605 println!("- **{}**: {} issues", rule_name, count);
606 }
607 println!();
608 }
609
610 println!("## Issues by File");
611 println!();
612
613 let mut file_groups: HashMap<String, Vec<&CodeIssue>> = HashMap::new();
614 for issue in issues {
615 let file_name = issue
616 .file_path
617 .file_name()
618 .unwrap_or_default()
619 .to_string_lossy()
620 .to_string();
621 file_groups.entry(file_name).or_default().push(issue);
622 }
623
624 for (file_name, file_issues) in file_groups {
625 println!("### 📁 {}", file_name);
626 println!();
627
628 let issues_to_show = if self.max_issues_per_file > 0 {
629 file_issues
630 .into_iter()
631 .take(self.max_issues_per_file)
632 .collect::<Vec<_>>()
633 } else {
634 file_issues
635 };
636
637 for issue in issues_to_show {
638 let severity_icon = match issue.severity {
639 Severity::Nuclear => "💥",
640 Severity::Spicy => "🌶️",
641 Severity::Mild => "😐",
642 };
643
644 let messages = self.i18n.get_roast_messages(&issue.rule_name);
645 let message = if !messages.is_empty() {
646 messages[issue.line % messages.len()].clone()
647 } else {
648 issue.message.clone()
649 };
650
651 println!(
652 "- {} **Line {}:{}** - {}",
653 severity_icon, issue.line, issue.column, message
654 );
655 }
656 println!();
657 }
658
659 println!("## {}", self.i18n.get("suggestions"));
660 println!();
661
662 let rule_names: Vec<String> = issues
663 .iter()
664 .map(|issue| issue.rule_name.clone())
665 .collect::<std::collections::HashSet<_>>()
666 .into_iter()
667 .collect();
668
669 let suggestions = self.i18n.get_suggestions(&rule_names);
670 for suggestion in suggestions {
671 println!("- {}", suggestion);
672 }
673 }
674}