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 !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 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 return Ok(None);
80 }
81
82 let (src_file, dst_root) = file_and_root.unwrap();
84 let mut dst_file = path::PathBuf::from(dst_root.as_path());
85 dst_file.push(".clang-tidy");
87
88 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 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 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 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 let _tidy = scopeguard::guard(tidy, |path| {
236 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 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 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 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}