summary refs log tree commit diff
path: root/src/tools/clippy/clippy_dev
diff options
context:
space:
mode:
authorflip1995 <hello@philkrones.com>2021-01-30 18:06:34 +0100
committerflip1995 <hello@philkrones.com>2021-01-30 18:06:34 +0100
commit21758bf1fbeb0993fe39daf073b4bf883d36e779 (patch)
tree24e6ce5e1552a540571c7f92c951254e1e0b2119 /src/tools/clippy/clippy_dev
parent7ce1b3b24491cbe10669cbe2b5733c2fe7cfe5b7 (diff)
parent95c0459217d1661edfa794c8bb122452b92fb485 (diff)
downloadrust-21758bf1fbeb0993fe39daf073b4bf883d36e779.tar.gz
rust-21758bf1fbeb0993fe39daf073b4bf883d36e779.zip
Merge commit '95c0459217d1661edfa794c8bb122452b92fb485' into clippyup
Diffstat (limited to 'src/tools/clippy/clippy_dev')
-rw-r--r--src/tools/clippy/clippy_dev/Cargo.toml8
-rw-r--r--src/tools/clippy/clippy_dev/lintcheck_crates.toml20
-rw-r--r--src/tools/clippy/clippy_dev/src/lib.rs1
-rw-r--r--src/tools/clippy/clippy_dev/src/lintcheck.rs286
-rw-r--r--src/tools/clippy/clippy_dev/src/main.rs28
-rw-r--r--src/tools/clippy/clippy_dev/src/ra_setup.rs2
6 files changed, 340 insertions, 5 deletions
diff --git a/src/tools/clippy/clippy_dev/Cargo.toml b/src/tools/clippy/clippy_dev/Cargo.toml
index b8a4a20114b..f48c1ee5ea2 100644
--- a/src/tools/clippy/clippy_dev/Cargo.toml
+++ b/src/tools/clippy/clippy_dev/Cargo.toml
@@ -4,14 +4,22 @@ version = "0.0.1"
 authors = ["Philipp Hansch <dev@phansch.net>"]
 edition = "2018"
 
+
 [dependencies]
 bytecount = "0.6"
 clap = "2.33"
+flate2 = { version = "1.0.19", optional = true }
 itertools = "0.9"
 opener = "0.4"
 regex = "1"
+serde = { version = "1.0", features = ["derive"], optional = true }
+serde_json = { version = "1.0", optional = true }
 shell-escape = "0.1"
+tar = { version = "0.4.30", optional = true }
+toml = { version = "0.5", optional = true }
+ureq = { version = "2.0.0-rc3", optional = true }
 walkdir = "2"
 
 [features]
+lintcheck = ["flate2", "serde_json", "tar", "toml", "ureq", "serde"]
 deny-warnings = []
diff --git a/src/tools/clippy/clippy_dev/lintcheck_crates.toml b/src/tools/clippy/clippy_dev/lintcheck_crates.toml
new file mode 100644
index 00000000000..1fbf7930d3e
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/lintcheck_crates.toml
@@ -0,0 +1,20 @@
+[crates]
+# some of these are from cargotest
+cargo = ['0.49.0']
+iron = ['0.6.1']
+ripgrep = ['12.1.1']
+xsv = ['0.13.0']
+#tokei = ['12.0.4']
+rayon = ['1.5.0']
+serde = ['1.0.118']
+# top 10 crates.io dls
+bitflags = ['1.2.1']
+libc = ['0.2.81']
+log = ['0.4.11']
+proc-macro2 = ['1.0.24']
+quote = ['1.0.7']
+rand = ['0.7.3']
+rand_core = ['0.6.0']
+regex = ['1.3.2']
+syn = ['1.0.54']
+unicode-xid = ['0.2.1']
diff --git a/src/tools/clippy/clippy_dev/src/lib.rs b/src/tools/clippy/clippy_dev/src/lib.rs
index 17cc08ee10f..24d70d433f3 100644
--- a/src/tools/clippy/clippy_dev/src/lib.rs
+++ b/src/tools/clippy/clippy_dev/src/lib.rs
@@ -12,6 +12,7 @@ use walkdir::WalkDir;
 
 pub mod bless;
 pub mod fmt;
+pub mod lintcheck;
 pub mod new_lint;
 pub mod ra_setup;
 pub mod serve;
