xbp_analysis/domain/
report.rs1use super::types::{Finding, LanguageId, Severity};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeMap;
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct AnalysisReport {
9 pub findings: Vec<Finding>,
10 pub files_analyzed: usize,
11 pub languages: Vec<LanguageId>,
12 pub duration_ms: u64,
13 pub skipped_rules: Vec<SkippedRule>,
14 pub stats: ReportStats,
15}
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct SkippedRule {
19 pub rule_id: String,
20 pub reason: String,
21}
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct ReportStats {
25 pub by_severity: BTreeMap<String, usize>,
26 pub by_rule: BTreeMap<String, usize>,
27 pub definite_bugs: usize,
28 pub review_required: usize,
29}
30
31impl AnalysisReport {
32 pub fn recompute_stats(&mut self) {
33 let mut by_severity = BTreeMap::new();
34 let mut by_rule = BTreeMap::new();
35 let mut definite_bugs = 0;
36 let mut review_required = 0;
37 for f in &self.findings {
38 *by_severity
39 .entry(f.severity.as_str().to_string())
40 .or_insert(0) += 1;
41 *by_rule.entry(f.rule_id.clone()).or_insert(0) += 1;
42 if f.definite_bug {
43 definite_bugs += 1;
44 } else {
45 review_required += 1;
46 }
47 }
48 self.stats = ReportStats {
49 by_severity,
50 by_rule,
51 definite_bugs,
52 review_required,
53 };
54 }
55
56 pub fn filter_min_severity(&mut self, min: Severity) {
57 self.findings.retain(|f| f.severity >= min);
58 self.recompute_stats();
59 }
60
61 pub fn sort_stable(&mut self) {
62 self.findings.sort_by(|a, b| {
63 a.location
64 .file
65 .cmp(&b.location.file)
66 .then(a.location.start_line.cmp(&b.location.start_line))
67 .then(a.location.start_column.cmp(&b.location.start_column))
68 .then(a.rule_id.cmp(&b.rule_id))
69 .then(a.id.cmp(&b.id))
70 });
71 }
72}