Skip to main content

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