1use std::collections::HashMap;
2use crate::analyzer::{CodeIssue, Severity};
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 rule_weights: HashMap<String, f64>,
78 severity_weights: HashMap<Severity, f64>,
80}
81
82impl CodeScorer {
83 pub fn new() -> Self {
84 let mut rule_weights = HashMap::new();
85
86 rule_weights.insert("terrible-naming".to_string(), 2.0);
88 rule_weights.insert("single-letter-variable".to_string(), 1.5);
89
90 rule_weights.insert("deep-nesting".to_string(), 3.0);
92 rule_weights.insert("long-function".to_string(), 2.5);
93
94 rule_weights.insert("unwrap-abuse".to_string(), 4.0); rule_weights.insert("unnecessary-clone".to_string(), 2.0);
97
98 rule_weights.insert("complex-closure".to_string(), 2.5);
100 rule_weights.insert("lifetime-abuse".to_string(), 3.5);
101 rule_weights.insert("trait-complexity".to_string(), 3.0);
102 rule_weights.insert("generic-abuse".to_string(), 2.5);
103
104 rule_weights.insert("channel-abuse".to_string(), 3.0);
106 rule_weights.insert("async-abuse".to_string(), 3.5);
107 rule_weights.insert("dyn-trait-abuse".to_string(), 2.5);
108 rule_weights.insert("unsafe-abuse".to_string(), 5.0); rule_weights.insert("ffi-abuse".to_string(), 4.5); rule_weights.insert("macro-abuse".to_string(), 3.0);
111 rule_weights.insert("module-complexity".to_string(), 2.0);
112 rule_weights.insert("pattern-matching-abuse".to_string(), 2.0);
113 rule_weights.insert("reference-abuse".to_string(), 2.5);
114 rule_weights.insert("box-abuse".to_string(), 2.0);
115 rule_weights.insert("slice-abuse".to_string(), 1.5);
116
117 let mut severity_weights = HashMap::new();
118 severity_weights.insert(Severity::Nuclear, 10.0); severity_weights.insert(Severity::Spicy, 5.0); severity_weights.insert(Severity::Mild, 2.0); Self {
123 rule_weights,
124 severity_weights,
125 }
126 }
127
128 pub fn calculate_score(&self, issues: &[CodeIssue], file_count: usize, total_lines: usize) -> CodeQualityScore {
130 if issues.is_empty() {
131 return CodeQualityScore {
132 total_score: 0.0,
133 category_scores: HashMap::new(),
134 file_count,
135 total_lines,
136 issue_density: 0.0,
137 severity_distribution: SeverityDistribution { nuclear: 0, spicy: 0, mild: 0 },
138 quality_level: QualityLevel::Excellent,
139 };
140 }
141
142 let severity_distribution = self.calculate_severity_distribution(issues);
144
145 let base_score = self.calculate_base_score(issues);
147
148 let density_penalty = self.calculate_density_penalty(issues.len(), file_count, total_lines);
150
151 let severity_penalty = self.calculate_severity_penalty(&severity_distribution);
153
154 let category_scores = self.calculate_category_scores(issues);
156
157 let total_score = (base_score + density_penalty + severity_penalty).min(100.0);
159
160 let issue_density = if total_lines > 0 {
161 issues.len() as f64 / total_lines as f64 * 1000.0 } else {
163 0.0
164 };
165
166 CodeQualityScore {
167 total_score,
168 category_scores,
169 file_count,
170 total_lines,
171 issue_density,
172 severity_distribution,
173 quality_level: QualityLevel::from_score(total_score),
174 }
175 }
176
177 fn calculate_severity_distribution(&self, issues: &[CodeIssue]) -> SeverityDistribution {
178 let mut nuclear = 0;
179 let mut spicy = 0;
180 let mut mild = 0;
181
182 for issue in issues {
183 match issue.severity {
184 Severity::Nuclear => nuclear += 1,
185 Severity::Spicy => spicy += 1,
186 Severity::Mild => mild += 1,
187 }
188 }
189
190 SeverityDistribution { nuclear, spicy, mild }
191 }
192
193 fn calculate_base_score(&self, issues: &[CodeIssue]) -> f64 {
194 let mut score = 0.0;
195
196 for issue in issues {
197 let rule_weight = self.rule_weights.get(&issue.rule_name).unwrap_or(&1.0);
198 let severity_weight = self.severity_weights.get(&issue.severity).unwrap_or(&1.0);
199
200 score += rule_weight * severity_weight;
202 }
203
204 score
205 }
206
207 fn calculate_density_penalty(&self, issue_count: usize, file_count: usize, total_lines: usize) -> f64 {
208 if total_lines == 0 || file_count == 0 {
209 return 0.0;
210 }
211
212 let issues_per_1000_lines = (issue_count as f64 / total_lines as f64) * 1000.0;
214
215 let issues_per_file = issue_count as f64 / file_count as f64;
217
218 let density_penalty = match issues_per_1000_lines {
220 x if x > 50.0 => 25.0, x if x > 30.0 => 15.0, x if x > 20.0 => 10.0, x if x > 10.0 => 5.0, _ => 0.0, };
226
227 let file_penalty = match issues_per_file {
229 x if x > 20.0 => 15.0,
230 x if x > 10.0 => 10.0,
231 x if x > 5.0 => 5.0,
232 _ => 0.0,
233 };
234
235 density_penalty + file_penalty
236 }
237
238 fn calculate_severity_penalty(&self, distribution: &SeverityDistribution) -> f64 {
239 let mut penalty = 0.0;
240
241 if distribution.nuclear > 0 {
243 penalty += 20.0 + (distribution.nuclear as f64 - 1.0) * 5.0; }
245
246 if distribution.spicy > 5 {
248 penalty += (distribution.spicy as f64 - 5.0) * 2.0; }
250
251 if distribution.mild > 20 {
253 penalty += (distribution.mild as f64 - 20.0) * 0.5; }
255
256 penalty
257 }
258
259 fn calculate_category_scores(&self, issues: &[CodeIssue]) -> HashMap<String, f64> {
260 let mut category_scores = HashMap::new();
261 let mut category_counts: HashMap<String, usize> = HashMap::new();
262
263 let categories = [
265 ("naming", vec!["terrible-naming", "single-letter-variable"]),
266 ("complexity", vec!["deep-nesting", "long-function"]),
267 ("rust-basics", vec!["unwrap-abuse", "unnecessary-clone"]),
268 ("advanced-rust", vec!["complex-closure", "lifetime-abuse", "trait-complexity", "generic-abuse"]),
269 ("rust-features", vec!["channel-abuse", "async-abuse", "dyn-trait-abuse", "unsafe-abuse", "ffi-abuse", "macro-abuse"]),
270 ("structure", vec!["module-complexity", "pattern-matching-abuse", "reference-abuse", "box-abuse", "slice-abuse"]),
271 ];
272
273 for issue in issues {
275 for (category_name, rules) in &categories {
276 if rules.contains(&issue.rule_name.as_str()) {
277 *category_counts.entry(category_name.to_string()).or_insert(0) += 1;
278
279 let rule_weight = self.rule_weights.get(&issue.rule_name).unwrap_or(&1.0);
280 let severity_weight = self.severity_weights.get(&issue.severity).unwrap_or(&1.0);
281
282 *category_scores.entry(category_name.to_string()).or_insert(0.0) +=
283 rule_weight * severity_weight;
284 }
285 }
286 }
287
288 category_scores
289 }
290}
291
292impl Default for CodeScorer {
293 fn default() -> Self {
294 Self::new()
295 }
296}