run_clang_tidy/
lib.rs

1use std::{fs, path};
2
3#[allow(unused_imports)]
4use color_eyre::{eyre::eyre, eyre::WrapErr, Help};
5use rayon::iter::{IntoParallelIterator, ParallelIterator};
6use serde::Deserialize;
7
8pub mod cli;
9pub mod cmd;
10
11mod globs;
12mod resolve;
13
14#[derive(Deserialize, Debug)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct JsonModel {
17    pub paths: Vec<String>,
18    pub filter_post: Option<Vec<String>>,
19    pub style: Option<path::PathBuf>,
20}
21
22enum Dump {
23    Error { msg: String, path: path::PathBuf },
24    Warning { msg: String, path: path::PathBuf },
25}
26
27fn log_pretty() -> bool {
28    // fancy logging using indicatif is only done for log level "info". when debugging we
29    // do not use a progress bar, if info is not enabled at all ("quiet") then the progress
30    // is also not shown
31    !log::log_enabled!(log::Level::Debug) && log::log_enabled!(log::Level::Info)
32}
33
34struct LogStep(u8);
35
36impl LogStep {
37    fn new() -> LogStep {
38        LogStep(1)
39    }
40
41    fn next(&mut self) -> String {
42        // TODO: the actual number of steps could be determined by a macro?
43        let str = format!(
44            "{}",
45            console::style(format!("[ {:1}/6 ]", self.0)).bold().dim()
46        );
47        self.0 += 1;
48        if log_pretty() {
49            str
50        } else {
51            "".to_string()
52        }
53    }
54}
55
56fn get_command(data: &cli::Data) -> eyre::Result<cmd::Runner> {
57    let cmd_path = resolve::command(data)?;
58    let mut cmd = cmd::Runner::new(&cmd_path);
59
60    cmd.validate()
61        .wrap_err(format!(
62            "Failed to execute the specified command '{}'",
63            cmd_path.display()
64        ))
65        .suggestion(format!(
66            "Please make sure that the command '{}' exists or is in your search path",
67            cmd_path.to_string_lossy()
68        ))?;
69
70    Ok(cmd)
71}
72
73fn place_tidy_file(
74    file_and_root: Option<(path::PathBuf, path::PathBuf)>,
75    step: &mut LogStep,
76) -> eyre::Result<Option<path::PathBuf>> {
77    if file_and_root.is_none() {
78        // in case no tidy file has been specified there's nothing to do
79        return Ok(None);
80    }
81
82    // the tidy file `src` should be copied to the destination directory `dst`
83    let (src_file, dst_root) = file_and_root.unwrap();
84    let mut dst_file = path::PathBuf::from(dst_root.as_path());
85    // by adding the filename of the tidy file we get the final name of the destination file
86    dst_file.push(".clang-tidy");
87
88    // it may happen that there is already a .clang-tidy file at the destination folder, e.g.,
89    // because the user placed it there while working with an editor supporting `clang-tidy`.
90    // in such a case we provide feedback by comparing the file contents and abort with an error
91    // if they do not match.
92    if dst_file.exists() {
93        let src_name = src_file.display();
94        let dst_name = dst_file.display();
95
96        log::warn!("Encountered existing tidy file {}", dst_name);
97
98        let content_src =
99            fs::read_to_string(&src_file).wrap_err(format!("Failed to read '{dst_name}'"))?;
100        let content_dst = fs::read_to_string(dst_file.as_path())
101            .wrap_err(format!("Failed to read '{dst_name}'"))
102            .wrap_err("Error while trying to compare existing tidy file")
103            .suggestion(format!(
104                "Please delete or fix the existing tidy file {dst_name}"
105            ))?;
106
107        if content_src == content_dst {
108            log::info!(
109                "{} Existing tidy file matches {}, skipping placement",
110                step.next(),
111                src_name
112            );
113            return Ok(None);
114        }
115
116        return Err(eyre::eyre!(
117            "Existing tidy file {} does not match provided tidy file {}",
118            dst_name,
119            src_name
120        )
121        .suggestion(format!(
122            "Please either delete the file {dst_name} or align the contents with {src_name}"
123        )));
124    }
125
126    log::info!(
127        "{} Copying tidy file to {}",
128        step.next(),
129        console::style(dst_file.to_string_lossy()).bold(),
130    );
131
132    // no file found at destination, copy the provided tidy file
133    let _ = fs::copy(&src_file, &dst_file)
134        .wrap_err(format!(
135            "Failed to copy tidy file to {}",
136            dst_root.to_string_lossy(),
137        ))
138        .suggestion(format!(
139            "Please check the permissions for the folder {}",
140            dst_root.to_string_lossy()
141        ))?;
142
143    Ok(Some(dst_file))
144}
145
146fn setup_jobs(jobs: Option<u8>) -> eyre::Result<()> {
147    // configure rayon to use the specified number of threads (globally)
148    if let Some(jobs) = jobs {
149        let jobs = if jobs == 0 { 1u8 } else { jobs };
150        let pool = rayon::ThreadPoolBuilder::new()
151            .num_threads(jobs.into())
152            .build_global();
153
154        if let Err(err) = pool {
155            return Err(err)
156                .wrap_err(format!("Failed to create thread pool of size {jobs}"))
157                .suggestion("Please try to decrease the number of jobs");
158        }
159    };
160    Ok(())
161}
162
163pub fn run(data: cli::Data) -> eyre::Result<()> {
164    let start = std::time::Instant::now();
165
166    log::info!(" ");
167    let mut step = LogStep::new();
168
169    let tidy_and_root = resolve::tidy_and_root(&data)?;
170    if let Some((tidy_file, _)) = &tidy_and_root {
171        log::info!(
172            "{} Found tidy file {}",
173            step.next(),
174            console::style(tidy_file.to_string_lossy()).bold(),
175        );
176    } else {
177        // no tidy file specified, it'll be picked by `clang-tidy` itself as the first `.clang-tidy`
178        // file that is encountered when walking all parent paths recursively.
179        log::info!(
180            "{} No tidy file specified, assuming .clang-tidy exists in the project tree",
181            step.next()
182        );
183    }
184
185    let build_root = resolve::build_root(&data)?;
186    log::info!(
187        "{} Using build root {}",
188        step.next(),
189        console::style(build_root.to_string_lossy()).bold(),
190    );
191
192    let candidates =
193        globs::build_matchers_from(&data.json.paths, &data.json.root, "paths", &data.json.name)?;
194    let filter_pre =
195        globs::build_glob_set_from(&data.json.filter_pre, "preFilter", &data.json.name)?;
196    let filter_post =
197        globs::build_glob_set_from(&data.json.filter_post, "postFilter", &data.json.name)?;
198
199    let (paths, filtered) = globs::match_paths(candidates, filter_pre, filter_post);
200    let paths = paths.into_iter().map(|p| p.canonicalize().unwrap());
201
202    let filtered = if filtered.is_empty() {
203        "".to_string()
204    } else {
205        format!(" (filtered {} paths)", filtered.len())
206    };
207
208    log::info!(
209        "{} Found {} files for the provided path patterns{}",
210        step.next(),
211        console::style(paths.len()).bold(),
212        filtered
213    );
214
215    let cmd = get_command(&data)?;
216    let cmd_path = match cmd.get_path().canonicalize() {
217        Ok(path) => path,
218        Err(_) => cmd.get_path(),
219    };
220    log::info!(
221        "{} Found clang-tidy version {} using command {}",
222        step.next(),
223        console::style(cmd.get_version().unwrap()).bold(),
224        console::style(cmd_path.to_string_lossy()).bold(),
225    );
226
227    let strip_root = if let Some((_, tidy_root)) = &tidy_and_root {
228        Some(path::PathBuf::from(tidy_root.as_path()))
229    } else {
230        None
231    };
232
233    let tidy = place_tidy_file(tidy_and_root, &mut step)?;
234    // binding for scope guard is not used, but an action needed when the variable goes out of scope
235    let _tidy = scopeguard::guard(tidy, |path| {
236        // ensure we delete the temporary tidy file at return or panic
237        if let Some(path) = path {
238            let str = format!("Cleaning up temporary file {}\n", path.to_string_lossy());
239            let str = console::style(str).dim().italic();
240
241            log::info!("\n{}", str);
242            let _ = fs::remove_file(path);
243        }
244    });
245
246    setup_jobs(data.jobs)?;
247    log::info!("{} Executing clang-tidy ...\n", step.next(),);
248
249    let pb = indicatif::ProgressBar::new(paths.len() as u64);
250    pb.set_style(
251        indicatif::ProgressStyle::with_template(if console::Term::stdout().size().1 > 80 {
252            "{prefix:>12.cyan.bold} [{bar:26}] {pos}/{len} {wide_msg}"
253        } else {
254            "{prefix:>12.cyan.bold} [{bar:26}] {pos}/{len}"
255        })
256        .unwrap()
257        .progress_chars("=> "),
258    );
259
260    if log_pretty() {
261        pb.set_prefix("Running");
262    }
263    let paths: Vec<_> = paths.collect();
264
265    let (failures, warnings) = {
266        let dump: Vec<_> = paths
267            .into_par_iter()
268            .map(|path| {
269                let result = cmd.run_tidy(&path, &build_root, data.fix, data.ignore_warn);
270                let strip_path = match &strip_root {
271                    None => path.clone(),
272                    Some(strip) => {
273                        if let Ok(path) = path.strip_prefix(strip) {
274                            path.to_path_buf()
275                        } else {
276                            path.clone()
277                        }
278                    }
279                };
280
281                // step log output
282                let (prefix, style) = match result {
283                    cmd::RunResult::Ok => ("Ok", console::Style::new().green().bold()),
284                    cmd::RunResult::Err(_) => ("Error", console::Style::new().red().bold()),
285                    cmd::RunResult::Warn(_) => {
286                        ("Warning", console::Style::new().color256(58).bold())
287                    }
288                };
289                log_step(prefix, path.as_path(), &strip_root, &pb, style);
290
291                // collection
292                match result {
293                    cmd::RunResult::Ok => None,
294                    cmd::RunResult::Err(msg) => {
295                        if !log_pretty() && !data.quiet {
296                            log::error!("{}", msg);
297                        }
298                        Some(Dump::Error {
299                            msg,
300                            path: strip_path,
301                        })
302                    }
303                    cmd::RunResult::Warn(msg) => {
304                        if !log_pretty() {
305                            log::warn!("{}", msg);
306                        }
307                        Some(Dump::Warning {
308                            msg,
309                            path: strip_path,
310                        })
311                    }
312                }
313            })
314            .flatten()
315            .collect();
316
317        let mut failures = Vec::with_capacity(dump.len());
318        let mut warnings: Vec<_> = vec![];
319
320        dump.into_iter().for_each(|item| {
321            match item {
322                Dump::Error { msg, path } => failures.push((path, msg)),
323                Dump::Warning { msg, path } => warnings.push((path, msg)),
324            };
325        });
326        (failures, warnings)
327    };
328
329    let duration = start.elapsed();
330    if log_pretty() {
331        pb.finish();
332
333        println!(
334            "{:>12} in {}",
335            console::Style::new().green().bold().apply_to("Finished"),
336            indicatif::HumanDuration(duration)
337        );
338    } else {
339        log::info!("{} Finished in {:#?}", step.next(), duration);
340    }
341
342    fn collect_dump(items: Vec<(path::PathBuf, String)>, style: console::Style) -> String {
343        items
344            .into_iter()
345            .map(|result| {
346                format!(
347                    "{}\n{}",
348                    style.apply_to(result.0.to_string_lossy()),
349                    result.1,
350                )
351            })
352            .collect::<Vec<_>>()
353            .join("\n")
354    }
355
356    if !warnings.is_empty() {
357        log::warn!(
358            "\n\nWarnings have been issued for the following files:\n\n{} ",
359            collect_dump(
360                warnings,
361                console::Style::new().white().bold().on_color256(58)
362            )
363            .trim_end()
364        );
365    }
366
367    if !failures.is_empty() {
368        Err(eyre::eyre!(format!(
369            "Execution failed for the following files:\n{}\n ",
370            collect_dump(failures, console::Style::new().white().bold().on_red()).trim_end()
371        )))
372    } else {
373        Ok(())
374    }
375}
376
377fn log_step(
378    prefix: &str,
379    path: &path::Path,
380    strip_path: &Option<path::PathBuf>,
381    progress: &indicatif::ProgressBar,
382    style: console::Style,
383) {
384    // let style = console::Style::new().green().bold();
385    let print_path = match strip_path {
386        None => path,
387        Some(strip) => {
388            if let Ok(path) = path.strip_prefix(strip) {
389                path
390            } else {
391                path
392            }
393        }
394    };
395
396    if log_pretty() {
397        progress.println(format!(
398            "{:>12} {}",
399            style.apply_to(prefix),
400            print_path.to_string_lossy(),
401        ));
402        progress.inc(1);
403    } else {
404        log::info!("  + {}", path.to_string_lossy());
405    }
406}