1use crate::Finding;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::Path;
5
6#[derive(Debug, Serialize, Deserialize, Default)]
8pub struct Baseline {
9 pub fingerprints: HashSet<String>,
10}
11
12impl Baseline {
13 pub fn from_findings(findings: &[Finding], project_root: &Path) -> Self {
15 let fingerprints = findings
16 .iter()
17 .map(|f| fingerprint(f, project_root))
18 .collect();
19 Self { fingerprints }
20 }
21
22 pub fn load(path: &Path) -> Option<Self> {
24 let content = std::fs::read_to_string(path).ok()?;
25 serde_json::from_str(&content).ok()
26 }
27
28 pub fn save(&self, path: &Path) -> std::io::Result<()> {
30 if let Some(dir) = path.parent() {
31 std::fs::create_dir_all(dir)?;
32 }
33 let json = serde_json::to_string_pretty(self).unwrap_or_default();
34 std::fs::write(path, json)
35 }
36
37 pub fn filter_new(&self, findings: Vec<Finding>, project_root: &Path) -> Vec<Finding> {
39 findings
40 .into_iter()
41 .filter(|f| !self.fingerprints.contains(&fingerprint(f, project_root)))
42 .collect()
43 }
44}
45
46fn fingerprint(f: &Finding, project_root: &Path) -> String {
49 let rel = f
50 .location
51 .path
52 .strip_prefix(project_root)
53 .unwrap_or(&f.location.path)
54 .to_string_lossy();
55 let symbol = f.location.name.as_deref().unwrap_or("");
56 format!("{}:{}:{}", f.smell_name, rel, symbol)
57}