Skip to main content

garbage_code_hunter/friend/
feedback.rs

1use crate::analyzer::{CodeIssue, Severity};
2use crate::scoring::CodeQualityScore;
3use crate::signals::StyleSignal;
4use std::collections::HashMap;
5
6fn is_zh(locale: &str) -> bool {
7    locale.starts_with("zh")
8}
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub enum FriendMood {
12    Proud,
13    Concerned,
14    Sarcastic,
15    Alarmed,
16    Exhausted,
17}
18
19impl FriendMood {
20    pub fn from_score(score: f64) -> Self {
21        if score >= 90.0 {
22            FriendMood::Proud
23        } else if score >= 70.0 {
24            FriendMood::Concerned
25        } else if score >= 50.0 {
26            FriendMood::Sarcastic
27        } else if score >= 30.0 {
28            FriendMood::Alarmed
29        } else {
30            FriendMood::Exhausted
31        }
32    }
33
34    pub fn emoji(&self) -> &'static str {
35        match self {
36            FriendMood::Proud => "😎",
37            FriendMood::Concerned => "🤔",
38            FriendMood::Sarcastic => "😏",
39            FriendMood::Alarmed => "😰",
40            FriendMood::Exhausted => "😩",
41        }
42    }
43
44    pub fn vibe(&self, locale: &str) -> &'static str {
45        if is_zh(locale) {
46            match self {
47                FriendMood::Proud => "嘿,这代码还不错嘛!",
48                FriendMood::Concerned => "还行,但咱得聊聊。",
49                FriendMood::Sarcastic => "哇哦。真是……绝了。",
50                FriendMood::Alarmed => "兄弟,我们需要 intervention 一下。",
51                FriendMood::Exhausted => "光看这代码我就累了。",
52            }
53        } else {
54            match self {
55                FriendMood::Proud => "Hey, this is actually pretty good!",
56                FriendMood::Concerned => "Not bad, but we need to talk.",
57                FriendMood::Sarcastic => "Oh wow. Just... wow.",
58                FriendMood::Alarmed => "Dude, we need to have an intervention.",
59                FriendMood::Exhausted => "I'm tired just looking at this.",
60            }
61        }
62    }
63}
64
65#[derive(Debug, Clone)]
66pub struct BehaviorPattern {
67    pub signal: StyleSignal,
68    pub severity: &'static str,
69    pub description: String,
70    pub suggestion: String,
71}
72
73impl BehaviorPattern {
74    fn desc_zh(signal: &StyleSignal) -> (&'static str, &'static str) {
75        match signal {
76            StyleSignal::Duplication => (
77                "同样的代码写了好几遍,而不是复用",
78                "把共享逻辑提取到函数或模块中",
79            ),
80            StyleSignal::PanicAddiction => (
81                "用 unwrap/expect/panic 代替正确的错误处理",
82                "使用 Result<T, E> 并用 '?' 传播错误",
83            ),
84            StyleSignal::NamingChaos => ("变量名看不出是干什么的", "用能表达意图的描述性名称"),
85            StyleSignal::NestedHell => (
86                "嵌套太深,代码难以阅读",
87                "用 early return 和 guard clause 减少嵌套",
88            ),
89            StyleSignal::HotfixCulture => {
90                ("残留的调试打印、TODO 和注释掉的代码", "提交前清理调试残留")
91            }
92            StyleSignal::OverEngineering => ("一个函数干太多事", "把大函数拆成职责单一的小函数"),
93            StyleSignal::CodeSmells => (
94                "unsafe 块、魔法数字和可疑的写法",
95                "优先用安全抽象;给常量起个好名字",
96            ),
97            StyleSignal::LegacyCode => ("源文件里留着注释掉的代码", "删掉死代码,git 有历史记录"),
98            StyleSignal::TodoMountain => (
99                "堆积的 TODO/FIXME/BUG/HACK 标记",
100                "用 issue 跟踪器管理待办,别写在代码里",
101            ),
102            StyleSignal::LineCountSmell => (
103                "文件行数超过了合理阈值",
104                "把大文件拆成更小、职责更清晰的模块",
105            ),
106        }
107    }
108
109    fn desc_en(signal: &StyleSignal) -> (&'static str, &'static str) {
110        match signal {
111            StyleSignal::Duplication => (
112                "Writing the same code multiple times instead of reusing it",
113                "Extract shared logic into functions or modules",
114            ),
115            StyleSignal::PanicAddiction => (
116                "Using unwrap/expect/panic instead of proper error handling",
117                "Use Result<T, E> and propagate errors with '?'",
118            ),
119            StyleSignal::NamingChaos => (
120                "Variable names that don't explain what they do",
121                "Use descriptive names that convey intent",
122            ),
123            StyleSignal::NestedHell => (
124                "Deeply nested blocks that are hard to follow",
125                "Early returns and guard clauses reduce nesting",
126            ),
127            StyleSignal::HotfixCulture => (
128                "Leftover debug prints, TODOs, and commented code",
129                "Clean up debug artifacts before committing",
130            ),
131            StyleSignal::OverEngineering => (
132                "Functions that try to do too many things at once",
133                "Split large functions into focused smaller ones",
134            ),
135            StyleSignal::CodeSmells => (
136                "Unsafe blocks, magic numbers, and questionable patterns",
137                "Prefer safe abstractions; name constants clearly",
138            ),
139            StyleSignal::LegacyCode => (
140                "Commented-out code left in source files",
141                "Delete dead code instead of commenting it out; git has history",
142            ),
143            StyleSignal::TodoMountain => (
144                "Accumulated TODO/FIXME/BUG/HACK markers",
145                "Track todos in an issue tracker, not in source code",
146            ),
147            StyleSignal::LineCountSmell => (
148                "Files that exceed reasonable line count thresholds",
149                "Split large files into smaller focused modules",
150            ),
151        }
152    }
153
154    pub fn from_signals(scores: &HashMap<StyleSignal, f64>, locale: &str) -> Vec<Self> {
155        let mut pairs: Vec<(&StyleSignal, f64)> = scores.iter().map(|(s, v)| (s, *v)).collect();
156        pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
157        let zh = is_zh(locale);
158
159        pairs
160            .into_iter()
161            .filter(|(_, v)| *v >= 3.0)
162            .take(3)
163            .map(|(signal, score)| {
164                let (severity, description, suggestion) = if zh {
165                    let sev = if score >= 12.0 {
166                        "严重"
167                    } else if score >= 6.0 {
168                        "中等"
169                    } else {
170                        "轻微"
171                    };
172                    let (desc, sugg) = Self::desc_zh(signal);
173                    (sev, desc.into(), sugg.into())
174                } else {
175                    let sev = if score >= 12.0 {
176                        "major"
177                    } else if score >= 6.0 {
178                        "moderate"
179                    } else {
180                        "minor"
181                    };
182                    let (desc, sugg) = Self::desc_en(signal);
183                    (sev, desc.into(), sugg.into())
184                };
185                BehaviorPattern {
186                    signal: *signal,
187                    severity,
188                    description,
189                    suggestion,
190                }
191            })
192            .collect()
193    }
194}
195
196#[derive(Debug, Clone)]
197pub struct NextAction {
198    pub priority: u8,
199    pub file: String,
200    pub line: usize,
201    pub action: String,
202    pub reason: String,
203}
204
205impl NextAction {
206    fn rule_name_zh(name: &str) -> String {
207        match name {
208            "unwrap-abuse" | "unwrap_abuse" => "滥用 unwrap",
209            "single-letter-variable" => "单字母变量",
210            "magic-number" => "魔法数字",
211            "deep-nesting" | "deep_nesting" => "深层嵌套",
212            "code-duplication" => "代码重复",
213            "cross-file-duplication" => "跨文件重复",
214            "near-duplicate" => "近似重复",
215            "god-function" => "上帝函数",
216            "long-function" => "过长函数",
217            "too-many-params" => "参数过多",
218            "terrible-naming" => "糟糕命名",
219            "hungarian-notation" => "匈牙利命名",
220            "abbreviation-abuse" => "滥用缩写",
221            "println-debugging" => "调试打印",
222            "complex-closure" => "复杂闭包",
223            "box-abuse" => "滥用 Box",
224            "rust-must-use" => "缺少 #[must_use]",
225            "rust-derive-order" => "derive 顺序",
226            "rust-doc-example" => "文档示例缺失",
227            "rust-error-display" => "Error 未实现 Display",
228            _ => return name.to_string(),
229        }
230        .to_string()
231    }
232
233    pub fn from_issues(issues: &[CodeIssue], locale: &str) -> Vec<Self> {
234        let mut actionable: Vec<&CodeIssue> = issues.iter().filter(|i| i.line > 0).collect();
235        actionable.sort_by(|a, b| {
236            let order = |s: &Severity| match s {
237                Severity::Nuclear => 3,
238                Severity::Spicy => 2,
239                Severity::Mild => 1,
240            };
241            order(&b.severity).cmp(&order(&a.severity))
242        });
243        let zh = is_zh(locale);
244
245        actionable
246            .into_iter()
247            .take(3)
248            .enumerate()
249            .map(|(i, issue)| {
250                let file = issue
251                    .file_path
252                    .file_name()
253                    .map(|n| n.to_string_lossy().to_string())
254                    .unwrap_or_else(|| issue.file_path.to_string_lossy().to_string());
255                let action = if zh {
256                    format!("修复 '{}'", Self::rule_name_zh(&issue.rule_name))
257                } else {
258                    format!("Fix '{}'", issue.rule_name)
259                };
260                let reason = issue.message.clone();
261                NextAction {
262                    priority: (i + 1) as u8,
263                    file,
264                    line: issue.line,
265                    action,
266                    reason,
267                }
268            })
269            .collect()
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct FriendFeedback {
275    pub mood: FriendMood,
276    pub patterns: Vec<BehaviorPattern>,
277    pub next_actions: Vec<NextAction>,
278    pub total_issues: usize,
279    pub total_score: f64,
280}
281
282impl FriendFeedback {
283    pub fn new(
284        issues: &[CodeIssue],
285        score: &CodeQualityScore,
286        signal_scores: &HashMap<StyleSignal, f64>,
287        locale: &str,
288    ) -> Self {
289        let mood = FriendMood::from_score(score.total_score);
290        let patterns = BehaviorPattern::from_signals(signal_scores, locale);
291        let next_actions = NextAction::from_issues(issues, locale);
292        FriendFeedback {
293            mood,
294            patterns,
295            next_actions,
296            total_issues: issues.len(),
297            total_score: score.total_score,
298        }
299    }
300
301    pub fn print(&self, locale: &str) {
302        use colored::*;
303        let zh = is_zh(locale);
304        println!();
305        if zh {
306            println!(
307                "{} 朋友的看法 {}",
308                "💬".bright_cyan(),
309                "─".repeat(60).bright_black()
310            );
311        } else {
312            println!(
313                "{} Friend's Take {}",
314                "💬".bright_cyan(),
315                "─".repeat(60).bright_black()
316            );
317        }
318        println!(
319            "{}  {} {}",
320            self.mood.emoji(),
321            self.mood.vibe(locale).bright_cyan().bold(),
322            if self.total_issues == 0 {
323                "".to_string()
324            } else if zh {
325                format!("  ({} 个问题)", self.total_issues.to_string().yellow())
326            } else {
327                format!(
328                    "  ({} issue{})",
329                    self.total_issues.to_string().yellow(),
330                    if self.total_issues == 1 { "" } else { "s" }
331                )
332            }
333        );
334        if zh {
335            println!("{}  评分: {:.1}/100", "📊".bright_blue(), self.total_score);
336        } else {
337            println!("{}  Score: {:.1}/100", "📊".bright_blue(), self.total_score);
338        }
339
340        if !self.patterns.is_empty() {
341            println!();
342            if zh {
343                println!("{}  发现的问题模式:", "🔍".bright_yellow());
344            } else {
345                println!("{}  Patterns I noticed:", "🔍".bright_yellow());
346            }
347            for p in &self.patterns {
348                let sev_color = match p.severity {
349                    "major" | "严重" => "red",
350                    "moderate" | "中等" => "yellow",
351                    _ => "blue",
352                };
353                println!(
354                    "  {} [{}] {}",
355                    match p.severity {
356                        "major" | "严重" => "🔴",
357                        "moderate" | "中等" => "🟡",
358                        _ => "🔵",
359                    },
360                    p.severity.bold().color(sev_color),
361                    p.description,
362                );
363                println!("     → {}", p.suggestion.dimmed());
364            }
365        }
366
367        if !self.next_actions.is_empty() {
368            println!();
369            if zh {
370                println!("{}  快速修复 (前 3 项):", "🎯".bright_green());
371            } else {
372                println!("{}  Quick wins (top 3):", "🎯".bright_green());
373            }
374            for a in &self.next_actions {
375                let location = format!("{}:{}", a.file, a.line).bright_white();
376                println!("  {}. {} — {}", a.priority, location, a.action.bold(),);
377                println!("     {}", a.reason.dimmed());
378            }
379        }
380        println!();
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use crate::scoring::{CodeQualityScore, QualityLevel, SeverityDistribution};
388    use std::path::PathBuf;
389
390    fn make_score(total: f64) -> CodeQualityScore {
391        CodeQualityScore {
392            total_score: total,
393            n_score: total,
394            d_score: 0.0,
395            category_scores: HashMap::new(),
396            signal_scores: HashMap::new(),
397            file_count: 1,
398            total_lines: 100,
399            issue_density: 0.0,
400            quality_level: QualityLevel::from_score(total),
401            severity_distribution: SeverityDistribution {
402                nuclear: 0,
403                spicy: 0,
404                mild: 0,
405            },
406        }
407    }
408
409    fn make_issue(severity: Severity, line: usize, rule: &str) -> CodeIssue {
410        CodeIssue {
411            file_path: PathBuf::from("src/main.rs"),
412            line,
413            column: 1,
414            rule_name: rule.to_string(),
415            message: format!("{} issue", rule),
416            severity,
417        }
418    }
419
420    #[test]
421    fn test_mood_proud_at_high_score() {
422        assert_eq!(FriendMood::from_score(95.0), FriendMood::Proud);
423    }
424
425    #[test]
426    fn test_mood_concerned_at_mid_score() {
427        assert_eq!(FriendMood::from_score(75.0), FriendMood::Concerned);
428    }
429
430    #[test]
431    fn test_mood_exhausted_at_low_score() {
432        assert_eq!(FriendMood::from_score(10.0), FriendMood::Exhausted);
433    }
434
435    #[test]
436    fn test_behavior_patterns_top_3_signals() {
437        let mut scores = HashMap::new();
438        scores.insert(StyleSignal::PanicAddiction, 18.0);
439        scores.insert(StyleSignal::NamingChaos, 12.0);
440        scores.insert(StyleSignal::NestedHell, 3.0);
441        let patterns = BehaviorPattern::from_signals(&scores, "en");
442        assert_eq!(patterns.len(), 3);
443        assert_eq!(patterns[0].signal, StyleSignal::PanicAddiction);
444        assert_eq!(patterns[1].signal, StyleSignal::NamingChaos);
445    }
446
447    #[test]
448    fn test_behavior_patterns_filters_low_scores() {
449        let mut scores = HashMap::new();
450        scores.insert(StyleSignal::PanicAddiction, 2.0);
451        scores.insert(StyleSignal::NamingChaos, 1.0);
452        let patterns = BehaviorPattern::from_signals(&scores, "en");
453        assert!(patterns.is_empty(), "signals below 3.0 should be filtered");
454    }
455
456    #[test]
457    fn test_next_actions_top_3_by_severity() {
458        let issues = vec![
459            make_issue(Severity::Mild, 1, "mild-rule"),
460            make_issue(Severity::Nuclear, 2, "nuclear-rule"),
461            make_issue(Severity::Spicy, 3, "spicy-rule"),
462            make_issue(Severity::Mild, 4, "another-mild"),
463        ];
464        let actions = NextAction::from_issues(&issues, "en");
465        assert_eq!(actions.len(), 3);
466        assert_eq!(actions[0].action, "Fix 'nuclear-rule'");
467        assert_eq!(actions[1].action, "Fix 'spicy-rule'");
468        assert_eq!(actions[2].action, "Fix 'mild-rule'");
469    }
470
471    #[test]
472    fn test_next_actions_empty_issues() {
473        let actions = NextAction::from_issues(&[], "en");
474        assert!(actions.is_empty());
475    }
476
477    #[test]
478    fn test_friend_feedback_construction() {
479        let issues = vec![make_issue(Severity::Spicy, 10, "unwrap-abuse")];
480        let score = make_score(65.0);
481        let mut signal_scores = HashMap::new();
482        signal_scores.insert(StyleSignal::PanicAddiction, 15.0);
483        let feedback = FriendFeedback::new(&issues, &score, &signal_scores, "en");
484        assert_eq!(feedback.mood, FriendMood::Sarcastic);
485        assert_eq!(feedback.total_issues, 1);
486        assert!(!feedback.patterns.is_empty());
487        assert!(!feedback.next_actions.is_empty());
488    }
489
490    #[test]
491    fn test_behavior_patterns_zh() {
492        let mut scores = HashMap::new();
493        scores.insert(StyleSignal::PanicAddiction, 18.0);
494        let patterns = BehaviorPattern::from_signals(&scores, "zh-CN");
495        assert_eq!(patterns.len(), 1);
496        assert_eq!(patterns[0].severity, "严重");
497        assert!(patterns[0].description.contains("unwrap"));
498    }
499
500    #[test]
501    fn test_next_actions_zh() {
502        let issues = vec![make_issue(Severity::Nuclear, 5, "unwrap-abuse")];
503        let actions = NextAction::from_issues(&issues, "zh-CN");
504        assert_eq!(actions.len(), 1);
505        assert!(actions[0].action.contains("修复"));
506        assert!(actions[0].action.contains("滥用 unwrap"));
507    }
508}