Skip to main content

garbage_code_hunter/reporter/
mod.rs

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        // calculate quality score
56        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        //sort by severity
65        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 harsh mode  only show the most severe issue
75        if self.harsh_mode {
76            issues.retain(|issue| matches!(issue.severity, Severity::Nuclear | Severity::Spicy));
77        }
78
79        // Generate roasts via provider (one call for all issues)
80        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            // Group issues by rule type
214            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            // Show limited number of issues per rule type
223            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            // Sort rule groups by severity (most severe first)
231            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                // Create compact summary for each rule type
259                if rule_name.contains("naming") || rule_name.contains("single-letter") {
260                    // Collect variable names for naming issues
261                    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                    // Extract instance count from message if available
302                    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                    // Extract depth range from messages
348                    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                    // For other types, show a generic summary with proper translation
396                    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            // Get the display name for the rule
527            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}