Skip to main content

garbage_code_hunter/ci_bot/
mod.rs

1//! CI Comment Bot — generate PR review comments with roast flavor.
2
3use crate::analyzer::{CodeAnalyzer, CodeIssue};
4use crate::common::i18n_ext::t;
5use crate::common::OutputFormat;
6use anyhow::Result;
7use colored::Colorize;
8use std::collections::HashMap;
9use std::path::Path;
10
11/// Generate a CI review comment for a PR.
12pub fn run(path: &Path, format: &OutputFormat, lang: &str) -> Result<String> {
13    let analyzer = CodeAnalyzer::new(&[], lang);
14    let issues = analyzer.analyze_path(path);
15
16    let output = match format {
17        OutputFormat::Terminal => format_terminal(&issues, lang),
18        OutputFormat::Json => format_json(&issues),
19    };
20
21    Ok(output)
22}
23
24fn format_terminal(issues: &[CodeIssue], lang: &str) -> String {
25    let mut out = String::new();
26
27    out.push_str(&format!(
28        "\n{}\n",
29        t(
30            lang,
31            "\u{26a0}\u{fe0f} 垃圾代码猎人审查",
32            "\u{26a0}\u{fe0f} Garbage Code Hunter Review"
33        )
34        .bold()
35    ));
36    out.push_str(&format!("{}\n\n", "\u{2501}".repeat(40)));
37
38    // Count by category
39    let mut categories: HashMap<&str, usize> = HashMap::new();
40    for issue in issues {
41        let cat = categorize(&issue.rule_name);
42        *categories.entry(cat).or_insert(0) += 1;
43    }
44
45    // Summary stats
46    out.push_str(&format!("  {}\n", t(lang, "这个 PR:", "This PR:")));
47    for (cat, count) in &categories {
48        let emoji = category_emoji(cat);
49        out.push_str(&format!("  {} +{} {}\n", emoji, count, cat));
50    }
51
52    // Emotion index
53    let emotion_level = (issues.len() / 5).min(5);
54    let emotions: Vec<&str> = (0..emotion_level + 1).map(|_| "\u{1f621}").collect();
55    out.push_str(&format!(
56        "\n  {}: {}\n",
57        t(lang, "审查者情绪指数", "Reviewer Emotion Index"),
58        emotions.join("")
59    ));
60
61    // Verdict
62    out.push_str(&format!("\n  {}\n", t(lang, "结论:", "Verdict:").bold()));
63    if issues.is_empty() {
64        out.push_str(&format!(
65            "  {}\n",
66            t(
67                lang,
68                "LGTM!(开个玩笑,我们都知道总会有问题的。)",
69                "LGTM! (Just kidding, we both know there's always something)."
70            )
71            .green()
72        ));
73    } else if issues.len() < 5 {
74        out.push_str(&format!(
75            "  {}\n",
76            t(
77                lang,
78                "小问题。你的代码审查者今天可能会微笑。",
79                "Minor issues. Your code reviewer might actually smile today."
80            )
81            .yellow()
82        ));
83    } else if issues.len() < 15 {
84        out.push_str(&format!(
85            "  {}\n",
86            t(
87                lang,
88                "发现多个问题。审查者建议先喝杯咖啡。",
89                "Several issues found. The reviewer suggests a coffee break first."
90            )
91            .red()
92        ));
93    } else {
94        out.push_str(&format!(
95            "  {}\n",
96            t(
97                lang,
98                "这个 PR 是对整洁代码的犯罪。作者,请坐下。",
99                "This PR is a war crime against clean code. Author, please sit down."
100            )
101            .red()
102            .bold()
103        ));
104    }
105
106    out.push_str(&format!(
107        "\n  {}\n",
108        t(
109            lang,
110            "提示:推送前在本地运行 `garbage-code-hunter`。",
111            "Tip: Run `garbage-code-hunter` locally before pushing."
112        )
113    ));
114
115    out
116}
117
118fn format_json(issues: &[CodeIssue]) -> String {
119    let mut categories: HashMap<&str, usize> = HashMap::new();
120    for issue in issues {
121        let cat = categorize(&issue.rule_name);
122        *categories.entry(cat).or_insert(0) += 1;
123    }
124
125    serde_json::json!({
126        "total_issues": issues.len(),
127        "categories": categories,
128        "verdict": if issues.is_empty() { "LGTM" }
129                   else if issues.len() < 5 { "Minor issues" }
130                   else if issues.len() < 15 { "Needs work" }
131                   else { "Major concerns" },
132        "comment": generate_markdown_comment(issues, &categories),
133    })
134    .to_string()
135}
136
137/// Generate a Markdown-formatted PR comment.
138fn generate_markdown_comment(issues: &[CodeIssue], categories: &HashMap<&str, usize>) -> String {
139    let mut md = String::new();
140    md.push_str("## \u{26a0}\u{fe0f} Garbage Code Hunter Review\n\n");
141
142    md.push_str("| Category | Count |\n|---|---|\n");
143    for (cat, count) in categories {
144        md.push_str(&format!(
145            "| {} {} | {} |\n",
146            category_emoji(cat),
147            cat,
148            count
149        ));
150    }
151
152    md.push_str(&format!("\n**Total issues:** {}\n\n", issues.len()));
153
154    let emotion_level = (issues.len() / 5).min(5);
155    let emotions: Vec<&str> = (0..emotion_level + 1).map(|_| "\u{1f621}").collect();
156    md.push_str(&format!("**Reviewer mood:** {}\n\n", emotions.join("")));
157
158    if issues.len() >= 15 {
159        md.push_str(
160            "> This PR's biggest problem isn't the bugs — \
161             it's what it reveals about the author's mental state.\n",
162        );
163    }
164
165    md.push_str(
166        "\n---\n*Generated by [Garbage Code Hunter](https://github.com/yourusername/garbage-code-hunter)*\n",
167    );
168
169    md
170}
171
172fn categorize(rule_name: &str) -> &'static str {
173    let lower = rule_name.to_lowercase();
174    if lower.contains("unwrap") {
175        "unwrap() abuse"
176    } else if lower.contains("nest") || lower.contains("complex") {
177        "complexity"
178    } else if lower.contains("name")
179        || lower.contains("single_letter")
180        || lower.contains("meaningless")
181    {
182        "naming"
183    } else if lower.contains("magic") {
184        "magic numbers"
185    } else if lower.contains("duplicat") {
186        "duplication"
187    } else if lower.contains("long") {
188        "long functions"
189    } else {
190        "other"
191    }
192}
193
194fn category_emoji(cat: &str) -> &'static str {
195    match cat {
196        "unwrap() abuse" => "\u{1f4a5}",
197        "complexity" => "\u{1f522}",
198        "naming" => "\u{1f4dd}",
199        "magic numbers" => "\u{1f52e}",
200        "duplication" => "\u{1f4cb}",
201        "long functions" => "\u{1f4dc}",
202        _ => "\u{1f4a9}",
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_categorize() {
212        assert_eq!(categorize("unwrap_abuse"), "unwrap() abuse");
213        assert_eq!(categorize("deep_nesting"), "complexity");
214    }
215
216    #[test]
217    fn test_run_on_current_dir() {
218        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "en-US");
219        assert!(result.is_ok());
220    }
221
222    #[test]
223    fn test_run_on_current_dir_chinese() {
224        let result = run(std::path::Path::new("."), &OutputFormat::Terminal, "zh-CN");
225        assert!(result.is_ok());
226    }
227
228    #[test]
229    fn test_run_json_format() {
230        let result = run(std::path::Path::new("."), &OutputFormat::Json, "en-US");
231        assert!(result.is_ok());
232        let json = result.unwrap();
233        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
234        assert!(parsed["total_issues"].is_number());
235    }
236}