dry4rust/suppression/
ignore_file.rs1use std::collections::HashSet;
7use std::fs;
8use std::io;
9use std::io::Error as IoError;
10use std::io::ErrorKind;
11use std::path::{Path, PathBuf};
12
13use serde::{Deserialize, Serialize};
14use toml::from_str;
15use toml::to_string_pretty;
16
17use crate::fingerprint::Fingerprint;
18use crate::grouper::DuplicateGroup;
19use crate::suppression::ignore_entry::IgnoreEntry;
20
21const IGNORE_FILE_NAME: &str = ".dry4rust-ignore.toml";
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct IgnoreFile {
30 #[serde(default)]
31 pub ignore: Vec<IgnoreEntry>,
32}
33
34impl IgnoreFile {
35 #[must_use]
37 pub fn path_in(root: &Path) -> PathBuf {
38 root.join(IGNORE_FILE_NAME)
39 }
40
41 #[must_use]
47 pub fn load(root: &Path) -> Self {
48 let path = Self::path_in(root);
49 if !path.exists() {
50 return Self::default();
51 }
52 fs::read_to_string(&path).map_or_else(
53 |_| Self::default(),
54 |content| from_str(&content).unwrap_or_default(),
55 )
56 }
57
58 pub fn save(&self, root: &Path) -> io::Result<()> {
71 if self.ignore.is_empty() {
72 return Self::remove_from(root);
73 }
74 let content = to_string_pretty(self)
75 .map_err(|e| IoError::other(format!("Failed to serialize ignore file: {e}")))?;
76 fs::write(Self::path_in(root), content)
77 }
78
79 pub fn remove_from(root: &Path) -> io::Result<()> {
85 match fs::remove_file(Self::path_in(root)) {
86 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
87 outcome => outcome,
88 }
89 }
90
91 #[must_use]
94 pub fn with_ignored(
95 self,
96 fingerprint: &Fingerprint,
97 reason: Option<String>,
98 members: Vec<String>,
99 ) -> Self {
100 let hex = fingerprint.to_hex();
101 if self.ignore.iter().any(|entry| entry.fingerprint == hex) {
102 return self;
103 }
104 let mut file = self;
105 file.ignore.push(IgnoreEntry {
106 fingerprint: hex,
107 reason,
108 members,
109 });
110 file
111 }
112
113 #[must_use]
115 pub fn without(self, fingerprint: &str) -> (Self, bool) {
116 let mut file = self;
117 let before = file.ignore.len();
118 file.ignore.retain(|entry| entry.fingerprint != fingerprint);
119 let removed = file.ignore.len() < before;
120 (file, removed)
121 }
122
123 #[must_use]
125 pub fn contains(&self, fingerprint: &Fingerprint) -> bool {
126 let hex = fingerprint.to_hex();
127 self.ignore.iter().any(|entry| entry.fingerprint == hex)
128 }
129
130 #[must_use]
132 pub fn retain_unsuppressed(&self, groups: Vec<DuplicateGroup>) -> Vec<DuplicateGroup> {
133 groups
134 .into_iter()
135 .filter(|group| !self.contains(&group.fingerprint))
136 .collect()
137 }
138
139 #[must_use]
144 pub fn stale(&self, live: &HashSet<Fingerprint>) -> Vec<&IgnoreEntry> {
145 self.ignore
146 .iter()
147 .filter(|entry| !Self::is_live(entry, live))
148 .collect()
149 }
150
151 #[must_use]
153 pub fn without_stale(self, live: &HashSet<Fingerprint>) -> (Self, Vec<IgnoreEntry>) {
154 let (kept, stale) = self
155 .ignore
156 .into_iter()
157 .partition(|entry| Self::is_live(entry, live));
158 (Self { ignore: kept }, stale)
159 }
160
161 fn is_live(entry: &IgnoreEntry, live: &HashSet<Fingerprint>) -> bool {
162 Fingerprint::from_hex(&entry.fingerprint).is_some_and(|fp| live.contains(&fp))
163 }
164}