1use crate::facts::ScanReport;
25use crate::health::{FileHotspot, HealthSummary, RuleFinding};
26use serde::{Deserialize, Serialize};
27use std::collections::{BTreeMap, BTreeSet};
28use std::path::PathBuf;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ProjectBaseline {
32 pub schema_version: u32,
33 pub root: PathBuf,
34 pub snapshot_id: String,
35 pub file_count: usize,
36 pub function_count: usize,
37 pub import_count: usize,
38 pub call_count: usize,
39 pub call_edge_count: usize,
40 pub score: u8,
41 pub coverage_score: u8,
42 pub structural_score: u8,
43 pub rules: Vec<RuleFinding>,
44 pub hotspots: Vec<FileHotspot>,
45 pub module_edges: Vec<BaselineModuleEdge>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49pub struct BaselineModuleEdge {
50 pub from_module: String,
51 pub to_module: String,
52 pub edges: usize,
53}
54
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct BaselineDiff {
57 pub score_delta: i16,
58 pub coverage_score_delta: i16,
59 pub structural_score_delta: i16,
60 pub file_count_delta: isize,
61 pub function_count_delta: isize,
62 pub import_count_delta: isize,
63 pub call_count_delta: isize,
64 pub call_edge_count_delta: isize,
65 pub added_rules: Vec<RuleFinding>,
66 pub removed_rules: Vec<RuleFinding>,
67 pub added_hotspots: Vec<FileHotspot>,
68 pub removed_hotspots: Vec<FileHotspot>,
69 pub added_module_edges: Vec<BaselineModuleEdge>,
70 pub removed_module_edges: Vec<BaselineModuleEdge>,
71 pub changed_module_edges: Vec<ModuleEdgeDelta>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ModuleEdgeDelta {
76 pub from_module: String,
77 pub to_module: String,
78 pub before: usize,
79 pub after: usize,
80 pub delta: isize,
81}
82
83pub fn build_baseline(report: &ScanReport, health: &HealthSummary) -> ProjectBaseline {
84 ProjectBaseline {
85 schema_version: 1,
86 root: report.snapshot.root.clone(),
87 snapshot_id: report.snapshot.snapshot_id.clone(),
88 file_count: report.snapshot.file_count,
89 function_count: report.snapshot.function_count,
90 import_count: report.snapshot.import_count,
91 call_count: report.snapshot.call_count,
92 call_edge_count: report.call_edges.len(),
93 score: health.score,
94 coverage_score: health.coverage_score,
95 structural_score: health.structural_score,
96 rules: health.rules.clone(),
97 hotspots: health.hotspots.clone(),
98 module_edges: health
99 .metrics
100 .dsm
101 .top_module_edges
102 .iter()
103 .map(|edge| BaselineModuleEdge {
104 from_module: edge.from_module.clone(),
105 to_module: edge.to_module.clone(),
106 edges: edge.edges,
107 })
108 .collect(),
109 }
110}
111
112pub fn diff_baselines(before: &ProjectBaseline, after: &ProjectBaseline) -> BaselineDiff {
113 let before_rules = keyed_rules(&before.rules);
114 let after_rules = keyed_rules(&after.rules);
115 let before_hotspots = keyed_hotspots(&before.hotspots);
116 let after_hotspots = keyed_hotspots(&after.hotspots);
117 let before_module_edges = keyed_module_edges(&before.module_edges);
118 let after_module_edges = keyed_module_edges(&after.module_edges);
119
120 BaselineDiff {
121 score_delta: after.score as i16 - before.score as i16,
122 coverage_score_delta: after.coverage_score as i16 - before.coverage_score as i16,
123 structural_score_delta: after.structural_score as i16 - before.structural_score as i16,
124 file_count_delta: after.file_count as isize - before.file_count as isize,
125 function_count_delta: after.function_count as isize - before.function_count as isize,
126 import_count_delta: after.import_count as isize - before.import_count as isize,
127 call_count_delta: after.call_count as isize - before.call_count as isize,
128 call_edge_count_delta: after.call_edge_count as isize - before.call_edge_count as isize,
129 added_rules: added_values(&before_rules, &after_rules),
130 removed_rules: removed_values(&before_rules, &after_rules),
131 added_hotspots: added_values(&before_hotspots, &after_hotspots),
132 removed_hotspots: removed_values(&before_hotspots, &after_hotspots),
133 added_module_edges: added_values(&before_module_edges, &after_module_edges),
134 removed_module_edges: removed_values(&before_module_edges, &after_module_edges),
135 changed_module_edges: changed_module_edges(&before_module_edges, &after_module_edges),
136 }
137}
138
139fn keyed_rules(rules: &[RuleFinding]) -> BTreeMap<String, RuleFinding> {
140 rules
141 .iter()
142 .map(|rule| {
143 (
144 format!(
145 "{}\u{1f}{}\u{1f}{}\u{1f}{:?}",
146 rule.code, rule.path, rule.message, rule.severity
147 ),
148 rule.clone(),
149 )
150 })
151 .collect()
152}
153
154fn keyed_hotspots(hotspots: &[FileHotspot]) -> BTreeMap<String, FileHotspot> {
155 hotspots
156 .iter()
157 .map(|hotspot| (hotspot.path.clone(), hotspot.clone()))
158 .collect()
159}
160
161fn keyed_module_edges(edges: &[BaselineModuleEdge]) -> BTreeMap<String, BaselineModuleEdge> {
162 edges
163 .iter()
164 .map(|edge| {
165 (
166 format!("{}\u{1f}{}", edge.from_module, edge.to_module),
167 edge.clone(),
168 )
169 })
170 .collect()
171}
172
173fn added_values<T: Clone>(before: &BTreeMap<String, T>, after: &BTreeMap<String, T>) -> Vec<T> {
174 after
175 .iter()
176 .filter(|(key, _)| !before.contains_key(*key))
177 .map(|(_, value)| value.clone())
178 .collect()
179}
180
181fn removed_values<T: Clone>(before: &BTreeMap<String, T>, after: &BTreeMap<String, T>) -> Vec<T> {
182 before
183 .iter()
184 .filter(|(key, _)| !after.contains_key(*key))
185 .map(|(_, value)| value.clone())
186 .collect()
187}
188
189fn changed_module_edges(
190 before: &BTreeMap<String, BaselineModuleEdge>,
191 after: &BTreeMap<String, BaselineModuleEdge>,
192) -> Vec<ModuleEdgeDelta> {
193 before
194 .keys()
195 .chain(after.keys())
196 .collect::<BTreeSet<_>>()
197 .into_iter()
198 .filter_map(|key| {
199 let before = before.get(key)?;
200 let after = after.get(key)?;
201 if before.edges == after.edges {
202 return None;
203 }
204 Some(ModuleEdgeDelta {
205 from_module: after.from_module.clone(),
206 to_module: after.to_module.clone(),
207 before: before.edges,
208 after: after.edges,
209 delta: after.edges as isize - before.edges as isize,
210 })
211 })
212 .collect()
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::health::RuleSeverity;
219
220 #[test]
221 fn diffs_baseline_score_and_rules() {
222 let before = baseline(90, vec![rule("old")], vec![edge("a", "b", 1)]);
223 let after = baseline(95, vec![rule("new")], vec![edge("a", "b", 3)]);
224
225 let diff = diff_baselines(&before, &after);
226
227 assert_eq!(diff.score_delta, 5);
228 assert_eq!(diff.added_rules.len(), 1);
229 assert_eq!(diff.added_rules[0].code, "new");
230 assert_eq!(diff.removed_rules.len(), 1);
231 assert_eq!(diff.changed_module_edges.len(), 1);
232 assert_eq!(diff.changed_module_edges[0].delta, 2);
233 }
234
235 fn baseline(
236 score: u8,
237 rules: Vec<RuleFinding>,
238 module_edges: Vec<BaselineModuleEdge>,
239 ) -> ProjectBaseline {
240 ProjectBaseline {
241 schema_version: 1,
242 root: PathBuf::from("."),
243 snapshot_id: String::new(),
244 file_count: 0,
245 function_count: 0,
246 import_count: 0,
247 call_count: 0,
248 call_edge_count: 0,
249 score,
250 coverage_score: score,
251 structural_score: score,
252 rules,
253 hotspots: Vec::new(),
254 module_edges,
255 }
256 }
257
258 fn rule(code: &str) -> RuleFinding {
259 RuleFinding {
260 severity: RuleSeverity::Info,
261 code: code.to_string(),
262 path: "src/lib.rs".to_string(),
263 message: "message".to_string(),
264 }
265 }
266
267 fn edge(from: &str, to: &str, edges: usize) -> BaselineModuleEdge {
268 BaselineModuleEdge {
269 from_module: from.to_string(),
270 to_module: to.to_string(),
271 edges,
272 }
273 }
274}