rust/lintcheck/src/main.rs

416 lines
14 KiB
Rust
Raw Normal View History

2020-12-23 13:02:02 +01:00
// Run clippy on a fixed set of crates and collect the warnings.
// This helps observing the impact clippy changes have on a set of real-world code (and not just our
// testsuite).
2020-12-23 13:02:02 +01:00
//
// When a new lint is introduced, we can search the results for new warnings and check for false
// positives.
#![feature(iter_collect_into)]
2024-03-18 21:28:43 +00:00
#![warn(
trivial_casts,
trivial_numeric_casts,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
#![allow(
clippy::collapsible_else_if,
clippy::needless_borrows_for_generic_args,
clippy::module_name_repetitions
)]
2020-12-18 22:53:45 +01:00
2022-05-07 22:10:56 +01:00
mod config;
mod driver;
mod input;
mod json;
mod output;
mod popular_crates;
mod recursive;
2022-05-07 22:10:56 +01:00
use crate::config::{Commands, LintcheckConfig, OutputFormat};
use crate::recursive::LintcheckServer;
2022-05-07 22:10:56 +01:00
use std::env::consts::EXE_SUFFIX;
use std::io::{self};
2022-05-07 22:10:56 +01:00
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{env, fs};
2022-05-07 22:10:56 +01:00
use cargo_metadata::Message;
use input::{read_crates, CrateSource};
use output::{ClippyCheckOutput, ClippyWarning, RustcIce};
use rayon::prelude::*;
2020-12-23 13:02:02 +01:00
const LINTCHECK_DOWNLOADS: &str = "target/lintcheck/downloads";
const LINTCHECK_SOURCES: &str = "target/lintcheck/sources";
/// Represents the actual source code of a crate that we ran "cargo clippy" on
#[derive(Debug)]
2020-12-23 13:02:02 +01:00
struct Crate {
version: String,
name: String,
// path to the extracted sources that clippy can check
path: PathBuf,
options: Option<Vec<String>>,
}
2020-12-23 13:02:02 +01:00
impl Crate {
/// Run `cargo clippy` on the `Crate` and collect and return all the lint warnings that clippy
/// issued
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn run_clippy_lints(
&self,
clippy_driver_path: &Path,
target_dir_index: &AtomicUsize,
total_crates_to_lint: usize,
2022-05-07 22:10:56 +01:00
config: &LintcheckConfig,
lint_levels_args: &[String],
server: &Option<LintcheckServer>,
) -> Vec<ClippyCheckOutput> {
// advance the atomic index by one
let index = target_dir_index.fetch_add(1, Ordering::SeqCst);
// "loop" the index within 0..thread_limit
2022-05-07 22:10:56 +01:00
let thread_index = index % config.max_jobs;
2021-03-05 11:10:15 +01:00
let perc = (index * 100) / total_crates_to_lint;
2022-05-07 22:10:56 +01:00
if config.max_jobs == 1 {
println!(
"{index}/{total_crates_to_lint} {perc}% Linting {} {}",
&self.name, &self.version
);
} else {
println!(
"{index}/{total_crates_to_lint} {perc}% Linting {} {} in target dir {thread_index:?}",
&self.name, &self.version
);
}
let shared_target_dir = clippy_project_root().join("target/lintcheck/shared_target_dir");
let cargo_home = env!("CARGO_HOME");
// `src/lib.rs` -> `target/lintcheck/sources/crate-1.2.3/src/lib.rs`
let remap_relative = format!("={}", self.path.display());
// Fallback for other sources, `~/.cargo/...` -> `$CARGO_HOME/...`
let remap_cargo_home = format!("{cargo_home}=$CARGO_HOME");
2024-06-22 12:33:51 +00:00
// `~/.cargo/registry/src/index.crates.io-6f17d22bba15001f/crate-2.3.4/src/lib.rs`
// -> `crate-2.3.4/src/lib.rs`
2024-06-22 12:33:51 +00:00
let remap_crates_io = format!("{cargo_home}/registry/src/index.crates.io-6f17d22bba15001f/=");
let mut clippy_args = vec![
"--remap-path-prefix",
&remap_relative,
"--remap-path-prefix",
&remap_cargo_home,
"--remap-path-prefix",
&remap_crates_io,
];
if let Some(options) = &self.options {
for opt in options {
clippy_args.push(opt);
}
}
clippy_args.extend(lint_levels_args.iter().map(String::as_str));
2024-06-22 12:33:51 +00:00
let mut cmd = Command::new("cargo");
cmd.arg(if config.fix { "fix" } else { "check" })
.arg("--quiet")
.current_dir(&self.path)
.env("CLIPPY_ARGS", clippy_args.join("__CLIPPY_HACKERY__"));
2024-06-22 12:33:51 +00:00
if let Some(server) = server {
// `cargo clippy` is a wrapper around `cargo check` that mainly sets `RUSTC_WORKSPACE_WRAPPER` to
// `clippy-driver`. We do the same thing here with a couple changes:
//
// `RUSTC_WRAPPER` is used instead of `RUSTC_WORKSPACE_WRAPPER` so that we can lint all crate
// dependencies rather than only workspace members
//
2024-06-22 12:33:51 +00:00
// The wrapper is set to `lintcheck` itself so we can force enable linting and ignore certain crates
// (see `crate::driver`)
2024-06-22 12:33:51 +00:00
let status = cmd
.env("CARGO_TARGET_DIR", shared_target_dir.join("recursive"))
.env("RUSTC_WRAPPER", env::current_exe().unwrap())
// Pass the absolute path so `crate::driver` can find `clippy-driver`, as it's executed in various
// different working directories
.env("CLIPPY_DRIVER", clippy_driver_path)
.env("LINTCHECK_SERVER", server.local_addr.to_string())
.status()
.expect("failed to run cargo");
assert_eq!(status.code(), Some(0));
return Vec::new();
2024-06-22 12:33:51 +00:00
};
2021-12-08 22:24:06 +01:00
2024-06-22 12:33:51 +00:00
if !config.fix {
cmd.arg("--message-format=json");
}
2024-06-22 12:33:51 +00:00
let all_output = cmd
// use the looping index to create individual target dirs
2022-10-22 07:37:23 -04:00
.env("CARGO_TARGET_DIR", shared_target_dir.join(format!("_{thread_index:?}")))
2024-06-22 12:33:51 +00:00
// Roughly equivalent to `cargo clippy`/`cargo clippy --fix`
.env("RUSTC_WORKSPACE_WRAPPER", clippy_driver_path)
2020-12-18 18:34:09 +01:00
.output()
2024-06-22 12:33:51 +00:00
.unwrap();
let stdout = String::from_utf8_lossy(&all_output.stdout);
let stderr = String::from_utf8_lossy(&all_output.stderr);
let status = &all_output.status;
if !status.success() {
eprintln!(
"\nWARNING: bad exit status after checking {} {} \n",
self.name, self.version
);
}
2022-05-07 22:10:56 +01:00
if config.fix {
if let Some(stderr) = stderr
.lines()
.find(|line| line.contains("failed to automatically apply fixes suggested by rustc to crate"))
{
let subcrate = &stderr[63..];
println!(
"ERROR: failed to apply some suggestion to {} / to (sub)crate {subcrate}",
2022-10-22 07:37:23 -04:00
self.name
);
}
// fast path, we don't need the warnings anyway
return Vec::new();
}
2022-05-07 22:10:56 +01:00
// get all clippy warnings and ICEs
let mut entries: Vec<ClippyCheckOutput> = Message::parse_stream(stdout.as_bytes())
.filter_map(|msg| match msg {
Ok(Message::CompilerMessage(message)) => ClippyWarning::new(message.message),
_ => None,
})
.map(ClippyCheckOutput::ClippyWarning)
2020-12-18 18:34:09 +01:00
.collect();
if let Some(ice) = RustcIce::from_stderr_and_status(&self.name, *status, &stderr) {
entries.push(ClippyCheckOutput::RustcIce(ice));
} else if !status.success() {
println!("non-ICE bad exit status for {} {}: {}", self.name, self.version, stderr);
}
entries
}
}
/// Builds clippy inside the repo to make sure we have a clippy executable we can use.
2024-06-22 12:33:51 +00:00
fn build_clippy() -> String {
let output = Command::new("cargo")
.args(["run", "--bin=clippy-driver", "--", "--version"])
.stderr(Stdio::inherit())
.output()
.unwrap();
if !output.status.success() {
eprintln!("Error: Failed to compile Clippy!");
std::process::exit(1);
}
2024-06-22 12:33:51 +00:00
String::from_utf8_lossy(&output.stdout).into_owned()
}
2022-05-07 22:10:56 +01:00
fn main() {
// We're being executed as a `RUSTC_WRAPPER` as part of `--recursive`
if let Ok(addr) = env::var("LINTCHECK_SERVER") {
driver::drive(&addr);
}
2021-03-11 15:25:43 +01:00
// assert that we launch lintcheck from the repo root (via cargo lintcheck)
2024-03-18 21:28:43 +00:00
if fs::metadata("lintcheck/Cargo.toml").is_err() {
2022-07-09 13:00:24 +03:00
eprintln!("lintcheck needs to be run from clippy's repo root!\nUse `cargo lintcheck` alternatively.");
std::process::exit(3);
}
2022-05-07 22:10:56 +01:00
let config = LintcheckConfig::new();
match config.subcommand {
Some(Commands::Diff { old, new }) => json::diff(&old, &new),
Some(Commands::Popular { output, number }) => popular_crates::fetch(output, number).unwrap(),
None => lintcheck(config),
}
}
#[allow(clippy::too_many_lines)]
fn lintcheck(config: LintcheckConfig) {
2024-06-22 12:33:51 +00:00
let clippy_ver = build_clippy();
let clippy_driver_path = fs::canonicalize(format!("target/debug/clippy-driver{EXE_SUFFIX}")).unwrap();
// assert that clippy is found
assert!(
2024-06-22 12:33:51 +00:00
clippy_driver_path.is_file(),
"target/debug/clippy-driver binary not found! {}",
clippy_driver_path.display()
);
2022-07-09 13:00:24 +03:00
// download and extract the crates, then run clippy on them and collect clippy's warnings
// flatten into one big list of warnings
let (crates, recursive_options) = read_crates(&config.sources_toml_path);
let counter = AtomicUsize::new(1);
let mut lint_level_args: Vec<String> = vec![];
if config.lint_filter.is_empty() {
lint_level_args.push("--cap-lints=warn".to_string());
// Set allow-by-default to warn
if config.warn_all {
[
"clippy::cargo",
"clippy::nursery",
"clippy::pedantic",
"clippy::restriction",
]
.iter()
.map(|group| format!("--warn={group}"))
.collect_into(&mut lint_level_args);
} else {
["clippy::cargo", "clippy::pedantic"]
.iter()
.map(|group| format!("--warn={group}"))
.collect_into(&mut lint_level_args);
}
} else {
lint_level_args.push("--cap-lints=allow".to_string());
config
.lint_filter
.iter()
.map(|filter| {
let mut filter = filter.clone();
filter.insert_str(0, "--force-warn=");
filter
})
.collect_into(&mut lint_level_args);
};
2022-05-07 22:10:56 +01:00
let crates: Vec<Crate> = crates
.into_iter()
.filter(|krate| {
if let Some(only_one_crate) = &config.only {
let name = match krate {
CrateSource::CratesIo { name, .. }
| CrateSource::Git { name, .. }
| CrateSource::Path { name, .. } => name,
};
2022-05-07 22:10:56 +01:00
name == only_one_crate
} else {
true
}
})
.map(|krate| krate.download_and_extract())
.collect();
if crates.is_empty() {
eprintln!(
"ERROR: could not find crate '{}' in lintcheck/lintcheck_crates.toml",
config.only.unwrap(),
);
std::process::exit(1);
}
// run parallel with rayon
// This helps when we check many small crates with dep-trees that don't have a lot of branches in
// order to achieve some kind of parallelism
rayon::ThreadPoolBuilder::new()
.num_threads(config.max_jobs)
.build_global()
.unwrap();
let server = config.recursive.then(|| {
let _: io::Result<()> = fs::remove_dir_all("target/lintcheck/shared_target_dir/recursive");
LintcheckServer::spawn(recursive_options)
});
let mut clippy_entries: Vec<ClippyCheckOutput> = crates
2022-05-07 22:10:56 +01:00
.par_iter()
.flat_map(|krate| {
krate.run_clippy_lints(
&clippy_driver_path,
&counter,
crates.len(),
&config,
&lint_level_args,
&server,
)
})
2022-05-07 22:10:56 +01:00
.collect();
2020-12-18 18:34:09 +01:00
if let Some(server) = server {
let server_clippy_entries = server.warnings().map(ClippyCheckOutput::ClippyWarning);
clippy_entries.extend(server_clippy_entries);
}
// if we are in --fix mode, don't change the log files, terminate here
if config.fix {
return;
}
// split up warnings and ices
let mut warnings: Vec<ClippyWarning> = vec![];
let mut raw_ices: Vec<RustcIce> = vec![];
for entry in clippy_entries {
if let ClippyCheckOutput::ClippyWarning(x) = entry {
warnings.push(x);
} else if let ClippyCheckOutput::RustcIce(x) = entry {
raw_ices.push(x);
}
}
let text = match config.format {
OutputFormat::Text | OutputFormat::Markdown => {
output::summarize_and_print_changes(&warnings, &raw_ices, clippy_ver, &config)
},
OutputFormat::Json => {
if !raw_ices.is_empty() {
for ice in raw_ices {
println!("{ice}");
}
panic!("Some crates ICEd");
}
json::output(warnings)
},
};
println!("Writing logs to {}", config.lintcheck_results_path.display());
fs::create_dir_all(config.lintcheck_results_path.parent().unwrap()).unwrap();
fs::write(&config.lintcheck_results_path, text).unwrap();
}
/// Returns the path to the Clippy project directory
#[must_use]
2022-05-07 22:10:56 +01:00
fn clippy_project_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap()
}
2021-03-04 22:33:22 +01:00
#[test]
fn lintcheck_test() {
let args = [
"run",
"--target-dir",
"lintcheck/target",
2021-03-04 22:33:22 +01:00
"--manifest-path",
"./lintcheck/Cargo.toml",
2021-03-04 22:33:22 +01:00
"--",
"--crates-toml",
"lintcheck/test_sources.toml",
2021-03-04 22:33:22 +01:00
];
2024-06-22 12:33:51 +00:00
let status = Command::new("cargo")
2022-10-07 05:07:09 -04:00
.args(args)
.current_dir("..") // repo root
2021-03-04 22:33:22 +01:00
.status();
//.output();
2021-03-04 22:33:22 +01:00
assert!(status.unwrap().success());
}