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 _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            // Sort rule groups by severity (most severe first)
232            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                // Create compact summary for each rule type
260                if rule_name.contains("naming") || rule_name.contains("single-letter") {
261                    // Collect variable names for naming issues
262                    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                    // Extract instance count from message if available
303                    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                    // Extract depth range from messages
349                    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                    // For other types, show a generic summary with proper translation
397                    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            // Get the display name for the rule
528            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}