1use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16use crate::utils::types::{RunResult, Severity};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ReportStatistics {
21 pub severity_counts: SeverityCounts,
23 pub by_language: HashMap<String, usize>,
25 pub by_tool: HashMap<String, usize>,
27 pub by_rule: HashMap<String, RuleStats>,
29 pub top_files: Vec<FileStats>,
31 pub summary: SummaryMetrics,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct SeverityCounts {
38 pub errors: usize,
39 pub warnings: usize,
40 pub info: usize,
41}
42
43impl SeverityCounts {
44 pub fn total(&self) -> usize {
46 self.errors + self.warnings + self.info
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RuleStats {
53 pub code: String,
54 pub count: usize,
55 pub severity: String,
56 pub example_message: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct FileStats {
62 pub path: String,
63 pub issue_count: usize,
64 pub error_count: usize,
65 pub warning_count: usize,
66 pub info_count: usize,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SummaryMetrics {
72 pub total_files: usize,
73 pub files_with_issues: usize,
74 pub clean_file_percentage: f64,
75 pub issues_per_file: f64,
76 pub most_common_rule: Option<String>,
77 pub most_problematic_language: Option<String>,
78}
79
80impl ReportStatistics {
81 pub fn from_run_result(result: &RunResult) -> Self {
83 let mut severity_counts = SeverityCounts::default();
84 let mut by_language: HashMap<String, usize> = HashMap::new();
85 let mut by_tool: HashMap<String, usize> = HashMap::new();
86 let mut by_rule: HashMap<String, RuleStats> = HashMap::new();
87 let mut file_stats: HashMap<String, FileStats> = HashMap::new();
88
89 for issue in &result.issues {
91 match issue.severity {
93 Severity::Error => severity_counts.errors += 1,
94 Severity::Warning => severity_counts.warnings += 1,
95 Severity::Info => severity_counts.info += 1,
96 }
97
98 if let Some(ref lang) = issue.language {
100 *by_language.entry(lang.name().to_string()).or_insert(0) += 1;
101 }
102
103 if let Some(ref source) = issue.source {
105 *by_tool.entry(source.clone()).or_insert(0) += 1;
106 }
107
108 if let Some(ref code) = issue.code {
110 by_rule
111 .entry(code.clone())
112 .and_modify(|stats| stats.count += 1)
113 .or_insert(RuleStats {
114 code: code.clone(),
115 count: 1,
116 severity: format!("{}", issue.severity),
117 example_message: issue.message.clone(),
118 });
119 }
120
121 let path_str = issue.file_path.to_string_lossy().to_string();
123 let file_stat = file_stats.entry(path_str.clone()).or_insert(FileStats {
124 path: path_str,
125 issue_count: 0,
126 error_count: 0,
127 warning_count: 0,
128 info_count: 0,
129 });
130 file_stat.issue_count += 1;
131 match issue.severity {
132 Severity::Error => file_stat.error_count += 1,
133 Severity::Warning => file_stat.warning_count += 1,
134 Severity::Info => file_stat.info_count += 1,
135 }
136 }
137
138 let mut top_files: Vec<FileStats> = file_stats.into_values().collect();
140 top_files.sort_by(|a, b| b.issue_count.cmp(&a.issue_count));
141 top_files.truncate(10); let most_common_rule = by_rule
145 .iter()
146 .max_by_key(|(_, stats)| stats.count)
147 .map(|(code, _)| code.clone());
148
149 let most_problematic_language = by_language
151 .iter()
152 .max_by_key(|(_, count)| *count)
153 .map(|(lang, _)| lang.clone());
154
155 let total_issues = severity_counts.total();
157 let clean_files = result.total_files.saturating_sub(result.files_with_issues);
158 let clean_file_percentage = if result.total_files > 0 {
159 (clean_files as f64 / result.total_files as f64) * 100.0
160 } else {
161 100.0
162 };
163 let issues_per_file = if result.files_with_issues > 0 {
164 total_issues as f64 / result.files_with_issues as f64
165 } else {
166 0.0
167 };
168
169 let summary = SummaryMetrics {
170 total_files: result.total_files,
171 files_with_issues: result.files_with_issues,
172 clean_file_percentage,
173 issues_per_file,
174 most_common_rule,
175 most_problematic_language,
176 };
177
178 Self {
179 severity_counts,
180 by_language,
181 by_tool,
182 by_rule,
183 top_files,
184 summary,
185 }
186 }
187
188 pub fn format_human(&self) -> String {
190 let mut output = String::new();
191
192 output.push_str("=== Lint Statistics ===\n\n");
194
195 output.push_str("Severity Breakdown:\n");
197 output.push_str(&format!(" Errors: {}\n", self.severity_counts.errors));
198 output.push_str(&format!(" Warnings: {}\n", self.severity_counts.warnings));
199 output.push_str(&format!(" Info: {}\n", self.severity_counts.info));
200 output.push_str(&format!(" Total: {}\n\n", self.severity_counts.total()));
201
202 output.push_str("File Summary:\n");
204 output.push_str(&format!(
205 " Total files: {}\n",
206 self.summary.total_files
207 ));
208 output.push_str(&format!(
209 " Files with issues: {}\n",
210 self.summary.files_with_issues
211 ));
212 output.push_str(&format!(
213 " Clean files: {:.1}%\n",
214 self.summary.clean_file_percentage
215 ));
216 output.push_str(&format!(
217 " Issues per file: {:.1}\n\n",
218 self.summary.issues_per_file
219 ));
220
221 if !self.by_language.is_empty() {
223 output.push_str("Issues by Language:\n");
224 let mut langs: Vec<_> = self.by_language.iter().collect();
225 langs.sort_by(|a, b| b.1.cmp(a.1));
226 for (lang, count) in langs {
227 output.push_str(&format!(" {}: {}\n", lang, count));
228 }
229 output.push('\n');
230 }
231
232 if !self.by_tool.is_empty() {
234 output.push_str("Issues by Tool:\n");
235 let mut tools: Vec<_> = self.by_tool.iter().collect();
236 tools.sort_by(|a, b| b.1.cmp(a.1));
237 for (tool, count) in tools {
238 output.push_str(&format!(" {}: {}\n", tool, count));
239 }
240 output.push('\n');
241 }
242
243 if !self.by_rule.is_empty() {
245 output.push_str("Top Rule Violations:\n");
246 let mut rules: Vec<_> = self.by_rule.values().collect();
247 rules.sort_by(|a, b| b.count.cmp(&a.count));
248 for rule in rules.iter().take(5) {
249 output.push_str(&format!(
250 " {} ({}): {} occurrences\n",
251 rule.code, rule.severity, rule.count
252 ));
253 }
254 output.push('\n');
255 }
256
257 if !self.top_files.is_empty() {
259 output.push_str("Top Problematic Files:\n");
260 for file in self.top_files.iter().take(5) {
261 output.push_str(&format!(
262 " {} - {} issues ({} errors, {} warnings)\n",
263 file.path, file.issue_count, file.error_count, file.warning_count
264 ));
265 }
266 }
267
268 output
269 }
270
271 pub fn format_json(&self) -> String {
273 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use crate::utils::types::LintIssue;
281 use std::path::PathBuf;
282
283 fn make_issue(severity: Severity, lang: &str, tool: &str, code: &str) -> LintIssue {
284 use crate::Language;
285 let mut issue = LintIssue::new(
286 PathBuf::from("test.rs"),
287 1,
288 "Test message".to_string(),
289 severity,
290 );
291 issue.language = Language::from_name(lang);
292 issue.source = Some(tool.to_string());
293 issue.code = Some(code.to_string());
294 issue
295 }
296
297 #[test]
298 fn test_statistics_from_run_result() {
299 let mut result = RunResult::new();
300 result.total_files = 10;
301 result.files_with_issues = 3;
302 result.add_issue(make_issue(Severity::Error, "rust", "clippy", "E0001"));
303 result.add_issue(make_issue(Severity::Warning, "rust", "clippy", "W0001"));
304 result.add_issue(make_issue(Severity::Warning, "python", "ruff", "W0001"));
305 result.add_issue(make_issue(Severity::Info, "python", "ruff", "I0001"));
306
307 let stats = ReportStatistics::from_run_result(&result);
308
309 assert_eq!(stats.severity_counts.errors, 1);
310 assert_eq!(stats.severity_counts.warnings, 2);
311 assert_eq!(stats.severity_counts.info, 1);
312 assert_eq!(stats.by_language.get("rust"), Some(&2));
313 assert_eq!(stats.by_language.get("python"), Some(&2));
314 assert_eq!(stats.by_tool.get("clippy"), Some(&2));
315 assert_eq!(stats.by_tool.get("ruff"), Some(&2));
316 assert_eq!(stats.summary.total_files, 10);
317 assert_eq!(stats.summary.files_with_issues, 3);
318 }
319
320 #[test]
321 fn test_severity_counts_total() {
322 let counts = SeverityCounts {
323 errors: 5,
324 warnings: 10,
325 info: 3,
326 };
327 assert_eq!(counts.total(), 18);
328 }
329
330 #[test]
331 fn test_format_human() {
332 let stats = ReportStatistics {
333 severity_counts: SeverityCounts {
334 errors: 2,
335 warnings: 5,
336 info: 1,
337 },
338 by_language: [("rust".to_string(), 5), ("python".to_string(), 3)]
339 .into_iter()
340 .collect(),
341 by_tool: [("clippy".to_string(), 5), ("ruff".to_string(), 3)]
342 .into_iter()
343 .collect(),
344 by_rule: HashMap::new(),
345 top_files: vec![],
346 summary: SummaryMetrics {
347 total_files: 10,
348 files_with_issues: 3,
349 clean_file_percentage: 70.0,
350 issues_per_file: 2.7,
351 most_common_rule: None,
352 most_problematic_language: Some("rust".to_string()),
353 },
354 };
355
356 let output = stats.format_human();
357 assert!(output.contains("Errors: 2"));
358 assert!(output.contains("Warnings: 5"));
359 assert!(output.contains("rust: 5"));
360 }
361}