1use super::Personality;
4use crate::analyzer::CodeIssue;
5use crate::signals::{classify_rule, StyleProfile, StyleSignal};
6use std::collections::HashMap;
7
8pub fn analyze(issues: &[CodeIssue]) -> Personality {
10 let total = issues.len() as f64;
11
12 if total == 0.0 {
13 return Personality {
14 title: "The Perfectionist",
15 emoji: "\u{1f45f}",
16 traits: vec![
17 "No issues detected — suspiciously clean code",
18 "Probably over-engineers everything",
19 "Definitely has a linter on save",
20 "Has never shipped a bug (or a feature on time)",
21 ],
22 advice: vec![
23 "Ship something imperfect once in a while",
24 "Your code is great but your deadlines are crying",
25 "Perfect is the enemy of shipped",
26 ],
27 score: 100.0,
28 };
29 }
30
31 let mut counts: HashMap<StyleSignal, u32> = HashMap::new();
32 for issue in issues {
33 let signal = classify_rule(&issue.rule_name.to_lowercase());
34 *counts.entry(signal).or_insert(0) += 1;
35 }
36
37 let profile = StyleProfile::from_signal_counts(counts.clone());
38 let get = |s| *counts.get(&s).unwrap_or(&0);
39
40 match profile.dominant_signal {
41 Some(StyleSignal::PanicAddiction) => {
42 panic_personality(get(StyleSignal::PanicAddiction), total)
43 }
44 Some(StyleSignal::NamingChaos) => naming_personality(get(StyleSignal::NamingChaos), total),
45 Some(StyleSignal::NestedHell) => nesting_personality(get(StyleSignal::NestedHell), total),
46 Some(StyleSignal::OverEngineering) => {
47 long_fn_personality(get(StyleSignal::OverEngineering), total)
48 }
49 Some(StyleSignal::LineCountSmell) => {
50 long_fn_personality(get(StyleSignal::LineCountSmell), total)
51 }
52 Some(StyleSignal::CodeSmells) => magic_personality(get(StyleSignal::CodeSmells), total),
53 Some(StyleSignal::Duplication) => dup_personality(get(StyleSignal::Duplication), total),
54 _ => balanced_personality(total),
55 }
56}
57
58fn panic_personality(count: u32, _total: f64) -> Personality {
59 Personality {
60 title: "The Optimist",
61 emoji: "\u{1f60f}",
62 traits: vec![
63 "Believes the world is full of happy paths",
64 "unwrap() is your safety blanket",
65 "Error handling is someone else's problem",
66 "Probably says 'it works on my machine' a lot",
67 "Treats panics as 'unexpected features'",
68 ],
69 advice: vec![
70 "Learn Result<T, E> — your future self will thank you",
71 "Every unwrap() is a potential production incident",
72 "Try `.unwrap_or_default()` at minimum",
73 "Use `?` operator to propagate errors gracefully",
74 ],
75 score: (100.0 - count as f64 * 3.0).max(0.0),
76 }
77}
78
79fn naming_personality(count: u32, _total: f64) -> Personality {
80 Personality {
81 title: "The Minimalist",
82 emoji: "\u{270d}\u{fe0f}",
83 traits: vec![
84 "Why use many word when few letter do trick",
85 "Variables named like chess coordinates",
86 "Your code reads like a math textbook",
87 "Comments explain what x, y, z mean",
88 "Considers 'data' a descriptive name",
89 ],
90 advice: vec![
91 "Descriptive names are not a luxury",
92 "Your IDE has autocomplete — use it",
93 "Future you won't remember what `d` meant",
94 "A good variable name eliminates the need for a comment",
95 ],
96 score: (100.0 - count as f64 * 2.0).max(0.0),
97 }
98}
99
100fn nesting_personality(count: u32, _total: f64) -> Personality {
101 Personality {
102 title: "The Architect",
103 emoji: "\u{1f3d7}\u{fe0f}",
104 traits: vec![
105 "Loves building pyramids of doom",
106 "Indentation is a competitive sport",
107 "Each function is a journey through layers",
108 "Probably dreams in nested brackets",
109 "Thinks 'flat is justice' only applies to anime",
110 ],
111 advice: vec![
112 "Extract inner logic into helper functions",
113 "Use early returns to reduce nesting",
114 "Consider the 'guard clause' pattern",
115 "If you need 4+ levels of nesting, the logic needs refactoring",
116 ],
117 score: (100.0 - count as f64 * 4.0).max(0.0),
118 }
119}
120
121fn long_fn_personality(count: u32, _total: f64) -> Personality {
122 Personality {
123 title: "The Storyteller",
124 emoji: "\u{1f4dd}",
125 traits: vec![
126 "Every function tells a complete story",
127 "Believes in 'single responsibility' — for files, not functions",
128 "Your scroll wheel gets a workout",
129 "Probably writes long commit messages too",
130 "Considers 200 lines a 'concise' function",
131 ],
132 advice: vec![
133 "If a function needs a comment to explain its sections, split it",
134 "Aim for functions that fit on one screen",
135 "The Single Responsibility Principle applies to functions too",
136 "Break complex logic into smaller, testable units",
137 ],
138 score: (100.0 - count as f64 * 3.0).max(0.0),
139 }
140}
141
142fn magic_personality(count: u32, _total: f64) -> Personality {
143 Personality {
144 title: "The Sorcerer",
145 emoji: "\u{1f9d9}",
146 traits: vec![
147 "Numbers have meaning — only to you",
148 "42 appears in your code more than in Hitchhiker's Guide",
149 "Constants are for the weak",
150 "Your code has its own secret numerology",
151 "Believes named constants are 'over-engineering'",
152 ],
153 advice: vec![
154 "Extract magic numbers into named constants",
155 "Your future self won't remember what 86400 means",
156 "Use enums or constants for repeated values",
157 "If a number appears twice, it needs a name",
158 ],
159 score: (100.0 - count as f64 * 2.0).max(0.0),
160 }
161}
162
163fn dup_personality(count: u32, _total: f64) -> Personality {
164 Personality {
165 title: "The Copy-Paste Artist",
166 emoji: "\u{1f4cb}",
167 traits: vec![
168 "Ctrl+C, Ctrl+V is your IDE's most used shortcut",
169 "Why abstract when you can duplicate",
170 "Same bug in 5 places = 5x the debugging fun",
171 "DRY stands for 'Don't Repeat... wait, too late'",
172 "Thinks 'reusable code' means copying it again",
173 ],
174 advice: vec![
175 "Extract common code into shared functions",
176 "One bug fix should fix it everywhere",
177 "Consider a utility module for repeated patterns",
178 "If you're copying code, you're copying bugs too",
179 ],
180 score: (100.0 - count as f64 * 3.0).max(0.0),
181 }
182}
183
184fn balanced_personality(total: f64) -> Personality {
185 Personality {
186 title: "The Pragmatist",
187 emoji: "\u{2696}\u{fe0f}",
188 traits: vec![
189 "A balanced mix of code smells",
190 "Not great at anything, not terrible at anything",
191 "The 'average developer' experience",
192 "Your code has character — like a diverse zoo",
193 "Jack of all trades, master of technical debt",
194 ],
195 advice: vec![
196 "Pick one area to improve at a time",
197 "Focus on the highest-severity issues first",
198 "Consistency is better than perfection",
199 "Tackle your highest-count issue category first",
200 ],
201 score: (100.0 - total * 1.5).max(0.0),
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use std::path::PathBuf;
209
210 fn make_issue(rule: &str) -> CodeIssue {
211 CodeIssue {
212 file_path: PathBuf::from("test.rs"),
213 line: 1,
214 column: 0,
215 rule_name: rule.to_string(),
216 message: "test".to_string(),
217 severity: crate::analyzer::Severity::Spicy,
218 }
219 }
220
221 #[test]
226 fn test_empty_issues() {
227 let p = analyze(&[]);
228 assert_eq!(p.title, "The Perfectionist", "empty => Perfectionist");
229 assert_eq!(p.score, 100.0, "empty => score 100");
230 }
231
232 #[test]
237 fn test_unwrap_dominant() {
238 let issues = vec![
239 make_issue("unwrap-abuse"),
240 make_issue("unwrap-abuse"),
241 make_issue("unwrap-abuse"),
242 ];
243 let p = analyze(&issues);
244 assert_eq!(p.title, "The Optimist", "3 unwrap => Optimist");
245 }
246
247 #[test]
248 fn test_naming_dominant() {
249 let issues = vec![
250 make_issue("single-letter-variable"),
251 make_issue("meaningless-naming"),
252 ];
253 let p = analyze(&issues);
254 assert_eq!(p.title, "The Minimalist", "2 naming => Minimalist");
255 }
256
257 #[test]
258 fn test_nesting_dominant() {
259 let issues = vec![
260 make_issue("deep-nesting"),
261 make_issue("cyclomatic-complexity"),
262 make_issue("complex-closure"),
263 ];
264 let p = analyze(&issues);
265 assert_eq!(p.title, "The Architect", "3 nesting/complex => Architect");
266 }
267
268 #[test]
269 fn test_long_fn_dominant() {
270 let issues = vec![make_issue("long-function"), make_issue("file-too-long")];
271 let p = analyze(&issues);
272 assert_eq!(p.title, "The Storyteller", "2 long-fn => Storyteller");
273 }
274
275 #[test]
276 fn test_magic_dominant() {
277 let issues = vec![make_issue("magic-number"), make_issue("magic-number")];
278 let p = analyze(&issues);
279 assert_eq!(p.title, "The Sorcerer", "2 magic => Sorcerer");
280 }
281
282 #[test]
283 fn test_dup_dominant() {
284 let issues = vec![
285 make_issue("code-duplication"),
286 make_issue("code-duplication"),
287 make_issue("code-duplication"),
288 ];
289 let p = analyze(&issues);
290 assert_eq!(
291 p.title, "The Copy-Paste Artist",
292 "3 dup => Copy-Paste Artist"
293 );
294 }
295
296 #[test]
301 fn test_score_boundary_floor_at_zero() {
302 let issues: Vec<_> = (0..34).map(|_| make_issue("unwrap-abuse")).collect();
304 let p = analyze(&issues);
305 assert_eq!(p.title, "The Optimist");
306 assert_eq!(
307 p.score, 0.0,
308 "34 unwraps => score should floor at 0.0, got {}",
309 p.score
310 );
311 }
312
313 #[test]
315 fn test_score_exact_value_for_small_count() {
316 let issues = vec![make_issue("unwrap-abuse")];
317 let p = analyze(&issues);
318 assert_eq!(p.score, 97.0, "1 unwrap => 100 - 3 = 97, got {}", p.score);
319 }
320
321 #[test]
324 fn test_archetype_specific_multipliers() {
325 let naming = analyze(&[
327 make_issue("terrible-naming"),
328 make_issue("single-letter-variable"),
329 ]);
330 let nesting = analyze(&[make_issue("deep-nesting"), make_issue("complex-closure")]);
331 assert_eq!(naming.title, "The Minimalist");
332 assert_eq!(nesting.title, "The Architect");
333 assert!(
334 nesting.score < naming.score,
335 "nesting (mult 4) should have lower score than naming (mult 2) for same count: {} < {}",
336 nesting.score,
337 naming.score
338 );
339 }
340
341 #[test]
347 fn test_unrecognized_rules_fall_to_sorcerer() {
348 let issues = vec![make_issue("random_rule"), make_issue("another_unknown")];
349 let p = analyze(&issues);
350 assert_eq!(
352 p.title, "The Sorcerer",
353 "2 unknown => CodeSmells => Sorcerer"
354 );
355 assert!(
356 (p.score - 96.0).abs() < f64::EPSILON,
357 "2 magic => score should be 96 (100 - 2*2), got {}",
358 p.score
359 );
360 }
361
362 #[test]
367 fn test_case_insensitivity() {
368 let issues = vec![
369 make_issue("UNWRAP-ABUSE"),
370 make_issue("Unwrap-Abuse"),
371 make_issue("DEEP-NESTING"),
372 ];
373 let p = analyze(&issues);
374 assert_eq!(
376 p.title, "The Optimist",
377 "case-insensitive matching via to_lowercase: UPPER/mixed should match"
378 );
379 }
380
381 #[test]
386 fn test_tied_categories_pick_last() {
387 let issues = vec![
388 make_issue("unwrap-abuse"),
389 make_issue("terrible-naming"),
390 make_issue("deep-nesting"),
391 ];
392 let p = analyze(&issues);
393 assert_eq!(
395 p.title, "The Architect",
396 "tied at 1 between unwrap/nesting => last max (nesting) => Architect"
397 );
398 }
399
400 #[test]
403 fn test_score_formula_with_dominant_category() {
404 let issues = vec![
405 make_issue("code-duplication"),
406 make_issue("code-duplication"),
407 make_issue("code-duplication"),
408 make_issue("deep-nesting"),
409 ];
410 let p = analyze(&issues);
411 assert_eq!(
412 p.title, "The Copy-Paste Artist",
413 "3 dup + 1 nesting => dup dominant"
414 );
415 assert!(
416 (p.score - 91.0).abs() < f64::EPSILON,
417 "score should be 91 (100 - 3*3), got {}",
418 p.score
419 );
420 }
421}