1use std::collections::HashSet;
2use std::fmt::{Display, Formatter};
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5
6use termcolor::{Color, WriteColor};
7
8#[derive(Clone, Default)]
9pub struct TidyFlags {
11 bless: bool,
13}
14
15impl TidyFlags {
16 pub fn new(cfg_args: &[String]) -> Self {
17 let mut flags = Self::default();
18
19 for arg in cfg_args {
20 match arg.as_str() {
21 "--bless" => flags.bless = true,
22 _ => continue,
23 }
24 }
25 flags
26 }
27}
28
29#[derive(Clone)]
35pub struct TidyCtx {
36 tidy_flags: TidyFlags,
37 diag_ctx: Arc<Mutex<DiagCtxInner>>,
38}
39
40impl TidyCtx {
41 pub fn new(root_path: &Path, verbose: bool, tidy_flags: TidyFlags) -> Self {
42 Self {
43 diag_ctx: Arc::new(Mutex::new(DiagCtxInner {
44 running_checks: Default::default(),
45 finished_checks: Default::default(),
46 root_path: root_path.to_path_buf(),
47 verbose,
48 })),
49 tidy_flags,
50 }
51 }
52
53 pub fn is_bless_enabled(&self) -> bool {
54 self.tidy_flags.bless
55 }
56
57 pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck {
58 let mut id = id.into();
59
60 let mut ctx = self.diag_ctx.lock().unwrap();
61
62 id.path = match id.path {
64 Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()),
65 None => None,
66 };
67
68 ctx.start_check(id.clone());
69 RunningCheck {
70 id,
71 bad: false,
72 ctx: self.diag_ctx.clone(),
73 #[cfg(test)]
74 errors: vec![],
75 }
76 }
77
78 pub fn into_failed_checks(self) -> Vec<FinishedCheck> {
79 let ctx = Arc::into_inner(self.diag_ctx).unwrap().into_inner().unwrap();
80 assert!(ctx.running_checks.is_empty(), "Some checks are still running");
81 ctx.finished_checks.into_iter().filter(|c| c.bad).collect()
82 }
83}
84
85struct DiagCtxInner {
86 running_checks: HashSet<CheckId>,
87 finished_checks: HashSet<FinishedCheck>,
88 verbose: bool,
89 root_path: PathBuf,
90}
91
92impl DiagCtxInner {
93 fn start_check(&mut self, id: CheckId) {
94 if self.has_check_id(&id) {
95 panic!("Starting a check named `{id:?}` for the second time");
96 }
97
98 self.running_checks.insert(id);
99 }
100
101 fn finish_check(&mut self, check: FinishedCheck) {
102 assert!(
103 self.running_checks.remove(&check.id),
104 "Finishing check `{:?}` that was not started",
105 check.id
106 );
107
108 if check.bad {
109 output_message("FAIL", Some(&check.id), Some(COLOR_ERROR));
110 } else if self.verbose {
111 output_message("OK", Some(&check.id), Some(COLOR_SUCCESS));
112 }
113
114 self.finished_checks.insert(check);
115 }
116
117 fn has_check_id(&self, id: &CheckId) -> bool {
118 self.running_checks
119 .iter()
120 .chain(self.finished_checks.iter().map(|c| &c.id))
121 .any(|c| c == id)
122 }
123}
124
125#[derive(PartialEq, Eq, Hash, Clone, Debug)]
127pub struct CheckId {
128 pub name: String,
129 pub path: Option<PathBuf>,
130}
131
132impl CheckId {
133 pub fn new(name: &'static str) -> Self {
134 Self { name: name.to_string(), path: None }
135 }
136
137 pub fn path(self, path: &Path) -> Self {
138 Self { path: Some(path.to_path_buf()), ..self }
139 }
140}
141
142impl From<&'static str> for CheckId {
143 fn from(name: &'static str) -> Self {
144 Self::new(name)
145 }
146}
147
148impl Display for CheckId {
149 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150 write!(f, "{}", self.name)?;
151 if let Some(path) = &self.path {
152 write!(f, " ({})", path.display())?;
153 }
154 Ok(())
155 }
156}
157
158#[derive(PartialEq, Eq, Hash, Debug)]
159pub struct FinishedCheck {
160 id: CheckId,
161 bad: bool,
162}
163
164impl FinishedCheck {
165 pub fn id(&self) -> &CheckId {
166 &self.id
167 }
168}
169
170pub struct RunningCheck {
172 id: CheckId,
173 bad: bool,
174 ctx: Arc<Mutex<DiagCtxInner>>,
175 #[cfg(test)]
176 errors: Vec<String>,
177}
178
179impl RunningCheck {
180 pub fn new_noop() -> Self {
185 let ctx = TidyCtx::new(Path::new(""), false, TidyFlags::default());
186 ctx.start_check("noop")
187 }
188
189 pub fn error<T: Display>(&mut self, msg: T) {
191 self.mark_as_bad();
192 let msg = msg.to_string();
193 output_message(&msg, Some(&self.id), Some(COLOR_ERROR));
194 #[cfg(test)]
195 self.errors.push(msg);
196 }
197
198 pub fn warning<T: Display>(&mut self, msg: T) {
200 output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
201 }
202
203 pub fn message<T: Display>(&mut self, msg: T) {
205 output_message(&msg.to_string(), Some(&self.id), None);
206 }
207
208 pub fn verbose_msg<T: Display>(&mut self, msg: T) {
210 if self.is_verbose_enabled() {
211 self.message(msg);
212 }
213 }
214
215 pub fn is_bad(&self) -> bool {
217 self.bad
218 }
219
220 pub fn is_verbose_enabled(&self) -> bool {
222 self.ctx.lock().unwrap().verbose
223 }
224
225 #[cfg(test)]
226 pub fn get_errors(&self) -> Vec<String> {
227 self.errors.clone()
228 }
229
230 fn mark_as_bad(&mut self) {
231 self.bad = true;
232 }
233}
234
235impl Drop for RunningCheck {
236 fn drop(&mut self) {
237 self.ctx.lock().unwrap().finish_check(FinishedCheck { id: self.id.clone(), bad: self.bad })
238 }
239}
240
241pub const COLOR_SUCCESS: Color = Color::Green;
242pub const COLOR_ERROR: Color = Color::Red;
243pub const COLOR_WARNING: Color = Color::Yellow;
244
245pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) {
248 use std::io::Write;
249
250 use termcolor::{ColorChoice, ColorSpec, StandardStream};
251
252 let mut stderr = StandardStream::stderr(ColorChoice::Auto);
253 if let Some(color) = &color {
254 stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap();
255 }
256
257 match id {
258 Some(id) => {
259 write!(&mut stderr, "tidy [{}", id.name).unwrap();
260 if let Some(path) = &id.path {
261 write!(&mut stderr, " ({})", path.display()).unwrap();
262 }
263 write!(&mut stderr, "]").unwrap();
264 }
265 None => {
266 write!(&mut stderr, "tidy").unwrap();
267 }
268 }
269 if color.is_some() {
270 stderr.set_color(&ColorSpec::new()).unwrap();
271 }
272
273 writeln!(&mut stderr, ": {msg}").unwrap();
274}