1use crate::analyzer::{CodeIssue, Severity};
2use std::collections::HashMap;
3
4#[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, Good, Average, Poor, Terrible, }
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 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, 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 let severity_distribution = self.calculate_severity_distribution(issues);
107
108 let category_scores = self.calculate_normalized_category_scores(issues, total_lines);
110
111 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 } 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 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 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 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 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 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; }
232
233 let issues_per_1k_lines = (issue_count as f64 / total_lines as f64) * 1000.0;
235
236 let (excellent_threshold, good_threshold, average_threshold, poor_threshold) =
238 match category {
239 "naming" => (0.0, 2.0, 5.0, 10.0), "complexity" => (0.0, 1.0, 3.0, 6.0), "duplication" => (0.0, 0.5, 2.0, 4.0), "rust-basics" => (0.0, 1.0, 3.0, 6.0), "advanced-rust" => (0.0, 0.5, 2.0, 4.0), "rust-features" => (0.0, 0.5, 1.5, 3.0), "structure" => (0.0, 1.0, 3.0, 6.0), _ => (0.0, 1.0, 3.0, 6.0), };
248
249 if issues_per_1k_lines <= excellent_threshold {
252 0.0 } 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 let excess = issues_per_1k_lines - poor_threshold;
265 (60.0 + excess * 2.0).min(90.0) }
267 }
268
269 fn calculate_weighted_final_score(&self, category_scores: &HashMap<String, f64>) -> f64 {
271 let weights = [
273 ("naming", 0.25), ("complexity", 0.20), ("duplication", 0.15), ("rust-basics", 0.15), ("advanced-rust", 0.10), ("rust-features", 0.10), ("structure", 0.05), ];
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 }
297 }
298}
299
300impl Default for CodeScorer {
301 fn default() -> Self {
302 Self::new()
303 }
304}