]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/crater.rs
clippy dev crater: add option to only check a single one of the listed crates with...
[rust.git] / clippy_dev / src / crater.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 #![allow(clippy::filter_map)]
8
9 use crate::clippy_project_root;
10
11 use std::collections::HashMap;
12 use std::process::Command;
13 use std::{fmt, fs::write, path::PathBuf};
14
15 use clap::ArgMatches;
16 use serde::{Deserialize, Serialize};
17 use serde_json::Value;
18
19 // use this to store the crates when interacting with the crates.toml file
20 #[derive(Debug, Serialize, Deserialize)]
21 struct CrateList {
22     crates: HashMap<String, Vec<String>>,
23 }
24
25 // crate data we stored in the toml, can have multiple versions per crate
26 // A single TomlCrate is laster mapped to several CrateSources in that case
27 struct TomlCrate {
28     name: String,
29     versions: Vec<String>,
30 }
31
32 // represents an archive we download from crates.io
33 #[derive(Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
34 struct CrateSource {
35     name: String,
36     version: String,
37 }
38
39 // represents the extracted sourcecode of a crate
40 #[derive(Debug)]
41 struct Crate {
42     version: String,
43     name: String,
44     // path to the extracted sources that clippy can check
45     path: PathBuf,
46 }
47
48 #[derive(Debug)]
49 struct ClippyWarning {
50     crate_name: String,
51     crate_version: String,
52     file: String,
53     line: String,
54     column: String,
55     linttype: String,
56     message: String,
57 }
58
59 impl std::fmt::Display for ClippyWarning {
60     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61         writeln!(
62             f,
63             r#"{}/{}/{}:{}:{} {} "{}""#,
64             &self.crate_name, &self.crate_version, &self.file, &self.line, &self.column, &self.linttype, &self.message
65         )
66     }
67 }
68
69 impl CrateSource {
70     fn download_and_extract(&self) -> Crate {
71         let extract_dir = PathBuf::from("target/crater/crates");
72         let krate_download_dir = PathBuf::from("target/crater/downloads");
73
74         // url to download the crate from crates.io
75         let url = format!(
76             "https://crates.io/api/v1/crates/{}/{}/download",
77             self.name, self.version
78         );
79         println!("Downloading and extracting {} {} from {}", self.name, self.version, url);
80         let _ = std::fs::create_dir("target/crater/");
81         let _ = std::fs::create_dir(&krate_download_dir);
82         let _ = std::fs::create_dir(&extract_dir);
83
84         let krate_file_path = krate_download_dir.join(format!("{}-{}.crate.tar.gz", &self.name, &self.version));
85         // don't download/extract if we already have done so
86         if !krate_file_path.is_file() {
87             // create a file path to download and write the crate data into
88             let mut krate_dest = std::fs::File::create(&krate_file_path).unwrap();
89             let mut krate_req = ureq::get(&url).call().unwrap().into_reader();
90             // copy the crate into the file
91             std::io::copy(&mut krate_req, &mut krate_dest).unwrap();
92
93             // unzip the tarball
94             let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap());
95             // extract the tar archive
96             let mut archive = tar::Archive::new(ungz_tar);
97             archive.unpack(&extract_dir).expect("Failed to extract!");
98         }
99         // crate is extracted, return a new Krate object which contains the path to the extracted
100         // sources that clippy can check
101         Crate {
102             version: self.version.clone(),
103             name: self.name.clone(),
104             path: extract_dir.join(format!("{}-{}/", self.name, self.version)),
105         }
106     }
107 }
108
109 impl Crate {
110     fn run_clippy_lints(&self, cargo_clippy_path: &PathBuf) -> Vec<ClippyWarning> {
111         println!("Linting {} {}...", &self.name, &self.version);
112         let cargo_clippy_path = std::fs::canonicalize(cargo_clippy_path).unwrap();
113
114         let shared_target_dir = clippy_project_root().join("target/crater/shared_target_dir/");
115
116         let all_output = std::process::Command::new(cargo_clippy_path)
117             .env("CARGO_TARGET_DIR", shared_target_dir)
118             // lint warnings will look like this:
119             // src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter`
120             .args(&[
121                 "--",
122                 "--message-format=json",
123                 "--",
124                 "--cap-lints=warn",
125                 "-Wclippy::pedantic",
126                 "-Wclippy::cargo",
127             ])
128             .current_dir(&self.path)
129             .output()
130             .unwrap();
131         let stdout = String::from_utf8_lossy(&all_output.stdout);
132         let output_lines = stdout.lines();
133         //dbg!(&output_lines);
134         let warnings: Vec<ClippyWarning> = output_lines
135             .into_iter()
136             // get all clippy warnings
137             .filter(|line| line.contains("clippy::"))
138             .map(|json_msg| parse_json_message(json_msg, &self))
139             .collect();
140         warnings
141     }
142 }
143
144 fn build_clippy() {
145     Command::new("cargo")
146         .arg("build")
147         .output()
148         .expect("Failed to build clippy!");
149 }
150
151 // get a list of CrateSources we want to check from a "crater_crates.toml" file.
152 fn read_crates() -> Vec<CrateSource> {
153     let toml_path = PathBuf::from("clippy_dev/crater_crates.toml");
154     let toml_content: String =
155         std::fs::read_to_string(&toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
156     let crate_list: CrateList =
157         toml::from_str(&toml_content).unwrap_or_else(|e| panic!("Failed to parse {}: \n{}", toml_path.display(), e));
158     // parse the hashmap of the toml file into a list of crates
159     let tomlcrates: Vec<TomlCrate> = crate_list
160         .crates
161         .into_iter()
162         .map(|(name, versions)| TomlCrate { name, versions })
163         .collect();
164
165     // flatten TomlCrates into CrateSources (one TomlCrates may represent several versions of a crate =>
166     // multiple Cratesources)
167     let mut crate_sources = Vec::new();
168     tomlcrates.into_iter().for_each(|tk| {
169         tk.versions.iter().for_each(|ver| {
170             crate_sources.push(CrateSource {
171                 name: tk.name.clone(),
172                 version: ver.to_string(),
173             });
174         })
175     });
176     crate_sources
177 }
178
179 // extract interesting data from a json lint message
180 fn parse_json_message(json_message: &str, krate: &Crate) -> ClippyWarning {
181     let jmsg: Value = serde_json::from_str(&json_message).unwrap_or_else(|e| panic!("Failed to parse json:\n{:?}", e));
182
183     ClippyWarning {
184         crate_name: krate.name.to_string(),
185         crate_version: krate.version.to_string(),
186         file: jmsg["message"]["spans"][0]["file_name"]
187             .to_string()
188             .trim_matches('"')
189             .into(),
190         line: jmsg["message"]["spans"][0]["line_start"]
191             .to_string()
192             .trim_matches('"')
193             .into(),
194         column: jmsg["message"]["spans"][0]["text"][0]["highlight_start"]
195             .to_string()
196             .trim_matches('"')
197             .into(),
198         linttype: jmsg["message"]["code"]["code"].to_string().trim_matches('"').into(),
199         message: jmsg["message"]["message"].to_string().trim_matches('"').into(),
200     }
201 }
202
203 // the main fn
204 pub fn run(clap_config: &ArgMatches) {
205     let cargo_clippy_path: PathBuf = PathBuf::from("target/debug/cargo-clippy");
206
207     println!("Compiling clippy...");
208     build_clippy();
209     println!("Done compiling");
210
211     // assert that clippy is found
212     assert!(
213         cargo_clippy_path.is_file(),
214         "target/debug/cargo-clippy binary not found! {}",
215         cargo_clippy_path.display()
216     );
217
218     // download and extract the crates, then run clippy on them and collect clippys warnings
219     // flatten into one big list of warnings
220
221     let clippy_warnings: Vec<ClippyWarning> = if let Some(only_one_crate) = clap_config.value_of("only") {
222         // only check a single
223         read_crates()
224             .into_iter()
225             .map(|krate| krate.download_and_extract())
226             .filter(|krate| krate.name == only_one_crate)
227             .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
228             .flatten()
229             .collect()
230     } else {
231         read_crates()
232             .into_iter()
233             .map(|krate| krate.download_and_extract())
234             .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
235             .flatten()
236             .collect()
237     };
238
239     // generate some stats:
240
241     // count lint type occurrences
242     let mut counter: HashMap<&String, usize> = HashMap::new();
243     clippy_warnings
244         .iter()
245         .for_each(|wrn| *counter.entry(&wrn.linttype).or_insert(0) += 1);
246
247     // collect into a tupled list for sorting
248     let mut stats: Vec<(&&String, &usize)> = counter.iter().map(|(lint, count)| (lint, count)).collect();
249     // sort by "000{count} {clippy::lintname}"
250     // to not have a lint with 200 and 2 warnings take the same spot
251     stats.sort_by_key(|(lint, count)| format!("{:0>4}, {}", count, lint));
252
253     let stats_formatted: String = stats
254         .iter()
255         .map(|(lint, count)| format!("{} {}\n", lint, count))
256         .collect::<String>();
257
258     let mut all_msgs: Vec<String> = clippy_warnings.iter().map(|warning| warning.to_string()).collect();
259     all_msgs.sort();
260     all_msgs.push("\n\n\n\nStats\n\n".into());
261     all_msgs.push(stats_formatted);
262
263     // save the text into mini-crater/logs.txt
264     let text = all_msgs.join("");
265     write("mini-crater/logs.txt", text).unwrap();
266 }