tidy/
diagnostics.rs

1use std::collections::HashSet;
2use std::fmt::{Display, Formatter};
3use std::io;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7use termcolor::Color;
8
9#[derive(Clone, Default)]
10///CLI flags used by tidy.
11pub struct TidyFlags {
12    ///Applies style and formatting changes during a tidy run.
13    bless: bool,
14}
15
16impl TidyFlags {
17    pub fn new(cfg_args: &[String]) -> Self {
18        let mut flags = Self::default();
19
20        for arg in cfg_args {
21            match arg.as_str() {
22                "--bless" => flags.bless = true,
23                _ => continue,
24            }
25        }
26        flags
27    }
28}
29
30/// Collects diagnostics from all tidy steps, and contains shared information
31/// that determines how should message and logs be presented.
32///
33/// Since checks are executed in parallel, the context is internally synchronized, to avoid
34/// all checks to lock it explicitly.
35#[derive(Clone)]
36pub struct TidyCtx {
37    tidy_flags: TidyFlags,
38    diag_ctx: Arc<Mutex<DiagCtxInner>>,
39}
40
41impl TidyCtx {
42    pub fn new(root_path: &Path, verbose: bool, tidy_flags: TidyFlags) -> Self {
43        Self {
44            diag_ctx: Arc::new(Mutex::new(DiagCtxInner {
45                running_checks: Default::default(),
46                finished_checks: Default::default(),
47                root_path: root_path.to_path_buf(),
48                verbose,
49            })),
50            tidy_flags,
51        }
52    }
53
54    pub fn is_bless_enabled(&self) -> bool {
55        self.tidy_flags.bless
56    }
57
58    pub fn start_check<Id: Into<CheckId>>(&self, id: Id) -> RunningCheck {
59        let mut id = id.into();
60
61        let mut ctx = self.diag_ctx.lock().unwrap();
62
63        // Shorten path for shorter diagnostics
64        id.path = match id.path {
65            Some(path) => Some(path.strip_prefix(&ctx.root_path).unwrap_or(&path).to_path_buf()),
66            None => None,
67        };
68
69        ctx.start_check(id.clone());
70        RunningCheck {
71            id,
72            bad: false,
73            ctx: self.diag_ctx.clone(),
74            #[cfg(test)]
75            errors: vec![],
76        }
77    }
78
79    pub fn into_failed_checks(self) -> Vec<FinishedCheck> {
80        let ctx = Arc::into_inner(self.diag_ctx).unwrap().into_inner().unwrap();
81        assert!(ctx.running_checks.is_empty(), "Some checks are still running");
82        ctx.finished_checks.into_iter().filter(|c| c.bad).collect()
83    }
84}
85
86struct DiagCtxInner {
87    running_checks: HashSet<CheckId>,
88    finished_checks: HashSet<FinishedCheck>,
89    verbose: bool,
90    root_path: PathBuf,
91}
92
93impl DiagCtxInner {
94    fn start_check(&mut self, id: CheckId) {
95        if self.has_check_id(&id) {
96            panic!("Starting a check named `{id:?}` for the second time");
97        }
98
99        self.running_checks.insert(id);
100    }
101
102    fn finish_check(&mut self, check: FinishedCheck) {
103        assert!(
104            self.running_checks.remove(&check.id),
105            "Finishing check `{:?}` that was not started",
106            check.id
107        );
108
109        if check.bad {
110            output_message("FAIL", Some(&check.id), Some(COLOR_ERROR));
111        } else if self.verbose {
112            output_message("OK", Some(&check.id), Some(COLOR_SUCCESS));
113        }
114
115        self.finished_checks.insert(check);
116    }
117
118    fn has_check_id(&self, id: &CheckId) -> bool {
119        self.running_checks
120            .iter()
121            .chain(self.finished_checks.iter().map(|c| &c.id))
122            .any(|c| c == id)
123    }
124}
125
126/// Identifies a single step
127#[derive(PartialEq, Eq, Hash, Clone, Debug)]
128pub struct CheckId {
129    pub name: String,
130    pub path: Option<PathBuf>,
131}
132
133impl CheckId {
134    pub fn new(name: &'static str) -> Self {
135        Self { name: name.to_string(), path: None }
136    }
137
138    pub fn path(self, path: &Path) -> Self {
139        Self { path: Some(path.to_path_buf()), ..self }
140    }
141}
142
143impl From<&'static str> for CheckId {
144    fn from(name: &'static str) -> Self {
145        Self::new(name)
146    }
147}
148
149impl Display for CheckId {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}", self.name)?;
152        if let Some(path) = &self.path {
153            write!(f, " ({})", path.display())?;
154        }
155        Ok(())
156    }
157}
158
159#[derive(PartialEq, Eq, Hash, Debug)]
160pub struct FinishedCheck {
161    id: CheckId,
162    bad: bool,
163}
164
165impl FinishedCheck {
166    pub fn id(&self) -> &CheckId {
167        &self.id
168    }
169}
170
171/// Represents a single tidy check, identified by its `name`, running.
172pub struct RunningCheck {
173    id: CheckId,
174    bad: bool,
175    ctx: Arc<Mutex<DiagCtxInner>>,
176    #[cfg(test)]
177    errors: Vec<String>,
178}
179
180impl RunningCheck {
181    /// Creates a new instance of a running check without going through the diag
182    /// context.
183    /// Useful if you want to run some functions from tidy without configuring
184    /// diagnostics.
185    pub fn new_noop() -> Self {
186        let ctx = TidyCtx::new(Path::new(""), false, TidyFlags::default());
187        ctx.start_check("noop")
188    }
189
190    /// Immediately output an error and mark the check as failed.
191    pub fn error<T: Display>(&mut self, msg: T) {
192        self.mark_as_bad();
193        let msg = msg.to_string();
194        output_message(&msg, Some(&self.id), Some(COLOR_ERROR));
195        #[cfg(test)]
196        self.errors.push(msg);
197    }
198
199    /// Immediately output a warning.
200    pub fn warning<T: Display>(&mut self, msg: T) {
201        output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
202    }
203
204    /// Output an informational message
205    pub fn message<T: Display>(&mut self, msg: T) {
206        output_message(&msg.to_string(), Some(&self.id), None);
207    }
208
209    /// Output a message only if verbose output is enabled.
210    pub fn verbose_msg<T: Display>(&mut self, msg: T) {
211        if self.is_verbose_enabled() {
212            self.message(msg);
213        }
214    }
215
216    /// Has an error already occured for this check?
217    pub fn is_bad(&self) -> bool {
218        self.bad
219    }
220
221    /// Is verbose output enabled?
222    pub fn is_verbose_enabled(&self) -> bool {
223        self.ctx.lock().unwrap().verbose
224    }
225
226    #[cfg(test)]
227    pub fn get_errors(&self) -> Vec<String> {
228        self.errors.clone()
229    }
230
231    fn mark_as_bad(&mut self) {
232        self.bad = true;
233    }
234}
235
236impl Drop for RunningCheck {
237    fn drop(&mut self) {
238        self.ctx.lock().unwrap().finish_check(FinishedCheck { id: self.id.clone(), bad: self.bad })
239    }
240}
241
242pub const COLOR_SUCCESS: Color = Color::Green;
243pub const COLOR_ERROR: Color = Color::Red;
244pub const COLOR_WARNING: Color = Color::Yellow;
245
246/// Output a message to stderr.
247/// The message can be optionally scoped to a certain check, and it can also have a certain color.
248pub fn output_message(msg: &str, id: Option<&CheckId>, color: Option<Color>) {
249    use termcolor::{ColorChoice, ColorSpec};
250
251    let stderr: &mut dyn termcolor::WriteColor = if cfg!(test) {
252        &mut StderrForUnitTests
253    } else {
254        &mut termcolor::StandardStream::stderr(ColorChoice::Auto)
255    };
256
257    if let Some(color) = &color {
258        stderr.set_color(ColorSpec::new().set_fg(Some(*color))).unwrap();
259    }
260
261    match id {
262        Some(id) => {
263            write!(stderr, "tidy [{}", id.name).unwrap();
264            if let Some(path) = &id.path {
265                write!(stderr, " ({})", path.display()).unwrap();
266            }
267            write!(stderr, "]").unwrap();
268        }
269        None => {
270            write!(stderr, "tidy").unwrap();
271        }
272    }
273    if color.is_some() {
274        stderr.set_color(&ColorSpec::new()).unwrap();
275    }
276
277    writeln!(stderr, ": {msg}").unwrap();
278}
279
280/// An implementation of `io::Write` and `termcolor::WriteColor` that writes
281/// to stderr via `eprint!`, so that the output can be properly captured when
282/// running tidy's unit tests.
283struct StderrForUnitTests;
284
285impl io::Write for StderrForUnitTests {
286    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
287        eprint!("{}", String::from_utf8_lossy(buf));
288        Ok(buf.len())
289    }
290
291    fn flush(&mut self) -> io::Result<()> {
292        Ok(())
293    }
294}
295
296impl termcolor::WriteColor for StderrForUnitTests {
297    fn supports_color(&self) -> bool {
298        false
299    }
300
301    fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> {
302        Ok(())
303    }
304
305    fn reset(&mut self) -> io::Result<()> {
306        Ok(())
307    }
308}