]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | // Run clippy on a fixed set of crates and collect the warnings. |
2 | // This helps observing the impact clippy changes have on a set of real-world code (and not just our | |
3 | // testsuite). | |
4 | // | |
5 | // When a new lint is introduced, we can search the results for new warnings and check for false | |
6 | // positives. | |
7 | ||
8 | #![allow(clippy::filter_map, clippy::collapsible_else_if)] | |
9 | ||
10 | use std::ffi::OsStr; | |
11 | use std::process::Command; | |
12 | use std::sync::atomic::{AtomicUsize, Ordering}; | |
13 | use std::{collections::HashMap, io::ErrorKind}; | |
14 | use std::{ | |
15 | env, fmt, | |
16 | fs::write, | |
17 | path::{Path, PathBuf}, | |
18 | }; | |
19 | ||
20 | use clap::{App, Arg, ArgMatches}; | |
21 | use rayon::prelude::*; | |
22 | use serde::{Deserialize, Serialize}; | |
23 | use serde_json::Value; | |
24 | ||
25 | const CLIPPY_DRIVER_PATH: &str = "target/debug/clippy-driver"; | |
26 | const CARGO_CLIPPY_PATH: &str = "target/debug/cargo-clippy"; | |
27 | ||
28 | const LINTCHECK_DOWNLOADS: &str = "target/lintcheck/downloads"; | |
29 | const LINTCHECK_SOURCES: &str = "target/lintcheck/sources"; | |
30 | ||
31 | /// List of sources to check, loaded from a .toml file | |
32 | #[derive(Debug, Serialize, Deserialize)] | |
33 | struct SourceList { | |
34 | crates: HashMap<String, TomlCrate>, | |
35 | } | |
36 | ||
37 | /// A crate source stored inside the .toml | |
38 | /// will be translated into on one of the `CrateSource` variants | |
39 | #[derive(Debug, Serialize, Deserialize)] | |
40 | struct TomlCrate { | |
41 | name: String, | |
42 | versions: Option<Vec<String>>, | |
43 | git_url: Option<String>, | |
44 | git_hash: Option<String>, | |
45 | path: Option<String>, | |
46 | options: Option<Vec<String>>, | |
47 | } | |
48 | ||
49 | /// Represents an archive we download from crates.io, or a git repo, or a local repo/folder | |
50 | /// Once processed (downloaded/extracted/cloned/copied...), this will be translated into a `Crate` | |
51 | #[derive(Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Ord, PartialOrd)] | |
52 | enum CrateSource { | |
53 | CratesIo { | |
54 | name: String, | |
55 | version: String, | |
56 | options: Option<Vec<String>>, | |
57 | }, | |
58 | Git { | |
59 | name: String, | |
60 | url: String, | |
61 | commit: String, | |
62 | options: Option<Vec<String>>, | |
63 | }, | |
64 | Path { | |
65 | name: String, | |
66 | path: PathBuf, | |
67 | options: Option<Vec<String>>, | |
68 | }, | |
69 | } | |
70 | ||
71 | /// Represents the actual source code of a crate that we ran "cargo clippy" on | |
72 | #[derive(Debug)] | |
73 | struct Crate { | |
74 | version: String, | |
75 | name: String, | |
76 | // path to the extracted sources that clippy can check | |
77 | path: PathBuf, | |
78 | options: Option<Vec<String>>, | |
79 | } | |
80 | ||
81 | /// A single warning that clippy issued while checking a `Crate` | |
82 | #[derive(Debug)] | |
83 | struct ClippyWarning { | |
84 | crate_name: String, | |
85 | crate_version: String, | |
86 | file: String, | |
87 | line: String, | |
88 | column: String, | |
89 | linttype: String, | |
90 | message: String, | |
91 | is_ice: bool, | |
92 | } | |
93 | ||
94 | impl std::fmt::Display for ClippyWarning { | |
95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
96 | writeln!( | |
97 | f, | |
98 | r#"target/lintcheck/sources/{}-{}/{}:{}:{} {} "{}""#, | |
99 | &self.crate_name, &self.crate_version, &self.file, &self.line, &self.column, &self.linttype, &self.message | |
100 | ) | |
101 | } | |
102 | } | |
103 | ||
104 | impl CrateSource { | |
105 | /// Makes the sources available on the disk for clippy to check. | |
106 | /// Clones a git repo and checks out the specified commit or downloads a crate from crates.io or | |
107 | /// copies a local folder | |
108 | fn download_and_extract(&self) -> Crate { | |
109 | match self { | |
110 | CrateSource::CratesIo { name, version, options } => { | |
111 | let extract_dir = PathBuf::from(LINTCHECK_SOURCES); | |
112 | let krate_download_dir = PathBuf::from(LINTCHECK_DOWNLOADS); | |
113 | ||
114 | // url to download the crate from crates.io | |
115 | let url = format!("https://crates.io/api/v1/crates/{}/{}/download", name, version); | |
116 | println!("Downloading and extracting {} {} from {}", name, version, url); | |
117 | create_dirs(&krate_download_dir, &extract_dir); | |
118 | ||
119 | let krate_file_path = krate_download_dir.join(format!("{}-{}.crate.tar.gz", name, version)); | |
120 | // don't download/extract if we already have done so | |
121 | if !krate_file_path.is_file() { | |
122 | // create a file path to download and write the crate data into | |
123 | let mut krate_dest = std::fs::File::create(&krate_file_path).unwrap(); | |
124 | let mut krate_req = ureq::get(&url).call().unwrap().into_reader(); | |
125 | // copy the crate into the file | |
126 | std::io::copy(&mut krate_req, &mut krate_dest).unwrap(); | |
127 | ||
128 | // unzip the tarball | |
129 | let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap()); | |
130 | // extract the tar archive | |
131 | let mut archive = tar::Archive::new(ungz_tar); | |
132 | archive.unpack(&extract_dir).expect("Failed to extract!"); | |
133 | } | |
134 | // crate is extracted, return a new Krate object which contains the path to the extracted | |
135 | // sources that clippy can check | |
136 | Crate { | |
137 | version: version.clone(), | |
138 | name: name.clone(), | |
139 | path: extract_dir.join(format!("{}-{}/", name, version)), | |
140 | options: options.clone(), | |
141 | } | |
142 | }, | |
143 | CrateSource::Git { | |
144 | name, | |
145 | url, | |
146 | commit, | |
147 | options, | |
148 | } => { | |
149 | let repo_path = { | |
150 | let mut repo_path = PathBuf::from(LINTCHECK_SOURCES); | |
151 | // add a -git suffix in case we have the same crate from crates.io and a git repo | |
152 | repo_path.push(format!("{}-git", name)); | |
153 | repo_path | |
154 | }; | |
155 | // clone the repo if we have not done so | |
156 | if !repo_path.is_dir() { | |
157 | println!("Cloning {} and checking out {}", url, commit); | |
158 | if !Command::new("git") | |
159 | .arg("clone") | |
160 | .arg(url) | |
161 | .arg(&repo_path) | |
162 | .status() | |
163 | .expect("Failed to clone git repo!") | |
164 | .success() | |
165 | { | |
166 | eprintln!("Failed to clone {} into {}", url, repo_path.display()) | |
167 | } | |
168 | } | |
169 | // check out the commit/branch/whatever | |
170 | if !Command::new("git") | |
171 | .arg("checkout") | |
172 | .arg(commit) | |
173 | .current_dir(&repo_path) | |
174 | .status() | |
175 | .expect("Failed to check out commit") | |
176 | .success() | |
177 | { | |
178 | eprintln!("Failed to checkout {} of repo at {}", commit, repo_path.display()) | |
179 | } | |
180 | ||
181 | Crate { | |
182 | version: commit.clone(), | |
183 | name: name.clone(), | |
184 | path: repo_path, | |
185 | options: options.clone(), | |
186 | } | |
187 | }, | |
188 | CrateSource::Path { name, path, options } => { | |
189 | use fs_extra::dir; | |
190 | ||
191 | // simply copy the entire directory into our target dir | |
192 | let copy_dest = PathBuf::from(format!("{}/", LINTCHECK_SOURCES)); | |
193 | ||
194 | // the source path of the crate we copied, ${copy_dest}/crate_name | |
195 | let crate_root = copy_dest.join(name); // .../crates/local_crate | |
196 | ||
197 | if crate_root.exists() { | |
198 | println!( | |
199 | "Not copying {} to {}, destination already exists", | |
200 | path.display(), | |
201 | crate_root.display() | |
202 | ); | |
203 | } else { | |
204 | println!("Copying {} to {}", path.display(), copy_dest.display()); | |
205 | ||
206 | dir::copy(path, ©_dest, &dir::CopyOptions::new()).unwrap_or_else(|_| { | |
207 | panic!("Failed to copy from {}, to {}", path.display(), crate_root.display()) | |
208 | }); | |
209 | } | |
210 | ||
211 | Crate { | |
212 | version: String::from("local"), | |
213 | name: name.clone(), | |
214 | path: crate_root, | |
215 | options: options.clone(), | |
216 | } | |
217 | }, | |
218 | } | |
219 | } | |
220 | } | |
221 | ||
222 | impl Crate { | |
223 | /// Run `cargo clippy` on the `Crate` and collect and return all the lint warnings that clippy | |
224 | /// issued | |
225 | fn run_clippy_lints( | |
226 | &self, | |
227 | cargo_clippy_path: &Path, | |
228 | target_dir_index: &AtomicUsize, | |
229 | thread_limit: usize, | |
230 | total_crates_to_lint: usize, | |
231 | fix: bool, | |
232 | ) -> Vec<ClippyWarning> { | |
233 | // advance the atomic index by one | |
234 | let index = target_dir_index.fetch_add(1, Ordering::SeqCst); | |
235 | // "loop" the index within 0..thread_limit | |
236 | let thread_index = index % thread_limit; | |
237 | let perc = (index * 100) / total_crates_to_lint; | |
238 | ||
239 | if thread_limit == 1 { | |
240 | println!( | |
241 | "{}/{} {}% Linting {} {}", | |
242 | index, total_crates_to_lint, perc, &self.name, &self.version | |
243 | ); | |
244 | } else { | |
245 | println!( | |
246 | "{}/{} {}% Linting {} {} in target dir {:?}", | |
247 | index, total_crates_to_lint, perc, &self.name, &self.version, thread_index | |
248 | ); | |
249 | } | |
250 | ||
251 | let cargo_clippy_path = std::fs::canonicalize(cargo_clippy_path).unwrap(); | |
252 | ||
253 | let shared_target_dir = clippy_project_root().join("target/lintcheck/shared_target_dir"); | |
254 | ||
255 | let mut args = if fix { | |
256 | vec![ | |
257 | "-Zunstable-options", | |
258 | "--fix", | |
259 | "-Zunstable-options", | |
260 | "--allow-no-vcs", | |
261 | "--", | |
262 | "--cap-lints=warn", | |
263 | ] | |
264 | } else { | |
265 | vec!["--", "--message-format=json", "--", "--cap-lints=warn"] | |
266 | }; | |
267 | ||
268 | if let Some(options) = &self.options { | |
269 | for opt in options { | |
270 | args.push(opt); | |
271 | } | |
272 | } else { | |
273 | args.extend(&["-Wclippy::pedantic", "-Wclippy::cargo"]) | |
274 | } | |
275 | ||
276 | let all_output = std::process::Command::new(&cargo_clippy_path) | |
277 | // use the looping index to create individual target dirs | |
278 | .env( | |
279 | "CARGO_TARGET_DIR", | |
280 | shared_target_dir.join(format!("_{:?}", thread_index)), | |
281 | ) | |
282 | // lint warnings will look like this: | |
283 | // src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter` | |
284 | .args(&args) | |
285 | .current_dir(&self.path) | |
286 | .output() | |
287 | .unwrap_or_else(|error| { | |
288 | panic!( | |
289 | "Encountered error:\n{:?}\ncargo_clippy_path: {}\ncrate path:{}\n", | |
290 | error, | |
291 | &cargo_clippy_path.display(), | |
292 | &self.path.display() | |
293 | ); | |
294 | }); | |
295 | let stdout = String::from_utf8_lossy(&all_output.stdout); | |
296 | let stderr = String::from_utf8_lossy(&all_output.stderr); | |
297 | ||
298 | if fix { | |
299 | if let Some(stderr) = stderr | |
300 | .lines() | |
301 | .find(|line| line.contains("failed to automatically apply fixes suggested by rustc to crate")) | |
302 | { | |
303 | let subcrate = &stderr[63..]; | |
304 | println!( | |
305 | "ERROR: failed to apply some suggetion to {} / to (sub)crate {}", | |
306 | self.name, subcrate | |
307 | ); | |
308 | } | |
309 | // fast path, we don't need the warnings anyway | |
310 | return Vec::new(); | |
311 | } | |
312 | ||
313 | let output_lines = stdout.lines(); | |
314 | let warnings: Vec<ClippyWarning> = output_lines | |
315 | .into_iter() | |
316 | // get all clippy warnings and ICEs | |
317 | .filter(|line| filter_clippy_warnings(&line)) | |
318 | .map(|json_msg| parse_json_message(json_msg, &self)) | |
319 | .collect(); | |
320 | ||
321 | warnings | |
322 | } | |
323 | } | |
324 | ||
325 | #[derive(Debug)] | |
326 | struct LintcheckConfig { | |
327 | // max number of jobs to spawn (default 1) | |
328 | max_jobs: usize, | |
329 | // we read the sources to check from here | |
330 | sources_toml_path: PathBuf, | |
331 | // we save the clippy lint results here | |
332 | lintcheck_results_path: PathBuf, | |
333 | // whether to just run --fix and not collect all the warnings | |
334 | fix: bool, | |
335 | } | |
336 | ||
337 | impl LintcheckConfig { | |
338 | fn from_clap(clap_config: &ArgMatches) -> Self { | |
339 | // first, check if we got anything passed via the LINTCHECK_TOML env var, | |
340 | // if not, ask clap if we got any value for --crates-toml <foo> | |
341 | // if not, use the default "lintcheck/lintcheck_crates.toml" | |
342 | let sources_toml = env::var("LINTCHECK_TOML").unwrap_or_else(|_| { | |
343 | clap_config | |
344 | .value_of("crates-toml") | |
345 | .clone() | |
346 | .unwrap_or("lintcheck/lintcheck_crates.toml") | |
347 | .to_string() | |
348 | }); | |
349 | ||
350 | let sources_toml_path = PathBuf::from(sources_toml); | |
351 | ||
352 | // for the path where we save the lint results, get the filename without extension (so for | |
353 | // wasd.toml, use "wasd"...) | |
354 | let filename: PathBuf = sources_toml_path.file_stem().unwrap().into(); | |
355 | let lintcheck_results_path = PathBuf::from(format!("lintcheck-logs/{}_logs.txt", filename.display())); | |
356 | ||
357 | // look at the --threads arg, if 0 is passed, ask rayon rayon how many threads it would spawn and | |
358 | // use half of that for the physical core count | |
359 | // by default use a single thread | |
360 | let max_jobs = match clap_config.value_of("threads") { | |
361 | Some(threads) => { | |
362 | let threads: usize = threads | |
363 | .parse() | |
364 | .unwrap_or_else(|_| panic!("Failed to parse '{}' to a digit", threads)); | |
365 | if threads == 0 { | |
366 | // automatic choice | |
367 | // Rayon seems to return thread count so half that for core count | |
368 | (rayon::current_num_threads() / 2) as usize | |
369 | } else { | |
370 | threads | |
371 | } | |
372 | }, | |
373 | // no -j passed, use a single thread | |
374 | None => 1, | |
375 | }; | |
376 | let fix: bool = clap_config.is_present("fix"); | |
377 | ||
378 | LintcheckConfig { | |
379 | max_jobs, | |
380 | sources_toml_path, | |
381 | lintcheck_results_path, | |
382 | fix, | |
383 | } | |
384 | } | |
385 | } | |
386 | ||
387 | /// takes a single json-formatted clippy warnings and returns true (we are interested in that line) | |
388 | /// or false (we aren't) | |
389 | fn filter_clippy_warnings(line: &str) -> bool { | |
390 | // we want to collect ICEs because clippy might have crashed. | |
391 | // these are summarized later | |
392 | if line.contains("internal compiler error: ") { | |
393 | return true; | |
394 | } | |
395 | // in general, we want all clippy warnings | |
396 | // however due to some kind of bug, sometimes there are absolute paths | |
397 | // to libcore files inside the message | |
398 | // or we end up with cargo-metadata output (https://github.com/rust-lang/rust-clippy/issues/6508) | |
399 | ||
400 | // filter out these message to avoid unnecessary noise in the logs | |
401 | if line.contains("clippy::") | |
402 | && !(line.contains("could not read cargo metadata") | |
403 | || (line.contains(".rustup") && line.contains("toolchains"))) | |
404 | { | |
405 | return true; | |
406 | } | |
407 | false | |
408 | } | |
409 | ||
410 | /// Builds clippy inside the repo to make sure we have a clippy executable we can use. | |
411 | fn build_clippy() { | |
412 | let status = Command::new("cargo") | |
413 | .arg("build") | |
414 | .status() | |
415 | .expect("Failed to build clippy!"); | |
416 | if !status.success() { | |
417 | eprintln!("Error: Failed to compile Clippy!"); | |
418 | std::process::exit(1); | |
419 | } | |
420 | } | |
421 | ||
422 | /// Read a `toml` file and return a list of `CrateSources` that we want to check with clippy | |
423 | fn read_crates(toml_path: &Path) -> Vec<CrateSource> { | |
424 | let toml_content: String = | |
425 | std::fs::read_to_string(&toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display())); | |
426 | let crate_list: SourceList = | |
427 | toml::from_str(&toml_content).unwrap_or_else(|e| panic!("Failed to parse {}: \n{}", toml_path.display(), e)); | |
428 | // parse the hashmap of the toml file into a list of crates | |
429 | let tomlcrates: Vec<TomlCrate> = crate_list | |
430 | .crates | |
431 | .into_iter() | |
432 | .map(|(_cratename, tomlcrate)| tomlcrate) | |
433 | .collect(); | |
434 | ||
435 | // flatten TomlCrates into CrateSources (one TomlCrates may represent several versions of a crate => | |
436 | // multiple Cratesources) | |
437 | let mut crate_sources = Vec::new(); | |
438 | tomlcrates.into_iter().for_each(|tk| { | |
439 | if let Some(ref path) = tk.path { | |
440 | crate_sources.push(CrateSource::Path { | |
441 | name: tk.name.clone(), | |
442 | path: PathBuf::from(path), | |
443 | options: tk.options.clone(), | |
444 | }); | |
445 | } | |
446 | ||
447 | // if we have multiple versions, save each one | |
448 | if let Some(ref versions) = tk.versions { | |
449 | versions.iter().for_each(|ver| { | |
450 | crate_sources.push(CrateSource::CratesIo { | |
451 | name: tk.name.clone(), | |
452 | version: ver.to_string(), | |
453 | options: tk.options.clone(), | |
454 | }); | |
455 | }) | |
456 | } | |
457 | // otherwise, we should have a git source | |
458 | if tk.git_url.is_some() && tk.git_hash.is_some() { | |
459 | crate_sources.push(CrateSource::Git { | |
460 | name: tk.name.clone(), | |
461 | url: tk.git_url.clone().unwrap(), | |
462 | commit: tk.git_hash.clone().unwrap(), | |
463 | options: tk.options.clone(), | |
464 | }); | |
465 | } | |
466 | // if we have a version as well as a git data OR only one git data, something is funky | |
467 | if tk.versions.is_some() && (tk.git_url.is_some() || tk.git_hash.is_some()) | |
468 | || tk.git_hash.is_some() != tk.git_url.is_some() | |
469 | { | |
470 | eprintln!("tomlkrate: {:?}", tk); | |
471 | if tk.git_hash.is_some() != tk.git_url.is_some() { | |
472 | panic!("Error: Encountered TomlCrate with only one of git_hash and git_url!"); | |
473 | } | |
474 | if tk.path.is_some() && (tk.git_hash.is_some() || tk.versions.is_some()) { | |
475 | panic!("Error: TomlCrate can only have one of 'git_.*', 'version' or 'path' fields"); | |
476 | } | |
477 | unreachable!("Failed to translate TomlCrate into CrateSource!"); | |
478 | } | |
479 | }); | |
480 | // sort the crates | |
481 | crate_sources.sort(); | |
482 | ||
483 | crate_sources | |
484 | } | |
485 | ||
486 | /// Parse the json output of clippy and return a `ClippyWarning` | |
487 | fn parse_json_message(json_message: &str, krate: &Crate) -> ClippyWarning { | |
488 | let jmsg: Value = serde_json::from_str(&json_message).unwrap_or_else(|e| panic!("Failed to parse json:\n{:?}", e)); | |
489 | ||
490 | let file: String = jmsg["message"]["spans"][0]["file_name"] | |
491 | .to_string() | |
492 | .trim_matches('"') | |
493 | .into(); | |
494 | ||
495 | let file = if file.contains(".cargo") { | |
496 | // if we deal with macros, a filename may show the origin of a macro which can be inside a dep from | |
497 | // the registry. | |
498 | // don't show the full path in that case. | |
499 | ||
500 | // /home/matthias/.cargo/registry/src/github.com-1ecc6299db9ec823/syn-1.0.63/src/custom_keyword.rs | |
501 | let path = PathBuf::from(file); | |
502 | let mut piter = path.iter(); | |
503 | // consume all elements until we find ".cargo", so that "/home/matthias" is skipped | |
504 | let _: Option<&OsStr> = piter.find(|x| x == &std::ffi::OsString::from(".cargo")); | |
505 | // collect the remaining segments | |
506 | let file = piter.collect::<PathBuf>(); | |
507 | format!("{}", file.display()) | |
508 | } else { | |
509 | file | |
510 | }; | |
511 | ||
512 | ClippyWarning { | |
513 | crate_name: krate.name.to_string(), | |
514 | crate_version: krate.version.to_string(), | |
515 | file, | |
516 | line: jmsg["message"]["spans"][0]["line_start"] | |
517 | .to_string() | |
518 | .trim_matches('"') | |
519 | .into(), | |
520 | column: jmsg["message"]["spans"][0]["text"][0]["highlight_start"] | |
521 | .to_string() | |
522 | .trim_matches('"') | |
523 | .into(), | |
524 | linttype: jmsg["message"]["code"]["code"].to_string().trim_matches('"').into(), | |
525 | message: jmsg["message"]["message"].to_string().trim_matches('"').into(), | |
526 | is_ice: json_message.contains("internal compiler error: "), | |
527 | } | |
528 | } | |
529 | ||
530 | /// Generate a short list of occuring lints-types and their count | |
531 | fn gather_stats(clippy_warnings: &[ClippyWarning]) -> (String, HashMap<&String, usize>) { | |
532 | // count lint type occurrences | |
533 | let mut counter: HashMap<&String, usize> = HashMap::new(); | |
534 | clippy_warnings | |
535 | .iter() | |
536 | .for_each(|wrn| *counter.entry(&wrn.linttype).or_insert(0) += 1); | |
537 | ||
538 | // collect into a tupled list for sorting | |
539 | let mut stats: Vec<(&&String, &usize)> = counter.iter().map(|(lint, count)| (lint, count)).collect(); | |
540 | // sort by "000{count} {clippy::lintname}" | |
541 | // to not have a lint with 200 and 2 warnings take the same spot | |
542 | stats.sort_by_key(|(lint, count)| format!("{:0>4}, {}", count, lint)); | |
543 | ||
544 | let stats_string = stats | |
545 | .iter() | |
546 | .map(|(lint, count)| format!("{} {}\n", lint, count)) | |
547 | .collect::<String>(); | |
548 | ||
549 | (stats_string, counter) | |
550 | } | |
551 | ||
552 | /// check if the latest modification of the logfile is older than the modification date of the | |
553 | /// clippy binary, if this is true, we should clean the lintchec shared target directory and recheck | |
554 | fn lintcheck_needs_rerun(lintcheck_logs_path: &Path) -> bool { | |
555 | if !lintcheck_logs_path.exists() { | |
556 | return true; | |
557 | } | |
558 | ||
559 | let clippy_modified: std::time::SystemTime = { | |
560 | let mut times = [CLIPPY_DRIVER_PATH, CARGO_CLIPPY_PATH].iter().map(|p| { | |
561 | std::fs::metadata(p) | |
562 | .expect("failed to get metadata of file") | |
563 | .modified() | |
564 | .expect("failed to get modification date") | |
565 | }); | |
566 | // the oldest modification of either of the binaries | |
567 | std::cmp::max(times.next().unwrap(), times.next().unwrap()) | |
568 | }; | |
569 | ||
570 | let logs_modified: std::time::SystemTime = std::fs::metadata(lintcheck_logs_path) | |
571 | .expect("failed to get metadata of file") | |
572 | .modified() | |
573 | .expect("failed to get modification date"); | |
574 | ||
575 | // time is represented in seconds since X | |
576 | // logs_modified 2 and clippy_modified 5 means clippy binary is older and we need to recheck | |
577 | logs_modified < clippy_modified | |
578 | } | |
579 | ||
580 | fn is_in_clippy_root() -> bool { | |
581 | if let Ok(pb) = std::env::current_dir() { | |
582 | if let Some(file) = pb.file_name() { | |
583 | return file == PathBuf::from("rust-clippy"); | |
584 | } | |
585 | } | |
586 | ||
587 | false | |
588 | } | |
589 | ||
590 | /// lintchecks `main()` function | |
591 | /// | |
592 | /// # Panics | |
593 | /// | |
594 | /// This function panics if the clippy binaries don't exist | |
595 | /// or if lintcheck is executed from the wrong directory (aka none-repo-root) | |
596 | pub fn main() { | |
597 | // assert that we launch lintcheck from the repo root (via cargo lintcheck) | |
598 | if !is_in_clippy_root() { | |
599 | eprintln!("lintcheck needs to be run from clippys repo root!\nUse `cargo lintcheck` alternatively."); | |
600 | std::process::exit(3); | |
601 | } | |
602 | ||
603 | let clap_config = &get_clap_config(); | |
604 | ||
605 | let config = LintcheckConfig::from_clap(clap_config); | |
606 | ||
607 | println!("Compiling clippy..."); | |
608 | build_clippy(); | |
609 | println!("Done compiling"); | |
610 | ||
611 | // if the clippy bin is newer than our logs, throw away target dirs to force clippy to | |
612 | // refresh the logs | |
613 | if lintcheck_needs_rerun(&config.lintcheck_results_path) { | |
614 | let shared_target_dir = "target/lintcheck/shared_target_dir"; | |
615 | // if we get an Err here, the shared target dir probably does simply not exist | |
616 | if let Ok(metadata) = std::fs::metadata(&shared_target_dir) { | |
617 | if metadata.is_dir() { | |
618 | println!("Clippy is newer than lint check logs, clearing lintcheck shared target dir..."); | |
619 | std::fs::remove_dir_all(&shared_target_dir) | |
620 | .expect("failed to remove target/lintcheck/shared_target_dir"); | |
621 | } | |
622 | } | |
623 | } | |
624 | ||
625 | let cargo_clippy_path: PathBuf = PathBuf::from(CARGO_CLIPPY_PATH) | |
626 | .canonicalize() | |
627 | .expect("failed to canonicalize path to clippy binary"); | |
628 | ||
629 | // assert that clippy is found | |
630 | assert!( | |
631 | cargo_clippy_path.is_file(), | |
632 | "target/debug/cargo-clippy binary not found! {}", | |
633 | cargo_clippy_path.display() | |
634 | ); | |
635 | ||
636 | let clippy_ver = std::process::Command::new(CARGO_CLIPPY_PATH) | |
637 | .arg("--version") | |
638 | .output() | |
639 | .map(|o| String::from_utf8_lossy(&o.stdout).into_owned()) | |
640 | .expect("could not get clippy version!"); | |
641 | ||
642 | // download and extract the crates, then run clippy on them and collect clippys warnings | |
643 | // flatten into one big list of warnings | |
644 | ||
645 | let crates = read_crates(&config.sources_toml_path); | |
646 | let old_stats = read_stats_from_file(&config.lintcheck_results_path); | |
647 | ||
648 | let counter = AtomicUsize::new(1); | |
649 | ||
650 | let clippy_warnings: Vec<ClippyWarning> = if let Some(only_one_crate) = clap_config.value_of("only") { | |
651 | // if we don't have the specified crate in the .toml, throw an error | |
652 | if !crates.iter().any(|krate| { | |
653 | let name = match krate { | |
654 | CrateSource::CratesIo { name, .. } | CrateSource::Git { name, .. } | CrateSource::Path { name, .. } => { | |
655 | name | |
656 | }, | |
657 | }; | |
658 | name == only_one_crate | |
659 | }) { | |
660 | eprintln!( | |
661 | "ERROR: could not find crate '{}' in lintcheck/lintcheck_crates.toml", | |
662 | only_one_crate | |
663 | ); | |
664 | std::process::exit(1); | |
665 | } | |
666 | ||
667 | // only check a single crate that was passed via cmdline | |
668 | crates | |
669 | .into_iter() | |
670 | .map(|krate| krate.download_and_extract()) | |
671 | .filter(|krate| krate.name == only_one_crate) | |
672 | .flat_map(|krate| krate.run_clippy_lints(&cargo_clippy_path, &AtomicUsize::new(0), 1, 1, config.fix)) | |
673 | .collect() | |
674 | } else { | |
675 | if config.max_jobs > 1 { | |
676 | // run parallel with rayon | |
677 | ||
678 | // Ask rayon for thread count. Assume that half of that is the number of physical cores | |
679 | // Use one target dir for each core so that we can run N clippys in parallel. | |
680 | // We need to use different target dirs because cargo would lock them for a single build otherwise, | |
681 | // killing the parallelism. However this also means that deps will only be reused half/a | |
682 | // quarter of the time which might result in a longer wall clock runtime | |
683 | ||
684 | // This helps when we check many small crates with dep-trees that don't have a lot of branches in | |
685 | // order to achive some kind of parallelism | |
686 | ||
687 | // by default, use a single thread | |
688 | let num_cpus = config.max_jobs; | |
689 | let num_crates = crates.len(); | |
690 | ||
691 | // check all crates (default) | |
692 | crates | |
693 | .into_par_iter() | |
694 | .map(|krate| krate.download_and_extract()) | |
695 | .flat_map(|krate| { | |
696 | krate.run_clippy_lints(&cargo_clippy_path, &counter, num_cpus, num_crates, config.fix) | |
697 | }) | |
698 | .collect() | |
699 | } else { | |
700 | // run sequential | |
701 | let num_crates = crates.len(); | |
702 | crates | |
703 | .into_iter() | |
704 | .map(|krate| krate.download_and_extract()) | |
705 | .flat_map(|krate| krate.run_clippy_lints(&cargo_clippy_path, &counter, 1, num_crates, config.fix)) | |
706 | .collect() | |
707 | } | |
708 | }; | |
709 | ||
710 | // if we are in --fix mode, don't change the log files, terminate here | |
711 | if config.fix { | |
712 | return; | |
713 | } | |
714 | ||
715 | // generate some stats | |
716 | let (stats_formatted, new_stats) = gather_stats(&clippy_warnings); | |
717 | ||
718 | // grab crashes/ICEs, save the crate name and the ice message | |
719 | let ices: Vec<(&String, &String)> = clippy_warnings | |
720 | .iter() | |
721 | .filter(|warning| warning.is_ice) | |
722 | .map(|w| (&w.crate_name, &w.message)) | |
723 | .collect(); | |
724 | ||
725 | let mut all_msgs: Vec<String> = clippy_warnings.iter().map(ToString::to_string).collect(); | |
726 | all_msgs.sort(); | |
727 | all_msgs.push("\n\n\n\nStats:\n".into()); | |
728 | all_msgs.push(stats_formatted); | |
729 | ||
730 | // save the text into lintcheck-logs/logs.txt | |
731 | let mut text = clippy_ver; // clippy version number on top | |
732 | text.push_str(&format!("\n{}", all_msgs.join(""))); | |
733 | text.push_str("ICEs:\n"); | |
734 | ices.iter() | |
735 | .for_each(|(cratename, msg)| text.push_str(&format!("{}: '{}'", cratename, msg))); | |
736 | ||
737 | println!("Writing logs to {}", config.lintcheck_results_path.display()); | |
738 | write(&config.lintcheck_results_path, text).unwrap(); | |
739 | ||
740 | print_stats(old_stats, new_stats); | |
741 | } | |
742 | ||
743 | /// read the previous stats from the lintcheck-log file | |
744 | fn read_stats_from_file(file_path: &Path) -> HashMap<String, usize> { | |
745 | let file_content: String = match std::fs::read_to_string(file_path).ok() { | |
746 | Some(content) => content, | |
747 | None => { | |
748 | return HashMap::new(); | |
749 | }, | |
750 | }; | |
751 | ||
752 | let lines: Vec<String> = file_content.lines().map(ToString::to_string).collect(); | |
753 | ||
754 | // search for the beginning "Stats:" and the end "ICEs:" of the section we want | |
755 | let start = lines.iter().position(|line| line == "Stats:").unwrap(); | |
756 | let end = lines.iter().position(|line| line == "ICEs:").unwrap(); | |
757 | ||
758 | let stats_lines = &lines[start + 1..end]; | |
759 | ||
760 | stats_lines | |
761 | .iter() | |
762 | .map(|line| { | |
763 | let mut spl = line.split(' '); | |
764 | ( | |
765 | spl.next().unwrap().to_string(), | |
766 | spl.next().unwrap().parse::<usize>().unwrap(), | |
767 | ) | |
768 | }) | |
769 | .collect::<HashMap<String, usize>>() | |
770 | } | |
771 | ||
772 | /// print how lint counts changed between runs | |
773 | fn print_stats(old_stats: HashMap<String, usize>, new_stats: HashMap<&String, usize>) { | |
774 | let same_in_both_hashmaps = old_stats | |
775 | .iter() | |
776 | .filter(|(old_key, old_val)| new_stats.get::<&String>(&old_key) == Some(old_val)) | |
777 | .map(|(k, v)| (k.to_string(), *v)) | |
778 | .collect::<Vec<(String, usize)>>(); | |
779 | ||
780 | let mut old_stats_deduped = old_stats; | |
781 | let mut new_stats_deduped = new_stats; | |
782 | ||
783 | // remove duplicates from both hashmaps | |
784 | same_in_both_hashmaps.iter().for_each(|(k, v)| { | |
785 | assert!(old_stats_deduped.remove(k) == Some(*v)); | |
786 | assert!(new_stats_deduped.remove(k) == Some(*v)); | |
787 | }); | |
788 | ||
789 | println!("\nStats:"); | |
790 | ||
791 | // list all new counts (key is in new stats but not in old stats) | |
792 | new_stats_deduped | |
793 | .iter() | |
794 | .filter(|(new_key, _)| old_stats_deduped.get::<str>(&new_key).is_none()) | |
795 | .for_each(|(new_key, new_value)| { | |
796 | println!("{} 0 => {}", new_key, new_value); | |
797 | }); | |
798 | ||
799 | // list all changed counts (key is in both maps but value differs) | |
800 | new_stats_deduped | |
801 | .iter() | |
802 | .filter(|(new_key, _new_val)| old_stats_deduped.get::<str>(&new_key).is_some()) | |
803 | .for_each(|(new_key, new_val)| { | |
804 | let old_val = old_stats_deduped.get::<str>(&new_key).unwrap(); | |
805 | println!("{} {} => {}", new_key, old_val, new_val); | |
806 | }); | |
807 | ||
808 | // list all gone counts (key is in old status but not in new stats) | |
809 | old_stats_deduped | |
810 | .iter() | |
811 | .filter(|(old_key, _)| new_stats_deduped.get::<&String>(&old_key).is_none()) | |
812 | .for_each(|(old_key, old_value)| { | |
813 | println!("{} {} => 0", old_key, old_value); | |
814 | }); | |
815 | } | |
816 | ||
817 | /// Create necessary directories to run the lintcheck tool. | |
818 | /// | |
819 | /// # Panics | |
820 | /// | |
821 | /// This function panics if creating one of the dirs fails. | |
822 | fn create_dirs(krate_download_dir: &Path, extract_dir: &Path) { | |
823 | std::fs::create_dir("target/lintcheck/").unwrap_or_else(|err| { | |
824 | if err.kind() != ErrorKind::AlreadyExists { | |
825 | panic!("cannot create lintcheck target dir"); | |
826 | } | |
827 | }); | |
828 | std::fs::create_dir(&krate_download_dir).unwrap_or_else(|err| { | |
829 | if err.kind() != ErrorKind::AlreadyExists { | |
830 | panic!("cannot create crate download dir"); | |
831 | } | |
832 | }); | |
833 | std::fs::create_dir(&extract_dir).unwrap_or_else(|err| { | |
834 | if err.kind() != ErrorKind::AlreadyExists { | |
835 | panic!("cannot create crate extraction dir"); | |
836 | } | |
837 | }); | |
838 | } | |
839 | ||
840 | fn get_clap_config<'a>() -> ArgMatches<'a> { | |
841 | App::new("lintcheck") | |
842 | .about("run clippy on a set of crates and check output") | |
843 | .arg( | |
844 | Arg::with_name("only") | |
845 | .takes_value(true) | |
846 | .value_name("CRATE") | |
847 | .long("only") | |
848 | .help("only process a single crate of the list"), | |
849 | ) | |
850 | .arg( | |
851 | Arg::with_name("crates-toml") | |
852 | .takes_value(true) | |
853 | .value_name("CRATES-SOURCES-TOML-PATH") | |
854 | .long("crates-toml") | |
855 | .help("set the path for a crates.toml where lintcheck should read the sources from"), | |
856 | ) | |
857 | .arg( | |
858 | Arg::with_name("threads") | |
859 | .takes_value(true) | |
860 | .value_name("N") | |
861 | .short("j") | |
862 | .long("jobs") | |
863 | .help("number of threads to use, 0 automatic choice"), | |
864 | ) | |
865 | .arg( | |
866 | Arg::with_name("fix") | |
867 | .long("--fix") | |
868 | .help("runs cargo clippy --fix and checks if all suggestions apply"), | |
869 | ) | |
870 | .get_matches() | |
871 | } | |
872 | ||
873 | /// Returns the path to the Clippy project directory | |
874 | /// | |
875 | /// # Panics | |
876 | /// | |
877 | /// Panics if the current directory could not be retrieved, there was an error reading any of the | |
878 | /// Cargo.toml files or ancestor directory is the clippy root directory | |
879 | #[must_use] | |
880 | pub fn clippy_project_root() -> PathBuf { | |
881 | let current_dir = std::env::current_dir().unwrap(); | |
882 | for path in current_dir.ancestors() { | |
883 | let result = std::fs::read_to_string(path.join("Cargo.toml")); | |
884 | if let Err(err) = &result { | |
885 | if err.kind() == std::io::ErrorKind::NotFound { | |
886 | continue; | |
887 | } | |
888 | } | |
889 | ||
890 | let content = result.unwrap(); | |
891 | if content.contains("[package]\nname = \"clippy\"") { | |
892 | return path.to_path_buf(); | |
893 | } | |
894 | } | |
895 | panic!("error: Can't determine root of project. Please run inside a Clippy working dir."); | |
896 | } | |
897 | ||
898 | #[test] | |
899 | fn lintcheck_test() { | |
900 | let args = [ | |
901 | "run", | |
902 | "--target-dir", | |
903 | "lintcheck/target", | |
904 | "--manifest-path", | |
905 | "./lintcheck/Cargo.toml", | |
906 | "--", | |
907 | "--crates-toml", | |
908 | "lintcheck/test_sources.toml", | |
909 | ]; | |
910 | let status = std::process::Command::new("cargo") | |
911 | .args(&args) | |
912 | .current_dir("..") // repo root | |
913 | .status(); | |
914 | //.output(); | |
915 | ||
916 | assert!(status.unwrap().success()); | |
917 | } |