garbage_code_hunter/ci_bot/
mod.rs1use 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
11pub 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 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 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 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 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
137fn 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}