dry4rust/suppression/
baseline_file.rs1use std::fs;
7use std::io::Error as IoError;
8use std::io::ErrorKind;
9use std::path::Path;
10use std::path::PathBuf;
11
12use serde::{Deserialize, Serialize};
13use serde_json::from_str;
14use serde_json::to_string_pretty;
15
16use crate::analysis::AnalysisResult;
17use crate::error::Error;
18use crate::error::Result;
19use crate::grouper::DuplicateGroup;
20use crate::suppression::baseline_entry::BaselineEntry;
21use crate::suppression::baseline_kind::BaselineKind;
22
23pub const DEFAULT_BASELINE_FILE: &str = "dry4rust-baseline.json";
30
31pub const FORMAT_VERSION: u32 = 1;
33
34#[must_use]
39pub fn baseline_path(root: &Path, configured: Option<&Path>) -> PathBuf {
40 let named = configured.unwrap_or_else(|| Path::new(DEFAULT_BASELINE_FILE));
41 if named.is_absolute() {
42 named.to_path_buf()
43 } else {
44 root.join(named)
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct BaselineFile {
51 pub version: u32,
53 pub entries: Vec<BaselineEntry>,
55}
56
57impl BaselineFile {
58 #[must_use]
64 pub fn record(result: &AnalysisResult) -> Self {
65 let mut entries: Vec<BaselineEntry> = [
66 (BaselineKind::Exact, &result.exact_groups),
67 (BaselineKind::Near, &result.near_groups),
68 (BaselineKind::SubExact, &result.sub_exact_groups),
69 (BaselineKind::SubNear, &result.sub_near_groups),
70 ]
71 .into_iter()
72 .flat_map(|(kind, groups)| groups.iter().map(move |g| BaselineEntry::of(kind, g)))
73 .collect();
74 entries.sort_by(|a, b| {
75 a.kind
76 .cmp(&b.kind)
77 .then_with(|| a.fingerprint.cmp(&b.fingerprint))
78 });
79 Self {
80 version: FORMAT_VERSION,
81 entries,
82 }
83 }
84
85 pub fn load(path: &Path) -> Result<Self> {
94 let content = fs::read_to_string(path).map_err(|e| Error::Baseline {
95 path: path.to_path_buf(),
96 problem: if e.kind() == ErrorKind::NotFound {
97 String::from("no such file; record one with `cargo dry4rust baseline`")
98 } else {
99 e.to_string()
100 },
101 })?;
102 let file: Self = from_str(&content).map_err(|e| Error::Baseline {
103 path: path.to_path_buf(),
104 problem: e.to_string(),
105 })?;
106 if file.version == FORMAT_VERSION {
107 Ok(file)
108 } else {
109 Err(Error::Baseline {
110 path: path.to_path_buf(),
111 problem: format!(
112 "written in format {}, this build reads format {FORMAT_VERSION}; \
113 re-record it with `cargo dry4rust baseline`",
114 file.version
115 ),
116 })
117 }
118 }
119
120 pub fn save(&self, path: &Path) -> Result<()> {
126 let problem = |e: IoError| Error::Baseline {
127 path: path.to_path_buf(),
128 problem: e.to_string(),
129 };
130 if let Some(parent) = path.parent()
131 && !parent.as_os_str().is_empty()
132 {
133 fs::create_dir_all(parent).map_err(problem)?;
134 }
135 let json = to_string_pretty(self).map_err(|e| Error::Baseline {
136 path: path.to_path_buf(),
137 problem: e.to_string(),
138 })?;
139 fs::write(path, format!("{json}\n")).map_err(problem)
140 }
141
142 #[must_use]
144 pub const fn len(&self) -> usize {
145 self.entries.len()
146 }
147
148 #[must_use]
150 pub const fn is_empty(&self) -> bool {
151 self.entries.is_empty()
152 }
153
154 #[must_use]
156 pub fn admits(&self, kind: BaselineKind, group: &DuplicateGroup) -> bool {
157 self.entries.iter().any(|entry| entry.admits(kind, group))
158 }
159}