Skip to main content

raysense/
baseline.rs

1/*
2 *   Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
3 *   All rights reserved.
4 *
5 *   Permission is hereby granted, free of charge, to any person obtaining a copy
6 *   of this software and associated documentation files (the "Software"), to deal
7 *   in the Software without restriction, including without limitation the rights
8 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 *   copies of the Software, and to permit persons to whom the Software is
10 *   furnished to do so, subject to the following conditions:
11 *
12 *   The above copyright notice and this permission notice shall be included in all
13 *   copies or substantial portions of the Software.
14 *
15 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 *   SOFTWARE.
22 */
23
24use 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}