diff --git a/src/tools/clippy/clippy_dev/src/lintcheck.rs b/src/tools/clippy/clippy_dev/src/lintcheck.rs
new file mode 100644
index 00000000000..785c692d3cb
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/lintcheck.rs
@@ -0,0 +1,286 @@
+// Run clippy on a fixed set of crates and collect the warnings.
+// This helps observing the impact clippy changs have on a set of real-world code.
+//
+// When a new lint is introduced, we can search the results for new warnings and check for false
+// positives.
+
+#![cfg(feature = "lintcheck")]
+#![allow(clippy::filter_map)]
+
+use crate::clippy_project_root;
+
+use std::collections::HashMap;
+use std::process::Command;
+use std::{fmt, fs::write, path::PathBuf};
+
+use clap::ArgMatches;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+// use this to store the crates when interacting with the crates.toml file
+#[derive(Debug, Serialize, Deserialize)]
+struct CrateList {
+    crates: HashMap<String, Vec<String>>,
+}
+
+// crate data we stored in the toml, can have multiple versions per crate
+// A single TomlCrate is laster mapped to several CrateSources in that case
+struct TomlCrate {
+    name: String,
+    versions: Vec<String>,
+}
+
+// represents an archive we download from crates.io
+#[derive(Debug, Serialize, Deserialize, Eq, Hash, PartialEq)]
+struct CrateSource {
+    name: String,
+    version: String,
+}
+
+// represents the extracted sourcecode of a crate
+#[derive(Debug)]
+struct Crate {
+    version: String,
+    name: String,
+    // path to the extracted sources that clippy can check
+    path: PathBuf,
+}
+
+#[derive(Debug)]
+struct ClippyWarning {
+    crate_name: String,
+    crate_version: String,
+    file: String,
+    line: String,
+    column: String,
+    linttype: String,
+    message: String,
+}
+
+impl std::fmt::Display for ClippyWarning {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        writeln!(
+            f,
+            r#"{}-{}/{}:{}:{} {} "{}""#,
+            &self.crate_name, &self.crate_version, &self.file, &self.line, &self.column, &self.linttype, &self.message
+        )
+    }
+}
+
+impl CrateSource {
+    fn download_and_extract(&self) -> Crate {
+        let extract_dir = PathBuf::from("target/lintcheck/crates");
+        let krate_download_dir = PathBuf::from("target/lintcheck/downloads");
+
+        // url to download the crate from crates.io
+        let url = format!(
+            "https://crates.io/api/v1/crates/{}/{}/download",
+            self.name, self.version
+        );
+        println!("Downloading and extracting {} {} from {}", self.name, self.version, url);
+        let _ = std::fs::create_dir("target/lintcheck/");
+        let _ = std::fs::create_dir(&krate_download_dir);
+        let _ = std::fs::create_dir(&extract_dir);
+
+        let krate_file_path = krate_download_dir.join(format!("{}-{}.crate.tar.gz", &self.name, &self.version));
+        // don't download/extract if we already have done so
+        if !krate_file_path.is_file() {
+            // create a file path to download and write the crate data into
+            let mut krate_dest = std::fs::File::create(&krate_file_path).unwrap();
+            let mut krate_req = ureq::get(&url).call().unwrap().into_reader();
+            // copy the crate into the file
+            std::io::copy(&mut krate_req, &mut krate_dest).unwrap();
+
+            // unzip the tarball
+            let ungz_tar = flate2::read::GzDecoder::new(std::fs::File::open(&krate_file_path).unwrap());
+            // extract the tar archive
+            let mut archive = tar::Archive::new(ungz_tar);
+            archive.unpack(&extract_dir).expect("Failed to extract!");
+        }
+        // crate is extracted, return a new Krate object which contains the path to the extracted
+        // sources that clippy can check
+        Crate {
+            version: self.version.clone(),
+            name: self.name.clone(),
+            path: extract_dir.join(format!("{}-{}/", self.name, self.version)),
+        }
+    }
+}
+
+impl Crate {
+    fn run_clippy_lints(&self, cargo_clippy_path: &PathBuf) -> Vec<ClippyWarning> {
+        println!("Linting {} {}...", &self.name, &self.version);
+        let cargo_clippy_path = std::fs::canonicalize(cargo_clippy_path).unwrap();
+
+        let shared_target_dir = clippy_project_root().join("target/lintcheck/shared_target_dir/");
+
+        let all_output = std::process::Command::new(cargo_clippy_path)
+            .env("CARGO_TARGET_DIR", shared_target_dir)
+            // lint warnings will look like this:
+            // src/cargo/ops/cargo_compile.rs:127:35: warning: usage of `FromIterator::from_iter`
+            .args(&[
+                "--",
+                "--message-format=json",
+                "--",
+                "--cap-lints=warn",
+                "-Wclippy::pedantic",
+                "-Wclippy::cargo",
+            ])
+            .current_dir(&self.path)
+            .output()
+            .unwrap();
+        let stdout = String::from_utf8_lossy(&all_output.stdout);
+        let output_lines = stdout.lines();
+        //dbg!(&output_lines);
+        let warnings: Vec<ClippyWarning> = output_lines
+            .into_iter()
+            // get all clippy warnings
+            .filter(|line| line.contains("clippy::"))
+            .map(|json_msg| parse_json_message(json_msg, &self))
+            .collect();
+        warnings
+    }
+}
+
+fn build_clippy() {
+    Command::new("cargo")
+        .arg("build")
+        .output()
+        .expect("Failed to build clippy!");
+}
+
+// get a list of CrateSources we want to check from a "lintcheck_crates.toml" file.
+fn read_crates() -> Vec<CrateSource> {
+    let toml_path = PathBuf::from("clippy_dev/lintcheck_crates.toml");
+    let toml_content: String =
+        std::fs::read_to_string(&toml_path).unwrap_or_else(|_| panic!("Failed to read {}", toml_path.display()));
+    let crate_list: CrateList =
+        toml::from_str(&toml_content).unwrap_or_else(|e| panic!("Failed to parse {}: \n{}", toml_path.display(), e));
+    // parse the hashmap of the toml file into a list of crates
+    let tomlcrates: Vec<TomlCrate> = crate_list
+        .crates
+        .into_iter()
+        .map(|(name, versions)| TomlCrate { name, versions })
+        .collect();
+
+    // flatten TomlCrates into CrateSources (one TomlCrates may represent several versions of a crate =>
+    // multiple Cratesources)
+    let mut crate_sources = Vec::new();
+    tomlcrates.into_iter().for_each(|tk| {
+        tk.versions.iter().for_each(|ver| {
+            crate_sources.push(CrateSource {
+                name: tk.name.clone(),
+                version: ver.to_string(),
+            });
+        })
+    });
+    crate_sources
+}
+
+// extract interesting data from a json lint message
+fn parse_json_message(json_message: &str, krate: &Crate) -> ClippyWarning {
+    let jmsg: Value = serde_json::from_str(&json_message).unwrap_or_else(|e| panic!("Failed to parse json:\n{:?}", e));
+
+    ClippyWarning {
+        crate_name: krate.name.to_string(),
+        crate_version: krate.version.to_string(),
+        file: jmsg["message"]["spans"][0]["file_name"]
+            .to_string()
+            .trim_matches('"')
+            .into(),
+        line: jmsg["message"]["spans"][0]["line_start"]
+            .to_string()
+            .trim_matches('"')
+            .into(),
+        column: jmsg["message"]["spans"][0]["text"][0]["highlight_start"]
+            .to_string()
+            .trim_matches('"')
+            .into(),
+        linttype: jmsg["message"]["code"]["code"].to_string().trim_matches('"').into(),
+        message: jmsg["message"]["message"].to_string().trim_matches('"').into(),
+    }
+}
+
+// the main fn
+pub fn run(clap_config: &ArgMatches) {
+    let cargo_clippy_path: PathBuf = PathBuf::from("target/debug/cargo-clippy");
+
+    println!("Compiling clippy...");
+    build_clippy();
+    println!("Done compiling");
+
+    // assert that clippy is found
+    assert!(
+        cargo_clippy_path.is_file(),
+        "target/debug/cargo-clippy binary not found! {}",
+        cargo_clippy_path.display()
+    );
+
+    let clippy_ver = std::process::Command::new("target/debug/cargo-clippy")
+        .arg("--version")
+        .output()
+        .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
+        .expect("could not get clippy version!");
+
+    // download and extract the crates, then run clippy on them and collect clippys warnings
+    // flatten into one big list of warnings
+
+    let crates = read_crates();
+
+    let clippy_warnings: Vec<ClippyWarning> = if let Some(only_one_crate) = clap_config.value_of("only") {
+        // if we don't have the specified crated in the .toml, throw an error
+        if !crates.iter().any(|krate| krate.name == only_one_crate) {
+            eprintln!(
+                "ERROR: could not find crate '{}' in clippy_dev/lintcheck_crates.toml",
+                only_one_crate
+            );
+            std::process::exit(1);
+        }
+
+        // only check a single crate that was passed via cmdline
+        crates
+            .into_iter()
+            .map(|krate| krate.download_and_extract())
+            .filter(|krate| krate.name == only_one_crate)
+            .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
+            .flatten()
+            .collect()
+    } else {
+        // check all crates (default)
+        crates
+            .into_iter()
+            .map(|krate| krate.download_and_extract())
+            .map(|krate| krate.run_clippy_lints(&cargo_clippy_path))
+            .flatten()
+            .collect()
+    };
+
+    // generate some stats:
+
+    // count lint type occurrences
+    let mut counter: HashMap<&String, usize> = HashMap::new();
+    clippy_warnings
+        .iter()
+        .for_each(|wrn| *counter.entry(&wrn.linttype).or_insert(0) += 1);
+
+    // collect into a tupled list for sorting
+    let mut stats: Vec<(&&String, &usize)> = counter.iter().map(|(lint, count)| (lint, count)).collect();
+    // sort by "000{count} {clippy::lintname}"
+    // to not have a lint with 200 and 2 warnings take the same spot
+    stats.sort_by_key(|(lint, count)| format!("{:0>4}, {}", count, lint));
+
+    let stats_formatted: String = stats
+        .iter()
+        .map(|(lint, count)| format!("{} {}\n", lint, count))
+        .collect::<String>();
+
+    let mut all_msgs: Vec<String> = clippy_warnings.iter().map(|warning| warning.to_string()).collect();
+    all_msgs.sort();
+    all_msgs.push("\n\n\n\nStats\n\n".into());
+    all_msgs.push(stats_formatted);
+
+    // save the text into lintcheck-logs/logs.txt
+    let mut text = clippy_ver; // clippy version number on top
+    text.push_str(&format!("\n{}", all_msgs.join("")));
+    write("lintcheck-logs/logs.txt", text).unwrap();
+}
diff --git a/src/tools/clippy/clippy_dev/src/main.rs b/src/tools/clippy/clippy_dev/src/main.rs
index 2ea56c42faf..e7a298a37e1 100644
--- a/src/tools/clippy/clippy_dev/src/main.rs
+++ b/src/tools/clippy/clippy_dev/src/main.rs
@@ -3,6 +3,9 @@
 use clap::{App, Arg, ArgMatches, SubCommand};
 use clippy_dev::{bless, fmt, new_lint, ra_setup, serve, stderr_length_check, update_lints};
 
