Skip to main content

garbage_code_hunter/personality/
profiles.rs

1//! Personality profiles based on code issue patterns.
2
3use super::Personality;
4use crate::analyzer::CodeIssue;
5use crate::signals::{classify_rule, StyleProfile, StyleSignal};
6use std::collections::HashMap;
7
8/// Analyze issues and determine a personality profile.
9pub 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    // ── empty input ──────────────────────────────────────────────
222
223    /// Objective: Verify empty issues return "The Perfectionist" with score 100.
224    /// Invariants: The early-return path is taken when total == 0.
225    #[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    // ── dominant archetype detection ─────────────────────────────
233
234    /// Objective: Verify each archetype is selected when its category has the highest count.
235    /// Invariants: The category with the most issues determines the archetype.
236    #[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    // ── score edge cases ─────────────────────────────────────────
297
298    /// Objective: Verify score floors at 0.0 when count * multiplier >= 100.
299    /// Invariants: score = max(100 - count * multiplier, 0). Must not go negative.
300    #[test]
301    fn test_score_boundary_floor_at_zero() {
302        // 34 unwraps => 100 - 34*3 = -2 => clamped to 0
303        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    /// Objective: Verify score is exactly 100 - n*multiplier for small n (not clamped).
314    #[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    /// Objective: Verify each archetype has its own multiplier.
322    /// Invariants: Same count but different category => different score.
323    #[test]
324    fn test_archetype_specific_multipliers() {
325        // naming has multiplier 2.0, nesting has 4.0
326        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    // ── unrecognized rules ───────────────────────────────────────
342
343    /// Objective: Verify that unrecognized rule names fall into CodeSmells (catch-all) and
344    ///            contribute to "The Sorcerer" personality, reflecting uncategorized smells.
345    /// Invariants: classify_rule maps all unlisted rule names to StyleSignal::CodeSmells.
346    #[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        // Both map to CodeSmells => magic_count = 2 => The Sorcerer, score = 100 - 2*2 = 96
351        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    // ── case insensitivity ───────────────────────────────────────
363
364    /// Objective: Verify rule name matching is case-insensitive via to_lowercase() before classify_rule.
365    /// Invariants: to_lowercase() normalizes UPPER/Mixed case to match classify_rule's lowercase strings.
366    #[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        // 2 PanicAddiction + 1 NestedHell => panic_addiction=2 dominant => Optimist
375        assert_eq!(
376            p.title, "The Optimist",
377            "case-insensitive matching via to_lowercase: UPPER/mixed should match"
378        );
379    }
380
381    // ── balanced personality ──────────────────────────────────────
382
383    /// Objective: Verify that when categories are tied, max_by_key returns the LAST tied max.
384    /// Invariants: unwrap=1, nesting=1, others=0 => last max with value 1 is nesting => Architect.
385    #[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        // PanicAddiction=1, NamingChaos=1, NestedHell=1 => last max value 1 is nesting => Architect
394        assert_eq!(
395            p.title, "The Architect",
396            "tied at 1 between unwrap/nesting => last max (nesting) => Architect"
397        );
398    }
399
400    /// Objective: Verify score is positive for 4 issues with a clear dominant category.
401    /// Invariants: 3 dups + 1 nesting => dup dominant => score = 100 - 3*3 = 91.
402    #[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}