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}