+#[cfg(feature = "lintcheck")]
+use clippy_dev::lintcheck;
+
 fn main() {
     let matches = get_clap_config();
 
@@ -10,6 +13,10 @@ fn main() {
         ("bless", Some(matches)) => {
             bless::bless(matches.is_present("ignore-timestamp"));
         },
+        #[cfg(feature = "lintcheck")]
+        ("lintcheck", Some(matches)) => {
+            lintcheck::run(&matches);
+        },
         ("fmt", Some(matches)) => {
             fmt::run(matches.is_present("check"), matches.is_present("verbose"));
         },
@@ -46,7 +53,18 @@ fn main() {
 }
 
 fn get_clap_config<'a>() -> ArgMatches<'a> {
-    App::new("Clippy developer tooling")
+    #[cfg(feature = "lintcheck")]
+    let lintcheck_sbcmd = SubCommand::with_name("lintcheck")
+        .about("run clippy on a set of crates and check output")
+        .arg(
+            Arg::with_name("only")
+                .takes_value(true)
+                .value_name("CRATE")
+                .long("only")
+                .help("only process a single crate of the list"),
+        );
+
+    let app = App::new("Clippy developer tooling")
         .subcommand(
             SubCommand::with_name("bless")
                 .about("bless the test output changes")
@@ -163,6 +181,10 @@ fn get_clap_config<'a>() -> ArgMatches<'a> {
                         .validator_os(serve::validate_port),
                 )
                 .arg(Arg::with_name("lint").help("Which lint's page to load initially (optional)")),
-        )
-        .get_matches()
+        );
+
+    #[cfg(feature = "lintcheck")]
+    let app = app.subcommand(lintcheck_sbcmd);
+
+    app.get_matches()
 }
diff --git a/src/tools/clippy/clippy_dev/src/ra_setup.rs b/src/tools/clippy/clippy_dev/src/ra_setup.rs
index 5f5048e79e7..a8a6a2cb1bd 100644
--- a/src/tools/clippy/clippy_dev/src/ra_setup.rs
+++ b/src/tools/clippy/clippy_dev/src/ra_setup.rs
@@ -1,5 +1,3 @@
-#![allow(clippy::filter_map)]
-
 use std::fs;
 use std::fs::File;
 use std::io::prelude::*;