tidy/
diagnostics.rs

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)]
9///CLI flags used by tidy.
10pub struct TidyFlags {
11    ///Applies style and formatting changes during a tidy run.
12    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/// Collects diagnostics from all tidy steps, and contains shared information
30/// that determines how should message and logs be presented.
31///
32/// Since checks are executed in parallel, the context is internally synchronized, to avoid
33/// all checks to lock it explicitly.
34#[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        // Shorten path for shorter diagnostics
63        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/// Identifies a single step
126#[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
170/// Represents a single tidy check, identified by its `name`, running.
171pub 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    /// Creates a new instance of a running check without going through the diag
181    /// context.
182    /// Useful if you want to run some functions from tidy without configuring
183    /// diagnostics.
184    pub fn new_noop() -> Self {
185        let ctx = TidyCtx::new(Path::new(""), false, TidyFlags::default());
186        ctx.start_check("noop")
187    }
188
189    /// Immediately output an error and mark the check as failed.
190    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    /// Immediately output a warning.
199    pub fn warning<T: Display>(&mut self, msg: T) {
200        output_message(&msg.to_string(), Some(&self.id), Some(COLOR_WARNING));
201    }
202
203    /// Output an informational message
204    pub fn message<T: Display>(&mut self, msg: T) {
205        output_message(&msg.to_string(), Some(&self.id), None);
206    }
207
208    /// Output a message only if verbose output is enabled.
209    pub fn verbose_msg<T: Display>(&mut self, msg: T) {
210        if self.is_verbose_enabled() {
211            self.message(msg);
212        }
213    }
214
215    /// Has an error already occured for this check?
216    pub fn is_bad(&self) -> bool {
217        self.bad
218    }
219
220    /// Is verbose output enabled?
221    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
245/// Output a message to stderr.
246/// The message can be optionally scoped to a certain check, and it can also have a certain color.
247pub 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}