]> git.lizzy.rs Git - rust.git/blob - clippy_dev/src/crater.rs
use a .toml file to list the crates we want to check
[rust.git] / clippy_dev / src / crater.rs
1 #![allow(clippy::filter_map)]
2
3 use crate::clippy_project_root;
4 use serde::{Deserialize, Serialize};
5 use std::collections::HashMap;
6 use std::process::Command;
7 use std::{fs::write, path::PathBuf};
8
9 // represents an archive we download from crates.io
10 #[derive(Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
11 struct KrateSource {
12     version: String,
13     name: String,
14 }
15
16 // use this to store the crates when interacting with the crates.toml file
17 #[derive(Debug, Serialize, Deserialize)]
18 struct CrateList {
19     crates: HashMap<String, String>,
20 }
21
22 // represents the extracted sourcecode of a crate
23 #[derive(Debug)]
24 struct Krate {
25     version: String,
26     name: String,
27     // path to the extracted sources that clippy can check
28     path: PathBuf,
29 }
30
31 impl KrateSource {
32     fn new(name: &str, version: &str) -> Self {
33         KrateSource {
34             version: version.into(),
35             name: name.into(),
36         }
37     }
38
39     fn download_and_extract(&self) -> Krate {
40         let extract_dir = PathBuf::from("target/crater/crates");
41         let krate_download_dir = PathBuf::from("target/crater/downloads");
42
43         // url to download the crate from crates.io
44         let url = format!(
45             "https://crates.io/api/v1/crates/{}/{}/download",
46             self.name, self.version
47         );
48         println!("Downloading and extracting {} {} from {}", self.name, self.version, url);
49         let _ = std::fs::create_dir("target/crater/");
50         let _ = std::fs::create_dir(&krate_download_dir);
51         let _ = std::fs::create_dir(&extract_dir);
52
53         let krate_file_path = krate_download_dir.join(format!("{}-{}.crate.tar.gz", &self.name, &self.version));
54         // don't download/extract if we already have done so
55         if !krate_file_path.is_file() {
56             // create a file path to download and write the crate data into
57             let mut krate_dest = std::fs::File::create(&krate_file_path).unwrap();
58             let mut krate_req = ureq::get(&url).call().unwrap().into_reader();
59             // copy the crate into the file
60             std::io::copy(&mut krate_req, &mut krate_dest).unwrap();
61
62             // unzip the tarball
63             let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap());
64             // extract the tar archive
65             let mut archiv = tar::Archive::new(ungz_tar);
66             archiv.unpack(&extract_dir).expect("Failed to extract!");
67
68             // unzip the tarball
69             let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap());
70             // extract the tar archive
71             let mut archiv = tar::Archive::new(ungz_tar);
72             archiv.unpack(&extract_dir).expect("Failed to extract!");
73         }
74         // crate is extracted, return a new Krate object which contains the path to the extracted
75         // sources that clippy can check
76         Krate {
77             version: self.version.clone(),
78             name: self.name.clone(),
79             path: extract_dir.join(format!("{}-{}/", self.name, self.version)),
80         }
81     }
82 }
83
84 impl Krate {
85     fn run_clippy_lints(&self, cargo_clippy_path: &PathBuf) -> Vec<String> {
86         println!("Linting {} {}...", &self.name, &self.version);
87         let cargo_clippy_path = std::fs::canonicalize(cargo_clippy_path).unwrap();
88
89         let shared_target_dir = clippy_project_root().join("target/crater/shared_target_dir/");
90
91         let all_output = std::process::Command::new(cargo_clippy_path)
92             .env("CARGO_TARGET_DIR", shared_target_dir)
93             // lint warnings will look like this:
94             // src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter`
95             .args(&[
96                 "--",
97                 "--message-format=short",
98                 "--",
99                 "--cap-lints=warn",
100                 "-Wclippy::pedantic",
101                 "-Wclippy::cargo",
102             ])
103             .current_dir(&self.path)
104             .output()
105             .unwrap();
106         let stderr = String::from_utf8_lossy(&all_output.stderr);
107         let output_lines = stderr.lines();
108         let mut output: Vec<String> = output_lines
109             .into_iter()
110             .filter(|line| line.contains(": warning: "))
111             // prefix with the crate name and version
112             // cargo-0.49.0/src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter`
113             .map(|line| format!("{}-{}/{}", self.name, self.version, line))
114             // remove the "warning: "
115             .map(|line| {
116                 let remove_pat = "warning: ";
117                 let pos = line
118                     .find(&remove_pat)
119                     .expect("clippy output did not contain \"warning: \"");
120                 let mut new = line[0..pos].to_string();
121                 new.push_str(&line[pos + remove_pat.len()..]);
122                 new.push('\n');
123                 new
124             })
125             .collect();
126
127         // sort messages alphabtically to avoid noise in the logs
128         output.sort();
129         output
130     }
131 }
132
133 fn build_clippy() {
134     Command::new("cargo")
135         .arg("build")
136         .output()
137         .expect("Failed to build clippy!");
138 }
139
140 // get a list of KrateSources we want to check from a "crater_crates.toml" file.
141 fn read_crates() -> Vec<KrateSource> {
142     let toml_path = PathBuf::from("clippy_dev/crater_crates.toml");
143     let toml_content: String =
144         std::fs::read_to_string(&toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
145     let crate_list: CrateList =
146         toml::from_str(&toml_content).unwrap_or_else(|e| panic!("Failed to parse {}: \n{}", toml_path.display(), e));
147     // parse the hashmap of the toml file into a list of crates
148     crate_list
149         .crates
150         .iter()
151         .map(|(name, version)| KrateSource::new(&name, &version))
152         .collect()
153 }
154
155 // the main fn
156 pub fn run() {
157     let cargo_clippy_path: PathBuf = PathBuf::from("target/debug/cargo-clippy");
158
159     println!("Compiling clippy...");
160     build_clippy();
161     println!("Done compiling");
162
163     // assert that clippy is found
164     assert!(
165         cargo_clippy_path.is_file(),
166         "target/debug/cargo-clippy binary not found! {}",
167         cargo_clippy_path.display()
168     );
169
170     // download and extract the crates, then run clippy on them and collect clippys warnings
171
172     let clippy_lint_results: Vec<Vec<String>> = read_crates()
173         .into_iter()
174         .map(|krate| krate.download_and_extract())
175         .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
176         .collect();
177
178     let mut all_warnings: Vec<String> = clippy_lint_results.into_iter().flatten().collect();
179     all_warnings.sort();
180
181     // save the text into mini-crater/logs.txt
182     let text = all_warnings.join("");
183     write("mini-crater/logs.txt", text).unwrap();
184 }