Skip to main content

garbage_code_hunter/
scoring.rs

1use crate::analyzer::{CodeIssue, Severity};
2use std::collections::HashMap;
3
4/// Code quality rating system
5/// Score range: 0-100, the higher the score, the worse the code quality
6/// 0-20: Excellent
7/// 21-40: Good
8/// 41-60: Average
9/// 61-80: Poor
10/// 81-100: Terrible
11#[derive(Debug, Clone)]
12pub struct CodeQualityScore {
13    pub total_score: f64,
14    pub category_scores: HashMap<String, f64>,
15    pub file_count: usize,
16    pub total_lines: usize,
17    pub issue_density: f64,
18    pub severity_distribution: SeverityDistribution,
19    pub quality_level: QualityLevel,
20}
21
22#[derive(Debug, Clone)]
23pub struct SeverityDistribution {
24    pub nuclear: usize,
25    pub spicy: usize,
26    pub mild: usize,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub enum QualityLevel {
31    Excellent, // 0-20
32    Good,      // 21-40
33    Average,   // 41-60
34    Poor,      // 61-80
35    Terrible,  // 81-100
36}
37
38impl QualityLevel {
39    pub fn from_score(score: f64) -> Self {
40        match score as u32 {
41            0..=20 => QualityLevel::Excellent,
42            21..=40 => QualityLevel::Good,
43            41..=60 => QualityLevel::Average,
44            61..=80 => QualityLevel::Poor,
45            _ => QualityLevel::Terrible,
46        }
47    }
48
49    pub fn description(&self, lang: &str) -> &'static str {
50        match (self, lang) {
51            (QualityLevel::Excellent, "zh-CN") => "优秀",
52            (QualityLevel::Good, "zh-CN") => "良好",
53            (QualityLevel::Average, "zh-CN") => "一般",
54            (QualityLevel::Poor, "zh-CN") => "较差",
55            (QualityLevel::Terrible, "zh-CN") => "糟糕",
56            (QualityLevel::Excellent, _) => "Excellent",
57            (QualityLevel::Good, _) => "Good",
58            (QualityLevel::Average, _) => "Average",
59            (QualityLevel::Poor, _) => "Poor",
60            (QualityLevel::Terrible, _) => "Terrible",
61        }
62    }
63
64    pub fn emoji(&self) -> &'static str {
65        match self {
66            QualityLevel::Excellent => "🏆",
67            QualityLevel::Good => "👍",
68            QualityLevel::Average => "😐",
69            QualityLevel::Poor => "😞",
70            QualityLevel::Terrible => "💀",
71        }
72    }
73}
74
75pub struct CodeScorer;
76
77impl CodeScorer {
78    pub fn new() -> Self {
79        Self
80    }
81
82    /// calculate code quality score using normalized category-based approach
83    pub fn calculate_score(
84        &self,
85        issues: &[CodeIssue],
86        file_count: usize,
87        total_lines: usize,
88    ) -> CodeQualityScore {
89        if issues.is_empty() {
90            return CodeQualityScore {
91                total_score: 0.0, // Perfect score when no issues (0 = best)
92                category_scores: HashMap::new(),
93                file_count,
94                total_lines,
95                issue_density: 0.0,
96                severity_distribution: SeverityDistribution {
97                    nuclear: 0,
98                    spicy: 0,
99                    mild: 0,
100                },
101                quality_level: QualityLevel::Excellent,
102            };
103        }
104
105        // calculate severity distribution
106        let severity_distribution = self.calculate_severity_distribution(issues);
107
108        // calculate category scores (0-100 for each category)
109        let category_scores = self.calculate_normalized_category_scores(issues, total_lines);
110
111        // calculate weighted final score
112        let total_score = self.calculate_weighted_final_score(&category_scores);
113
114        let issue_density = if total_lines > 0 {
115            issues.len() as f64 / total_lines as f64 * 1000.0 // issues per 1000 lines
116        } else {
117            0.0
118        };
119
120        CodeQualityScore {
121            total_score,
122            category_scores,
123            file_count,
124            total_lines,
125            issue_density,
126            severity_distribution,
127            quality_level: QualityLevel::from_score(total_score),
128        }
129    }
130
131    fn calculate_severity_distribution(&self, issues: &[CodeIssue]) -> SeverityDistribution {
132        let mut nuclear = 0;
133        let mut spicy = 0;
134        let mut mild = 0;
135
136        for issue in issues {
137            match issue.severity {
138                Severity::Nuclear => nuclear += 1,
139                Severity::Spicy => spicy += 1,
140                Severity::Mild => mild += 1,
141            }
142        }
143
144        SeverityDistribution {
145            nuclear,
146            spicy,
147            mild,
148        }
149    }
150
151    /// Calculate normalized category scores (0-100 for each category)
152    fn calculate_normalized_category_scores(
153        &self,
154        issues: &[CodeIssue],
155        total_lines: usize,
156    ) -> HashMap<String, f64> {
157        let mut category_scores = HashMap::new();
158        let mut category_counts: HashMap<String, usize> = HashMap::new();
159
160        // Define categories with weights and thresholds
161        let categories = [
162            ("naming", vec!["terrible-naming", "single-letter-variable"]),
163            (
164                "complexity",
165                vec!["deep-nesting", "long-function", "cyclomatic-complexity"],
166            ),
167            ("duplication", vec!["code-duplication"]),
168            ("rust-basics", vec!["unwrap-abuse", "unnecessary-clone"]),
169            (
170                "advanced-rust",
171                vec![
172                    "complex-closure",
173                    "lifetime-abuse",
174                    "trait-complexity",
175                    "generic-abuse",
176                ],
177            ),
178            (
179                "rust-features",
180                vec![
181                    "channel-abuse",
182                    "async-abuse",
183                    "dyn-trait-abuse",
184                    "unsafe-abuse",
185                    "ffi-abuse",
186                    "macro-abuse",
187                ],
188            ),
189            (
190                "structure",
191                vec![
192                    "module-complexity",
193                    "pattern-matching-abuse",
194                    "reference-abuse",
195                    "box-abuse",
196                    "slice-abuse",
197                ],
198            ),
199        ];
200
201        // Count issues per category
202        for issue in issues {
203            for (category_name, rules) in &categories {
204                if rules.contains(&issue.rule_name.as_str()) {
205                    *category_counts
206                        .entry(category_name.to_string())
207                        .or_insert(0) += 1;
208                }
209            }
210        }
211
212        // Calculate normalized scores for each category (0-100)
213        for (category_name, _) in &categories {
214            let count = category_counts.get(*category_name).unwrap_or(&0);
215            let score = self.calculate_category_score(*count, total_lines, category_name);
216            category_scores.insert(category_name.to_string(), score);
217        }
218
219        category_scores
220    }
221
222    /// Calculate score for a specific category (0-100, where 0 is perfect, 100 is terrible, maximum 90)
223    fn calculate_category_score(
224        &self,
225        issue_count: usize,
226        total_lines: usize,
227        category: &str,
228    ) -> f64 {
229        if total_lines == 0 {
230            return 0.0; // Perfect score when no code
231        }
232
233        // Calculate issues per 1000 lines for this category
234        let issues_per_1k_lines = (issue_count as f64 / total_lines as f64) * 1000.0;
235
236        // Different thresholds for different categories
237        let (excellent_threshold, good_threshold, average_threshold, poor_threshold) =
238            match category {
239                "naming" => (0.0, 2.0, 5.0, 10.0), // Naming should be very clean
240                "complexity" => (0.0, 1.0, 3.0, 6.0), // Complexity should be low
241                "duplication" => (0.0, 0.5, 2.0, 4.0), // Duplication should be minimal
242                "rust-basics" => (0.0, 1.0, 3.0, 6.0), // Basic Rust issues
243                "advanced-rust" => (0.0, 0.5, 2.0, 4.0), // Advanced features should be used carefully
244                "rust-features" => (0.0, 0.5, 1.5, 3.0), // Special features should be rare
245                "structure" => (0.0, 1.0, 3.0, 6.0),     // Structure issues
246                _ => (0.0, 1.0, 3.0, 6.0),               // Default thresholds
247            };
248
249        // Calculate score based on thresholds (0 = excellent, 100 = terrible)
250
251        if issues_per_1k_lines <= excellent_threshold {
252            0.0 // Perfect score
253        } else if issues_per_1k_lines <= good_threshold {
254            (issues_per_1k_lines - excellent_threshold) / (good_threshold - excellent_threshold)
255                * 20.0
256        } else if issues_per_1k_lines <= average_threshold {
257            20.0 + (issues_per_1k_lines - good_threshold) / (average_threshold - good_threshold)
258                * 20.0
259        } else if issues_per_1k_lines <= poor_threshold {
260            40.0 + (issues_per_1k_lines - average_threshold) / (poor_threshold - average_threshold)
261                * 20.0
262        } else {
263            // Beyond poor threshold, score increases rapidly but caps at 90
264            let excess = issues_per_1k_lines - poor_threshold;
265            (60.0 + excess * 2.0).min(90.0) // Cap at 90 to avoid perfect 100
266        }
267    }
268
269    /// Calculate weighted final score from category scores
270    fn calculate_weighted_final_score(&self, category_scores: &HashMap<String, f64>) -> f64 {
271        // Category weights (should sum to 1.0)
272        let weights = [
273            ("naming", 0.25),        // 25% - Very important
274            ("complexity", 0.20),    // 20% - Very important
275            ("duplication", 0.15),   // 15% - Important
276            ("rust-basics", 0.15),   // 15% - Important
277            ("advanced-rust", 0.10), // 10% - Moderate
278            ("rust-features", 0.10), // 10% - Moderate
279            ("structure", 0.05),     // 5% - Less critical
280        ];
281
282        let mut weighted_sum = 0.0;
283        let mut total_weight = 0.0;
284
285        for (category, weight) in &weights {
286            if let Some(score) = category_scores.get(*category) {
287                weighted_sum += score * weight;
288                total_weight += weight;
289            }
290        }
291
292        if total_weight > 0.0 {
293            weighted_sum / total_weight
294        } else {
295            100.0 // Default to perfect score if no categories found
296        }
297    }
298}
299
300impl Default for CodeScorer {
301    fn default() -> Self {
302        Self::new()
303    }
304}