]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/lintcheck.rs
Auto merge of #81993 - flip1995:clippyup, r=Manishearth
[rust.git] / clippy_dev / src / lintcheck.rs
1 // Run clippy on a fixed set of crates and collect the warnings.
2 // This helps observing the impact clippy changs have on a set of real-world code.
3 //
4 // When a new lint is introduced, we can search the results for new warnings and check for false
5 // positives.
6
7 #![cfg(feature = "lintcheck")]
8 #![allow(clippy::filter_map)]
9
10 use crate::clippy_project_root;
11
12 use std::collections::HashMap;
13 use std::process::Command;
14 use std::{fmt, fs::write, path::PathBuf};
15
16 use clap::ArgMatches;
17 use serde::{Deserialize, Serialize};
18 use serde_json::Value;
19
20 // use this to store the crates when interacting with the crates.toml file
21 #[derive(Debug, Serialize, Deserialize)]
22 struct CrateList {
23     crates: HashMap<String, TomlCrate>,
24 }
25
26 // crate data we stored in the toml, can have multiple versions per crate
27 // A single TomlCrate is laster mapped to several CrateSources in that case
28 #[derive(Debug, Serialize, Deserialize)]
29 struct TomlCrate {
30     name: String,
31     versions: Option<Vec<String>>,
32     git_url: Option<String>,
33     git_hash: Option<String>,
34     path: Option<String>,
35 }
36
37 // represents an archive we download from crates.io, or a git repo, or a local repo
38 #[derive(Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
39 enum CrateSource {
40     CratesIo { name: String, version: String },
41     Git { name: String, url: String, commit: String },
42     Path { name: String, path: PathBuf },
43 }
44
45 // represents the extracted sourcecode of a crate
46 // we actually don't need to special-case git repos here because it does not matter for clippy, yay!
47 // (clippy only needs a simple path)
48 #[derive(Debug)]
49 struct Crate {
50     version: String,
51     name: String,
52     // path to the extracted sources that clippy can check
53     path: PathBuf,
54 }
55
56 #[derive(Debug)]
57 struct ClippyWarning {
58     crate_name: String,
59     crate_version: String,
60     file: String,
61     line: String,
62     column: String,
63     linttype: String,
64     message: String,
65     ice: bool,
66 }
67
68 impl std::fmt::Display for ClippyWarning {
69     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70         writeln!(
71             f,
72             r#"{}-{}/{}:{}:{} {} "{}""#,
73             &self.crate_name, &self.crate_version, &self.file, &self.line, &self.column, &self.linttype, &self.message
74         )
75     }
76 }
77
78 impl CrateSource {
79     fn download_and_extract(&self) -> Crate {
80         match self {
81             CrateSource::CratesIo { name, version } => {
82                 let extract_dir = PathBuf::from("target/lintcheck/crates");
83                 let krate_download_dir = PathBuf::from("target/lintcheck/downloads");
84
85                 // url to download the crate from crates.io
86                 let url = format!("https://crates.io/api/v1/crates/{}/{}/download", name, version);
87                 println!("Downloading and extracting {} {} from {}", name, version, url);
88                 let _ = std::fs::create_dir("target/lintcheck/");
89                 let _ = std::fs::create_dir(&krate_download_dir);
90                 let _ = std::fs::create_dir(&extract_dir);
91
92                 let krate_file_path = krate_download_dir.join(format!("{}-{}.crate.tar.gz", name, version));
93                 // don't download/extract if we already have done so
94                 if !krate_file_path.is_file() {
95                     // create a file path to download and write the crate data into
96                     let mut krate_dest = std::fs::File::create(&krate_file_path).unwrap();
97                     let mut krate_req = ureq::get(&url).call().unwrap().into_reader();
98                     // copy the crate into the file
99                     std::io::copy(&mut krate_req, &mut krate_dest).unwrap();
100
101                     // unzip the tarball
102                     let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap());
103                     // extract the tar archive
104                     let mut archive = tar::Archive::new(ungz_tar);
105                     archive.unpack(&extract_dir).expect("Failed to extract!");
106                 }
107                 // crate is extracted, return a new Krate object which contains the path to the extracted
108                 // sources that clippy can check
109                 Crate {
110                     version: version.clone(),
111                     name: name.clone(),
112                     path: extract_dir.join(format!("{}-{}/", name, version)),
113                 }
114             },
115             CrateSource::Git { name, url, commit } => {
116                 let repo_path = {
117                     let mut repo_path = PathBuf::from("target/lintcheck/crates");
118                     // add a -git suffix in case we have the same crate from crates.io and a git repo
119                     repo_path.push(format!("{}-git", name));
120                     repo_path
121                 };
122                 // clone the repo if we have not done so
123                 if !repo_path.is_dir() {
124                     println!("Cloning {} and checking out {}", url, commit);
125                     Command::new("git")
126                         .arg("clone")
127                         .arg(url)
128                         .arg(&repo_path)
129                         .output()
130                         .expect("Failed to clone git repo!");
131                 }
132                 // check out the commit/branch/whatever
133                 Command::new("git")
134                     .arg("checkout")
135                     .arg(commit)
136                     .output()
137                     .expect("Failed to check out commit");
138
139                 Crate {
140                     version: commit.clone(),
141                     name: name.clone(),
142                     path: repo_path,
143                 }
144             },
145             CrateSource::Path { name, path } => {
146                 use fs_extra::dir;
147
148                 // simply copy the entire directory into our target dir
149                 let copy_dest = PathBuf::from("target/lintcheck/crates/");
150
151                 // the source path of the crate we copied,  ${copy_dest}/crate_name
152                 let crate_root = copy_dest.join(name); // .../crates/local_crate
153
154                 if !crate_root.exists() {
155                     println!("Copying {} to {}", path.display(), copy_dest.display());
156
157                     dir::copy(path, &copy_dest, &dir::CopyOptions::new()).expect(&format!(
158                         "Failed to copy from {}, to  {}",
159                         path.display(),
160                         crate_root.display()
161                     ));
162                 } else {
163                     println!(
164                         "Not copying {} to {}, destination already exists",
165                         path.display(),
166                         crate_root.display()
167                     );
168                 }
169
170                 Crate {
171                     version: String::from("local"),
172                     name: name.clone(),
173                     path: crate_root,
174                 }
175             },
176         }
177     }
178 }
179
180 impl Crate {
181     fn run_clippy_lints(&self, cargo_clippy_path: &PathBuf) -> Vec<ClippyWarning> {
182         println!("Linting {} {}...", &self.name, &self.version);
183         let cargo_clippy_path = std::fs::canonicalize(cargo_clippy_path).unwrap();
184
185         let shared_target_dir = clippy_project_root().join("target/lintcheck/shared_target_dir/");
186
187         let all_output = std::process::Command::new(&cargo_clippy_path)
188             .env("CARGO_TARGET_DIR", shared_target_dir)
189             // lint warnings will look like this:
190             // src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter`
191             .args(&[
192                 "--",
193                 "--message-format=json",
194                 "--",
195                 "--cap-lints=warn",
196                 "-Wclippy::pedantic",
197                 "-Wclippy::cargo",
198             ])
199             .current_dir(&self.path)
200             .output()
201             .unwrap_or_else(|error| {
202                 panic!(
203                     "Encountered error:\n{:?}\ncargo_clippy_path: {}\ncrate path:{}\n",
204                     error,
205                     &cargo_clippy_path.display(),
206                     &self.path.display()
207                 );
208             });
209         let stdout = String::from_utf8_lossy(&all_output.stdout);
210         let output_lines = stdout.lines();
211         let warnings: Vec<ClippyWarning> = output_lines
212             .into_iter()
213             // get all clippy warnings and ICEs
214             .filter(|line| line.contains("clippy::") || line.contains("internal compiler error: "))
215             .map(|json_msg| parse_json_message(json_msg, &self))
216             .collect();
217         warnings
218     }
219 }
220
221 fn build_clippy() {
222     Command::new("cargo")
223         .arg("build")
224         .output()
225         .expect("Failed to build clippy!");
226 }
227
228 // get a list of CrateSources we want to check from a "lintcheck_crates.toml" file.
229 fn read_crates(toml_path: Option<&str>) -> (String, Vec<CrateSource>) {
230     let toml_path = PathBuf::from(toml_path.unwrap_or("clippy_dev/lintcheck_crates.toml"));
231     // save it so that we can use the name of the sources.toml as name for the logfile later.
232     let toml_filename = toml_path.file_stem().unwrap().to_str().unwrap().to_string();
233     let toml_content: String =
234         std::fs::read_to_string(&toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
235     let crate_list: CrateList =
236         toml::from_str(&toml_content).unwrap_or_else(|e| panic!("Failed to parse {}: \n{}", toml_path.display(), e));
237     // parse the hashmap of the toml file into a list of crates
238     let tomlcrates: Vec<TomlCrate> = crate_list
239         .crates
240         .into_iter()
241         .map(|(_cratename, tomlcrate)| tomlcrate)
242         .collect();
243
244     // flatten TomlCrates into CrateSources (one TomlCrates may represent several versions of a crate =>
245     // multiple Cratesources)
246     let mut crate_sources = Vec::new();
247     tomlcrates.into_iter().for_each(|tk| {
248         if let Some(ref path) = tk.path {
249             crate_sources.push(CrateSource::Path {
250                 name: tk.name.clone(),
251                 path: PathBuf::from(path),
252             });
253         }
254
255         // if we have multiple versions, save each one
256         if let Some(ref versions) = tk.versions {
257             versions.iter().for_each(|ver| {
258                 crate_sources.push(CrateSource::CratesIo {
259                     name: tk.name.clone(),
260                     version: ver.to_string(),
261                 });
262             })
263         }
264         // otherwise, we should have a git source
265         if tk.git_url.is_some() && tk.git_hash.is_some() {
266             crate_sources.push(CrateSource::Git {
267                 name: tk.name.clone(),
268                 url: tk.git_url.clone().unwrap(),
269                 commit: tk.git_hash.clone().unwrap(),
270             });
271         }
272         // if we have a version as well as a git data OR only one git data, something is funky
273         if tk.versions.is_some() && (tk.git_url.is_some() || tk.git_hash.is_some())
274             || tk.git_hash.is_some() != tk.git_url.is_some()
275         {
276             eprintln!("tomlkrate: {:?}", tk);
277             if tk.git_hash.is_some() != tk.git_url.is_some() {
278                 panic!("Error: Encountered TomlCrate with only one of git_hash and git_url!");
279             }
280             if tk.path.is_some() && (tk.git_hash.is_some() || tk.versions.is_some()) {
281                 panic!("Error: TomlCrate can only have one of 'git_.*', 'version' or 'path' fields");
282             }
283             unreachable!("Failed to translate TomlCrate into CrateSource!");
284         }
285     });
286     (toml_filename, crate_sources)
287 }
288
289 // extract interesting data from a json lint message
290 fn parse_json_message(json_message: &str, krate: &Crate) -> ClippyWarning {
291     let jmsg: Value = serde_json::from_str(&json_message).unwrap_or_else(|e| panic!("Failed to parse json:\n{:?}", e));
292
293     ClippyWarning {
294         crate_name: krate.name.to_string(),
295         crate_version: krate.version.to_string(),
296         file: jmsg["message"]["spans"][0]["file_name"]
297             .to_string()
298             .trim_matches('"')
299             .into(),
300         line: jmsg["message"]["spans"][0]["line_start"]
301             .to_string()
302             .trim_matches('"')
303             .into(),
304         column: jmsg["message"]["spans"][0]["text"][0]["highlight_start"]
305             .to_string()
306             .trim_matches('"')
307             .into(),
308         linttype: jmsg["message"]["code"]["code"].to_string().trim_matches('"').into(),
309         message: jmsg["message"]["message"].to_string().trim_matches('"').into(),
310         ice: json_message.contains("internal compiler error: "),
311     }
312 }
313
314 // the main fn
315 pub fn run(clap_config: &ArgMatches) {
316     let cargo_clippy_path: PathBuf = PathBuf::from("target/debug/cargo-clippy");
317
318     println!("Compiling clippy...");
319     build_clippy();
320     println!("Done compiling");
321
322     // assert that clippy is found
323     assert!(
324         cargo_clippy_path.is_file(),
325         "target/debug/cargo-clippy binary not found! {}",
326         cargo_clippy_path.display()
327     );
328
329     let clippy_ver = std::process::Command::new("target/debug/cargo-clippy")
330         .arg("--version")
331         .output()
332         .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
333         .expect("could not get clippy version!");
334
335     // download and extract the crates, then run clippy on them and collect clippys warnings
336     // flatten into one big list of warnings
337
338     let (filename, crates) = read_crates(clap_config.value_of("crates-toml"));
339
340     let clippy_warnings: Vec<ClippyWarning> = if let Some(only_one_crate) = clap_config.value_of("only") {
341         // if we don't have the specified crate in the .toml, throw an error
342         if !crates.iter().any(|krate| {
343             let name = match krate {
344                 CrateSource::CratesIo { name, .. } => name,
345                 CrateSource::Git { name, .. } => name,
346                 CrateSource::Path { name, .. } => name,
347             };
348             name == only_one_crate
349         }) {
350             eprintln!(
351                 "ERROR: could not find crate '{}' in clippy_dev/lintcheck_crates.toml",
352                 only_one_crate
353             );
354             std::process::exit(1);
355         }
356
357         // only check a single crate that was passed via cmdline
358         crates
359             .into_iter()
360             .map(|krate| krate.download_and_extract())
361             .filter(|krate| krate.name == only_one_crate)
362             .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
363             .flatten()
364             .collect()
365     } else {
366         // check all crates (default)
367         crates
368             .into_iter()
369             .map(|krate| krate.download_and_extract())
370             .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
371             .flatten()
372             .collect()
373     };
374
375     // generate some stats:
376
377     // grab crashes/ICEs, save the crate name and the ice message
378     let ices: Vec<(&String, &String)> = clippy_warnings
379         .iter()
380         .filter(|warning| warning.ice)
381         .map(|w| (&w.crate_name, &w.message))
382         .collect();
383
384     // count lint type occurrences
385     let mut counter: HashMap<&String, usize> = HashMap::new();
386     clippy_warnings
387         .iter()
388         .for_each(|wrn| *counter.entry(&wrn.linttype).or_insert(0) += 1);
389
390     // collect into a tupled list for sorting
391     let mut stats: Vec<(&&String, &usize)> = counter.iter().map(|(lint, count)| (lint, count)).collect();
392     // sort by "000{count} {clippy::lintname}"
393     // to not have a lint with 200 and 2 warnings take the same spot
394     stats.sort_by_key(|(lint, count)| format!("{:0>4}, {}", count, lint));
395
396     let stats_formatted: String = stats
397         .iter()
398         .map(|(lint, count)| format!("{} {}\n", lint, count))
399         .collect::<String>();
400
401     let mut all_msgs: Vec<String> = clippy_warnings.iter().map(|warning| warning.to_string()).collect();
402     all_msgs.sort();
403     all_msgs.push("\n\n\n\nStats\n\n".into());
404     all_msgs.push(stats_formatted);
405
406     // save the text into lintcheck-logs/logs.txt
407     let mut text = clippy_ver; // clippy version number on top
408     text.push_str(&format!("\n{}", all_msgs.join("")));
409     text.push_str("ICEs:\n");
410     ices.iter()
411         .for_each(|(cratename, msg)| text.push_str(&format!("{}: '{}'", cratename, msg)));
412
413     let file = format!("lintcheck-logs/{}_logs.txt", filename);
414     write(file, text).unwrap();
415 }