summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_dev
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /src/tools/clippy/clippy_dev
parentInitial commit. (diff)
downloadrustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz
rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/clippy/clippy_dev')
-rw-r--r--src/tools/clippy/clippy_dev/Cargo.toml21
-rw-r--r--src/tools/clippy/clippy_dev/src/bless.rs60
-rw-r--r--src/tools/clippy/clippy_dev/src/dogfood.rs33
-rw-r--r--src/tools/clippy/clippy_dev/src/fmt.rs226
-rw-r--r--src/tools/clippy/clippy_dev/src/lib.rs58
-rw-r--r--src/tools/clippy/clippy_dev/src/lint.rs55
-rw-r--r--src/tools/clippy/clippy_dev/src/main.rs314
-rw-r--r--src/tools/clippy/clippy_dev/src/new_lint.rs575
-rw-r--r--src/tools/clippy/clippy_dev/src/serve.rs65
-rw-r--r--src/tools/clippy/clippy_dev/src/setup/git_hook.rs85
-rw-r--r--src/tools/clippy/clippy_dev/src/setup/intellij.rs223
-rw-r--r--src/tools/clippy/clippy_dev/src/setup/mod.rs23
-rw-r--r--src/tools/clippy/clippy_dev/src/setup/vscode.rs104
-rw-r--r--src/tools/clippy/clippy_dev/src/update_lints.rs1277
14 files changed, 3119 insertions, 0 deletions
diff --git a/src/tools/clippy/clippy_dev/Cargo.toml b/src/tools/clippy/clippy_dev/Cargo.toml
new file mode 100644
index 000000000..2ac3b4fe2
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "clippy_dev"
+version = "0.0.1"
+edition = "2021"
+
+[dependencies]
+aho-corasick = "0.7"
+clap = "3.2"
+indoc = "1.0"
+itertools = "0.10.1"
+opener = "0.5"
+shell-escape = "0.1"
+tempfile = "3.2"
+walkdir = "2.3"
+
+[features]
+deny-warnings = []
+
+[package.metadata.rust-analyzer]
+# This package uses #[feature(rustc_private)]
+rustc_private = true
diff --git a/src/tools/clippy/clippy_dev/src/bless.rs b/src/tools/clippy/clippy_dev/src/bless.rs
new file mode 100644
index 000000000..f5c51b947
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/bless.rs
@@ -0,0 +1,60 @@
+//! `bless` updates the reference files in the repo with changed output files
+//! from the last test run.
+
+use crate::cargo_clippy_path;
+use std::ffi::OsStr;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::LazyLock;
+use walkdir::{DirEntry, WalkDir};
+
+static CLIPPY_BUILD_TIME: LazyLock<Option<std::time::SystemTime>> =
+ LazyLock::new(|| cargo_clippy_path().metadata().ok()?.modified().ok());
+
+/// # Panics
+///
+/// Panics if the path to a test file is broken
+pub fn bless(ignore_timestamp: bool) {
+ let extensions = ["stdout", "stderr", "fixed"].map(OsStr::new);
+
+ WalkDir::new(build_dir())
+ .into_iter()
+ .map(Result::unwrap)
+ .filter(|entry| entry.path().extension().map_or(false, |ext| extensions.contains(&ext)))
+ .for_each(|entry| update_reference_file(&entry, ignore_timestamp));
+}
+
+fn update_reference_file(test_output_entry: &DirEntry, ignore_timestamp: bool) {
+ let test_output_path = test_output_entry.path();
+
+ let reference_file_name = test_output_entry.file_name().to_str().unwrap().replace(".stage-id", "");
+ let reference_file_path = Path::new("tests")
+ .join(test_output_path.strip_prefix(build_dir()).unwrap())
+ .with_file_name(reference_file_name);
+
+ // If the test output was not updated since the last clippy build, it may be outdated
+ if !ignore_timestamp && !updated_since_clippy_build(test_output_entry).unwrap_or(true) {
+ return;
+ }
+
+ let test_output_file = fs::read(&test_output_path).expect("Unable to read test output file");
+ let reference_file = fs::read(&reference_file_path).unwrap_or_default();
+
+ if test_output_file != reference_file {
+ // If a test run caused an output file to change, update the reference file
+ println!("updating {}", reference_file_path.display());
+ fs::copy(test_output_path, &reference_file_path).expect("Could not update reference file");
+ }
+}
+
+fn updated_since_clippy_build(entry: &DirEntry) -> Option<bool> {
+ let clippy_build_time = (*CLIPPY_BUILD_TIME)?;
+ let modified = entry.metadata().ok()?.modified().ok()?;
+ Some(modified >= clippy_build_time)
+}
+
+fn build_dir() -> PathBuf {
+ let mut path = std::env::current_exe().unwrap();
+ path.set_file_name("test");
+ path
+}
diff --git a/src/tools/clippy/clippy_dev/src/dogfood.rs b/src/tools/clippy/clippy_dev/src/dogfood.rs
new file mode 100644
index 000000000..b69e9f649
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/dogfood.rs
@@ -0,0 +1,33 @@
+use crate::clippy_project_root;
+use std::process::Command;
+
+/// # Panics
+///
+/// Panics if unable to run the dogfood test
+pub fn dogfood(fix: bool, allow_dirty: bool, allow_staged: bool) {
+ let mut cmd = Command::new("cargo");
+
+ cmd.current_dir(clippy_project_root())
+ .args(["test", "--test", "dogfood"])
+ .args(["--features", "internal"])
+ .args(["--", "dogfood_clippy"]);
+
+ let mut dogfood_args = Vec::new();
+ if fix {
+ dogfood_args.push("--fix");
+ }
+
+ if allow_dirty {
+ dogfood_args.push("--allow-dirty");
+ }
+
+ if allow_staged {
+ dogfood_args.push("--allow-staged");
+ }
+
+ cmd.env("__CLIPPY_DOGFOOD_ARGS", dogfood_args.join(" "));
+
+ let output = cmd.output().expect("failed to run command");
+
+ println!("{}", String::from_utf8_lossy(&output.stdout));
+}
diff --git a/src/tools/clippy/clippy_dev/src/fmt.rs b/src/tools/clippy/clippy_dev/src/fmt.rs
new file mode 100644
index 000000000..3b27f061e
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/fmt.rs
@@ -0,0 +1,226 @@
+use crate::clippy_project_root;
+use itertools::Itertools;
+use shell_escape::escape;
+use std::ffi::{OsStr, OsString};
+use std::path::Path;
+use std::process::{self, Command, Stdio};
+use std::{fs, io};
+use walkdir::WalkDir;
+
+#[derive(Debug)]
+pub enum CliError {
+ CommandFailed(String, String),
+ IoError(io::Error),
+ RustfmtNotInstalled,
+ WalkDirError(walkdir::Error),
+ IntellijSetupActive,
+}
+
+impl From<io::Error> for CliError {
+ fn from(error: io::Error) -> Self {
+ Self::IoError(error)
+ }
+}
+
+impl From<walkdir::Error> for CliError {
+ fn from(error: walkdir::Error) -> Self {
+ Self::WalkDirError(error)
+ }
+}
+
+struct FmtContext {
+ check: bool,
+ verbose: bool,
+ rustfmt_path: String,
+}
+
+// the "main" function of cargo dev fmt
+pub fn run(check: bool, verbose: bool) {
+ fn try_run(context: &FmtContext) -> Result<bool, CliError> {
+ let mut success = true;
+
+ let project_root = clippy_project_root();
+
+ // if we added a local rustc repo as path dependency to clippy for rust analyzer, we do NOT want to
+ // format because rustfmt would also format the entire rustc repo as it is a local
+ // dependency
+ if fs::read_to_string(project_root.join("Cargo.toml"))
+ .expect("Failed to read clippy Cargo.toml")
+ .contains(&"[target.'cfg(NOT_A_PLATFORM)'.dependencies]")
+ {
+ return Err(CliError::IntellijSetupActive);
+ }
+
+ rustfmt_test(context)?;
+
+ success &= cargo_fmt(context, project_root.as_path())?;
+ success &= cargo_fmt(context, &project_root.join("clippy_dev"))?;
+ success &= cargo_fmt(context, &project_root.join("rustc_tools_util"))?;
+ success &= cargo_fmt(context, &project_root.join("lintcheck"))?;
+
+ let chunks = WalkDir::new(project_root.join("tests"))
+ .into_iter()
+ .filter_map(|entry| {
+ let entry = entry.expect("failed to find tests");
+ let path = entry.path();
+
+ if path.extension() != Some("rs".as_ref()) || entry.file_name() == "ice-3891.rs" {
+ None
+ } else {
+ Some(entry.into_path().into_os_string())
+ }
+ })
+ .chunks(250);
+
+ for chunk in &chunks {
+ success &= rustfmt(context, chunk)?;
+ }
+
+ Ok(success)
+ }
+
+ fn output_err(err: CliError) {
+ match err {
+ CliError::CommandFailed(command, stderr) => {
+ eprintln!("error: A command failed! `{}`\nstderr: {}", command, stderr);
+ },
+ CliError::IoError(err) => {
+ eprintln!("error: {}", err);
+ },
+ CliError::RustfmtNotInstalled => {
+ eprintln!("error: rustfmt nightly is not installed.");
+ },
+ CliError::WalkDirError(err) => {
+ eprintln!("error: {}", err);
+ },
+ CliError::IntellijSetupActive => {
+ eprintln!(
+ "error: a local rustc repo is enabled as path dependency via `cargo dev setup intellij`.
+Not formatting because that would format the local repo as well!
+Please revert the changes to Cargo.tomls with `cargo dev remove intellij`."
+ );
+ },
+ }
+ }
+
+ let output = Command::new("rustup")
+ .args(["which", "rustfmt"])
+ .stderr(Stdio::inherit())
+ .output()
+ .expect("error running `rustup which rustfmt`");
+ if !output.status.success() {
+ eprintln!("`rustup which rustfmt` did not execute successfully");
+ process::exit(1);
+ }
+ let mut rustfmt_path = String::from_utf8(output.stdout).expect("invalid rustfmt path");
+ rustfmt_path.truncate(rustfmt_path.trim_end().len());
+
+ let context = FmtContext {
+ check,
+ verbose,
+ rustfmt_path,
+ };
+ let result = try_run(&context);
+ let code = match result {
+ Ok(true) => 0,
+ Ok(false) => {
+ eprintln!();
+ eprintln!("Formatting check failed.");
+ eprintln!("Run `cargo dev fmt` to update formatting.");
+ 1
+ },
+ Err(err) => {
+ output_err(err);
+ 1
+ },
+ };
+ process::exit(code);
+}
+
+fn format_command(program: impl AsRef<OsStr>, dir: impl AsRef<Path>, args: &[impl AsRef<OsStr>]) -> String {
+ let arg_display: Vec<_> = args.iter().map(|a| escape(a.as_ref().to_string_lossy())).collect();
+
+ format!(
+ "cd {} && {} {}",
+ escape(dir.as_ref().to_string_lossy()),
+ escape(program.as_ref().to_string_lossy()),
+ arg_display.join(" ")
+ )
+}
+
+fn exec(
+ context: &FmtContext,
+ program: impl AsRef<OsStr>,
+ dir: impl AsRef<Path>,
+ args: &[impl AsRef<OsStr>],
+) -> Result<bool, CliError> {
+ if context.verbose {
+ println!("{}", format_command(&program, &dir, args));
+ }
+
+ let output = Command::new(&program)
+ .env("RUSTFMT", &context.rustfmt_path)
+ .current_dir(&dir)
+ .args(args.iter())
+ .output()
+ .unwrap();
+ let success = output.status.success();
+
+ if !context.check && !success {
+ let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
+ return Err(CliError::CommandFailed(
+ format_command(&program, &dir, args),
+ String::from(stderr),
+ ));
+ }
+
+ Ok(success)
+}
+
+fn cargo_fmt(context: &FmtContext, path: &Path) -> Result<bool, CliError> {
+ let mut args = vec!["fmt", "--all"];
+ if context.check {
+ args.push("--check");
+ }
+ let success = exec(context, "cargo", path, &args)?;
+
+ Ok(success)
+}
+
+fn rustfmt_test(context: &FmtContext) -> Result<(), CliError> {
+ let program = "rustfmt";
+ let dir = std::env::current_dir()?;
+ let args = &["--version"];
+
+ if context.verbose {
+ println!("{}", format_command(&program, &dir, args));
+ }
+
+ let output = Command::new(&program).current_dir(&dir).args(args.iter()).output()?;
+
+ if output.status.success() {
+ Ok(())
+ } else if std::str::from_utf8(&output.stderr)
+ .unwrap_or("")
+ .starts_with("error: 'rustfmt' is not installed")
+ {
+ Err(CliError::RustfmtNotInstalled)
+ } else {
+ Err(CliError::CommandFailed(
+ format_command(&program, &dir, args),
+ std::str::from_utf8(&output.stderr).unwrap_or("").to_string(),
+ ))
+ }
+}
+
+fn rustfmt(context: &FmtContext, paths: impl Iterator<Item = OsString>) -> Result<bool, CliError> {
+ let mut args = Vec::new();
+ if context.check {
+ args.push(OsString::from("--check"));
+ }
+ args.extend(paths);
+
+ let success = exec(context, &context.rustfmt_path, std::env::current_dir()?, &args)?;
+
+ Ok(success)
+}
diff --git a/src/tools/clippy/clippy_dev/src/lib.rs b/src/tools/clippy/clippy_dev/src/lib.rs
new file mode 100644
index 000000000..82574a8e6
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/lib.rs
@@ -0,0 +1,58 @@
+#![feature(let_chains)]
+#![feature(let_else)]
+#![feature(once_cell)]
+#![feature(rustc_private)]
+#![cfg_attr(feature = "deny-warnings", deny(warnings))]
+// warn on lints, that are included in `rust-lang/rust`s bootstrap
+#![warn(rust_2018_idioms, unused_lifetimes)]
+
+extern crate rustc_lexer;
+
+use std::path::PathBuf;
+
+pub mod bless;
+pub mod dogfood;
+pub mod fmt;
+pub mod lint;
+pub mod new_lint;
+pub mod serve;
+pub mod setup;
+pub mod update_lints;
+
+#[cfg(not(windows))]
+static CARGO_CLIPPY_EXE: &str = "cargo-clippy";
+#[cfg(windows)]
+static CARGO_CLIPPY_EXE: &str = "cargo-clippy.exe";
+
+/// Returns the path to the `cargo-clippy` binary
+#[must_use]
+pub fn cargo_clippy_path() -> PathBuf {
+ let mut path = std::env::current_exe().expect("failed to get current executable name");
+ path.set_file_name(CARGO_CLIPPY_EXE);
+ path
+}
+
+/// Returns the path to the Clippy project directory
+///
+/// # Panics
+///
+/// Panics if the current directory could not be retrieved, there was an error reading any of the
+/// Cargo.toml files or ancestor directory is the clippy root directory
+#[must_use]
+pub fn clippy_project_root() -> PathBuf {
+ let current_dir = std::env::current_dir().unwrap();
+ for path in current_dir.ancestors() {
+ let result = std::fs::read_to_string(path.join("Cargo.toml"));
+ if let Err(err) = &result {
+ if err.kind() == std::io::ErrorKind::NotFound {
+ continue;
+ }
+ }
+
+ let content = result.unwrap();
+ if content.contains("[package]\nname = \"clippy\"") {
+ return path.to_path_buf();
+ }
+ }
+ panic!("error: Can't determine root of project. Please run inside a Clippy working dir.");
+}
diff --git a/src/tools/clippy/clippy_dev/src/lint.rs b/src/tools/clippy/clippy_dev/src/lint.rs
new file mode 100644
index 000000000..71005449b
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/lint.rs
@@ -0,0 +1,55 @@
+use crate::cargo_clippy_path;
+use std::process::{self, Command, ExitStatus};
+use std::{fs, io};
+
+fn exit_if_err(status: io::Result<ExitStatus>) {
+ match status.expect("failed to run command").code() {
+ Some(0) => {},
+ Some(n) => process::exit(n),
+ None => {
+ eprintln!("Killed by signal");
+ process::exit(1);
+ },
+ }
+}
+
+pub fn run<'a>(path: &str, args: impl Iterator<Item = &'a String>) {
+ let is_file = match fs::metadata(path) {
+ Ok(metadata) => metadata.is_file(),
+ Err(e) => {
+ eprintln!("Failed to read {path}: {e:?}");
+ process::exit(1);
+ },
+ };
+
+ if is_file {
+ exit_if_err(
+ Command::new("cargo")
+ .args(["run", "--bin", "clippy-driver", "--"])
+ .args(["-L", "./target/debug"])
+ .args(["-Z", "no-codegen"])
+ .args(["--edition", "2021"])
+ .arg(path)
+ .args(args)
+ .status(),
+ );
+ } else {
+ exit_if_err(Command::new("cargo").arg("build").status());
+
+ // Run in a tempdir as changes to clippy do not retrigger linting
+ let target = tempfile::Builder::new()
+ .prefix("clippy")
+ .tempdir()
+ .expect("failed to create tempdir");
+
+ let status = Command::new(cargo_clippy_path())
+ .arg("clippy")
+ .args(args)
+ .current_dir(path)
+ .env("CARGO_TARGET_DIR", target.as_ref())
+ .status();
+
+ target.close().expect("failed to remove tempdir");
+ exit_if_err(status);
+ }
+}
diff --git a/src/tools/clippy/clippy_dev/src/main.rs b/src/tools/clippy/clippy_dev/src/main.rs
new file mode 100644
index 000000000..a417d3dd8
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/main.rs
@@ -0,0 +1,314 @@
+#![cfg_attr(feature = "deny-warnings", deny(warnings))]
+// warn on lints, that are included in `rust-lang/rust`s bootstrap
+#![warn(rust_2018_idioms, unused_lifetimes)]
+
+use clap::{Arg, ArgAction, ArgMatches, Command, PossibleValue};
+use clippy_dev::{bless, dogfood, fmt, lint, new_lint, serve, setup, update_lints};
+use indoc::indoc;
+
+fn main() {
+ let matches = get_clap_config();
+
+ match matches.subcommand() {
+ Some(("bless", matches)) => {
+ bless::bless(matches.contains_id("ignore-timestamp"));
+ },
+ Some(("dogfood", matches)) => {
+ dogfood::dogfood(
+ matches.contains_id("fix"),
+ matches.contains_id("allow-dirty"),
+ matches.contains_id("allow-staged"),
+ );
+ },
+ Some(("fmt", matches)) => {
+ fmt::run(matches.contains_id("check"), matches.contains_id("verbose"));
+ },
+ Some(("update_lints", matches)) => {
+ if matches.contains_id("print-only") {
+ update_lints::print_lints();
+ } else if matches.contains_id("check") {
+ update_lints::update(update_lints::UpdateMode::Check);
+ } else {
+ update_lints::update(update_lints::UpdateMode::Change);
+ }
+ },
+ Some(("new_lint", matches)) => {
+ match new_lint::create(
+ matches.get_one::<String>("pass"),
+ matches.get_one::<String>("name"),
+ matches.get_one::<String>("category").map(String::as_str),
+ matches.get_one::<String>("type").map(String::as_str),
+ matches.contains_id("msrv"),
+ ) {
+ Ok(_) => update_lints::update(update_lints::UpdateMode::Change),
+ Err(e) => eprintln!("Unable to create lint: {}", e),
+ }
+ },
+ Some(("setup", sub_command)) => match sub_command.subcommand() {
+ Some(("intellij", matches)) => {
+ if matches.contains_id("remove") {
+ setup::intellij::remove_rustc_src();
+ } else {
+ setup::intellij::setup_rustc_src(
+ matches
+ .get_one::<String>("rustc-repo-path")
+ .expect("this field is mandatory and therefore always valid"),
+ );
+ }
+ },
+ Some(("git-hook", matches)) => {
+ if matches.contains_id("remove") {
+ setup::git_hook::remove_hook();
+ } else {
+ setup::git_hook::install_hook(matches.contains_id("force-override"));
+ }
+ },
+ Some(("vscode-tasks", matches)) => {
+ if matches.contains_id("remove") {
+ setup::vscode::remove_tasks();
+ } else {
+ setup::vscode::install_tasks(matches.contains_id("force-override"));
+ }
+ },
+ _ => {},
+ },
+ Some(("remove", sub_command)) => match sub_command.subcommand() {
+ Some(("git-hook", _)) => setup::git_hook::remove_hook(),
+ Some(("intellij", _)) => setup::intellij::remove_rustc_src(),
+ Some(("vscode-tasks", _)) => setup::vscode::remove_tasks(),
+ _ => {},
+ },
+ Some(("serve", matches)) => {
+ let port = *matches.get_one::<u16>("port").unwrap();
+ let lint = matches.get_one::<String>("lint");
+ serve::run(port, lint);
+ },
+ Some(("lint", matches)) => {
+ let path = matches.get_one::<String>("path").unwrap();
+ let args = matches.get_many::<String>("args").into_iter().flatten();
+ lint::run(path, args);
+ },
+ Some(("rename_lint", matches)) => {
+ let old_name = matches.get_one::<String>("old_name").unwrap();
+ let new_name = matches.get_one::<String>("new_name").unwrap_or(old_name);
+ let uplift = matches.contains_id("uplift");
+ update_lints::rename(old_name, new_name, uplift);
+ },
+ Some(("deprecate", matches)) => {
+ let name = matches.get_one::<String>("name").unwrap();
+ let reason = matches.get_one("reason");
+ update_lints::deprecate(name, reason);
+ },
+ _ => {},
+ }
+}
+
+fn get_clap_config() -> ArgMatches {
+ Command::new("Clippy developer tooling")
+ .arg_required_else_help(true)
+ .subcommands([
+ Command::new("bless").about("bless the test output changes").arg(
+ Arg::new("ignore-timestamp")
+ .long("ignore-timestamp")
+ .help("Include files updated before clippy was built"),
+ ),
+ Command::new("dogfood").about("Runs the dogfood test").args([
+ Arg::new("fix").long("fix").help("Apply the suggestions when possible"),
+ Arg::new("allow-dirty")
+ .long("allow-dirty")
+ .help("Fix code even if the working directory has changes")
+ .requires("fix"),
+ Arg::new("allow-staged")
+ .long("allow-staged")
+ .help("Fix code even if the working directory has staged changes")
+ .requires("fix"),
+ ]),
+ Command::new("fmt")
+ .about("Run rustfmt on all projects and tests")
+ .args([
+ Arg::new("check").long("check").help("Use the rustfmt --check option"),
+ Arg::new("verbose").short('v').long("verbose").help("Echo commands run"),
+ ]),
+ Command::new("update_lints")
+ .about("Updates lint registration and information from the source code")
+ .long_about(
+ "Makes sure that:\n \
+ * the lint count in README.md is correct\n \
+ * the changelog contains markdown link references at the bottom\n \
+ * all lint groups include the correct lints\n \
+ * lint modules in `clippy_lints/*` are visible in `src/lib.rs` via `pub mod`\n \
+ * all lints are registered in the lint store",
+ )
+ .args([
+ Arg::new("print-only").long("print-only").help(
+ "Print a table of lints to STDOUT. \
+ This does not include deprecated and internal lints. \
+ (Does not modify any files)",
+ ),
+ Arg::new("check")
+ .long("check")
+ .help("Checks that `cargo dev update_lints` has been run. Used on CI."),
+ ]),
+ Command::new("new_lint")
+ .about("Create new lint and run `cargo dev update_lints`")
+ .args([
+ Arg::new("pass")
+ .short('p')
+ .long("pass")
+ .help("Specify whether the lint runs during the early or late pass")
+ .takes_value(true)
+ .value_parser([PossibleValue::new("early"), PossibleValue::new("late")])
+ .conflicts_with("type")
+ .required_unless_present("type"),
+ Arg::new("name")
+ .short('n')
+ .long("name")
+ .help("Name of the new lint in snake case, ex: fn_too_long")
+ .takes_value(true)
+ .required(true),
+ Arg::new("category")
+ .short('c')
+ .long("category")
+ .help("What category the lint belongs to")
+ .default_value("nursery")
+ .value_parser([
+ PossibleValue::new("style"),
+ PossibleValue::new("correctness"),
+ PossibleValue::new("suspicious"),
+ PossibleValue::new("complexity"),
+ PossibleValue::new("perf"),
+ PossibleValue::new("pedantic"),
+ PossibleValue::new("restriction"),
+ PossibleValue::new("cargo"),
+ PossibleValue::new("nursery"),
+ PossibleValue::new("internal"),
+ PossibleValue::new("internal_warn"),
+ ])
+ .takes_value(true),
+ Arg::new("type")
+ .long("type")
+ .help("What directory the lint belongs in")
+ .takes_value(true)
+ .required(false),
+ Arg::new("msrv").long("msrv").help("Add MSRV config code to the lint"),
+ ]),
+ Command::new("setup")
+ .about("Support for setting up your personal development environment")
+ .arg_required_else_help(true)
+ .subcommands([
+ Command::new("intellij")
+ .about("Alter dependencies so Intellij Rust can find rustc internals")
+ .args([
+ Arg::new("remove")
+ .long("remove")
+ .help("Remove the dependencies added with 'cargo dev setup intellij'")
+ .required(false),
+ Arg::new("rustc-repo-path")
+ .long("repo-path")
+ .short('r')
+ .help("The path to a rustc repo that will be used for setting the dependencies")
+ .takes_value(true)
+ .value_name("path")
+ .conflicts_with("remove")
+ .required(true),
+ ]),
+ Command::new("git-hook")
+ .about("Add a pre-commit git hook that formats your code to make it look pretty")
+ .args([
+ Arg::new("remove")
+ .long("remove")
+ .help("Remove the pre-commit hook added with 'cargo dev setup git-hook'")
+ .required(false),
+ Arg::new("force-override")
+ .long("force-override")
+ .short('f')
+ .help("Forces the override of an existing git pre-commit hook")
+ .required(false),
+ ]),
+ Command::new("vscode-tasks")
+ .about("Add several tasks to vscode for formatting, validation and testing")
+ .args([
+ Arg::new("remove")
+ .long("remove")
+ .help("Remove the tasks added with 'cargo dev setup vscode-tasks'")
+ .required(false),
+ Arg::new("force-override")
+ .long("force-override")
+ .short('f')
+ .help("Forces the override of existing vscode tasks")
+ .required(false),
+ ]),
+ ]),
+ Command::new("remove")
+ .about("Support for undoing changes done by the setup command")
+ .arg_required_else_help(true)
+ .subcommands([
+ Command::new("git-hook").about("Remove any existing pre-commit git hook"),
+ Command::new("vscode-tasks").about("Remove any existing vscode tasks"),
+ Command::new("intellij").about("Removes rustc source paths added via `cargo dev setup intellij`"),
+ ]),
+ Command::new("serve")
+ .about("Launch a local 'ALL the Clippy Lints' website in a browser")
+ .args([
+ Arg::new("port")
+ .long("port")
+ .short('p')
+ .help("Local port for the http server")
+ .default_value("8000")
+ .value_parser(clap::value_parser!(u16)),
+ Arg::new("lint").help("Which lint's page to load initially (optional)"),
+ ]),
+ Command::new("lint")
+ .about("Manually run clippy on a file or package")
+ .after_help(indoc! {"
+ EXAMPLES
+ Lint a single file:
+ cargo dev lint tests/ui/attrs.rs
+
+ Lint a package directory:
+ cargo dev lint tests/ui-cargo/wildcard_dependencies/fail
+ cargo dev lint ~/my-project
+
+ Run rustfix:
+ cargo dev lint ~/my-project -- --fix
+
+ Set lint levels:
+ cargo dev lint file.rs -- -W clippy::pedantic
+ cargo dev lint ~/my-project -- -- -W clippy::pedantic
+ "})
+ .args([
+ Arg::new("path")
+ .required(true)
+ .help("The path to a file or package directory to lint"),
+ Arg::new("args")
+ .action(ArgAction::Append)
+ .help("Pass extra arguments to cargo/clippy-driver"),
+ ]),
+ Command::new("rename_lint").about("Renames the given lint").args([
+ Arg::new("old_name")
+ .index(1)
+ .required(true)
+ .help("The name of the lint to rename"),
+ Arg::new("new_name")
+ .index(2)
+ .required_unless_present("uplift")
+ .help("The new name of the lint"),
+ Arg::new("uplift")
+ .long("uplift")
+ .help("This lint will be uplifted into rustc"),
+ ]),
+ Command::new("deprecate").about("Deprecates the given lint").args([
+ Arg::new("name")
+ .index(1)
+ .required(true)
+ .help("The name of the lint to deprecate"),
+ Arg::new("reason")
+ .long("reason")
+ .short('r')
+ .required(false)
+ .takes_value(true)
+ .help("The reason for deprecation"),
+ ]),
+ ])
+ .get_matches()
+}
diff --git a/src/tools/clippy/clippy_dev/src/new_lint.rs b/src/tools/clippy/clippy_dev/src/new_lint.rs
new file mode 100644
index 000000000..03d2ef3d1
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/new_lint.rs
@@ -0,0 +1,575 @@
+use crate::clippy_project_root;
+use indoc::{indoc, writedoc};
+use std::fmt::Write as _;
+use std::fs::{self, OpenOptions};
+use std::io::prelude::*;
+use std::io::{self, ErrorKind};
+use std::path::{Path, PathBuf};
+
+struct LintData<'a> {
+ pass: &'a str,
+ name: &'a str,
+ category: &'a str,
+ ty: Option<&'a str>,
+ project_root: PathBuf,
+}
+
+trait Context {
+ fn context<C: AsRef<str>>(self, text: C) -> Self;
+}
+
+impl<T> Context for io::Result<T> {
+ fn context<C: AsRef<str>>(self, text: C) -> Self {
+ match self {
+ Ok(t) => Ok(t),
+ Err(e) => {
+ let message = format!("{}: {}", text.as_ref(), e);
+ Err(io::Error::new(ErrorKind::Other, message))
+ },
+ }
+ }
+}
+
+/// Creates the files required to implement and test a new lint and runs `update_lints`.
+///
+/// # Errors
+///
+/// This function errors out if the files couldn't be created or written to.
+pub fn create(
+ pass: Option<&String>,
+ lint_name: Option<&String>,
+ category: Option<&str>,
+ mut ty: Option<&str>,
+ msrv: bool,
+) -> io::Result<()> {
+ if category == Some("cargo") && ty.is_none() {
+ // `cargo` is a special category, these lints should always be in `clippy_lints/src/cargo`
+ ty = Some("cargo");
+ }
+
+ let lint = LintData {
+ pass: pass.map_or("", String::as_str),
+ name: lint_name.expect("`name` argument is validated by clap"),
+ category: category.expect("`category` argument is validated by clap"),
+ ty,
+ project_root: clippy_project_root(),
+ };
+
+ create_lint(&lint, msrv).context("Unable to create lint implementation")?;
+ create_test(&lint).context("Unable to create a test for the new lint")?;
+
+ if lint.ty.is_none() {
+ add_lint(&lint, msrv).context("Unable to add lint to clippy_lints/src/lib.rs")?;
+ }
+
+ Ok(())
+}
+
+fn create_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
+ if let Some(ty) = lint.ty {
+ create_lint_for_ty(lint, enable_msrv, ty)
+ } else {
+ let lint_contents = get_lint_file_contents(lint, enable_msrv);
+ let lint_path = format!("clippy_lints/src/{}.rs", lint.name);
+ write_file(lint.project_root.join(&lint_path), lint_contents.as_bytes())?;
+ println!("Generated lint file: `{}`", lint_path);
+
+ Ok(())
+ }
+}
+
+fn create_test(lint: &LintData<'_>) -> io::Result<()> {
+ fn create_project_layout<P: Into<PathBuf>>(lint_name: &str, location: P, case: &str, hint: &str) -> io::Result<()> {
+ let mut path = location.into().join(case);
+ fs::create_dir(&path)?;
+ write_file(path.join("Cargo.toml"), get_manifest_contents(lint_name, hint))?;
+
+ path.push("src");
+ fs::create_dir(&path)?;
+ let header = format!("// compile-flags: --crate-name={}", lint_name);
+ write_file(path.join("main.rs"), get_test_file_contents(lint_name, Some(&header)))?;
+
+ Ok(())
+ }
+
+ if lint.category == "cargo" {
+ let relative_test_dir = format!("tests/ui-cargo/{}", lint.name);
+ let test_dir = lint.project_root.join(&relative_test_dir);
+ fs::create_dir(&test_dir)?;
+
+ create_project_layout(lint.name, &test_dir, "fail", "Content that triggers the lint goes here")?;
+ create_project_layout(lint.name, &test_dir, "pass", "This file should not trigger the lint")?;
+
+ println!("Generated test directories: `{relative_test_dir}/pass`, `{relative_test_dir}/fail`");
+ } else {
+ let test_path = format!("tests/ui/{}.rs", lint.name);
+ let test_contents = get_test_file_contents(lint.name, None);
+ write_file(lint.project_root.join(&test_path), test_contents)?;
+
+ println!("Generated test file: `{}`", test_path);
+ }
+
+ Ok(())
+}
+
+fn add_lint(lint: &LintData<'_>, enable_msrv: bool) -> io::Result<()> {
+ let path = "clippy_lints/src/lib.rs";
+ let mut lib_rs = fs::read_to_string(path).context("reading")?;
+
+ let comment_start = lib_rs.find("// add lints here,").expect("Couldn't find comment");
+
+ let new_lint = if enable_msrv {
+ format!(
+ "store.register_{lint_pass}_pass(move || Box::new({module_name}::{camel_name}::new(msrv)));\n ",
+ lint_pass = lint.pass,
+ module_name = lint.name,
+ camel_name = to_camel_case(lint.name),
+ )
+ } else {
+ format!(
+ "store.register_{lint_pass}_pass(|| Box::new({module_name}::{camel_name}));\n ",
+ lint_pass = lint.pass,
+ module_name = lint.name,
+ camel_name = to_camel_case(lint.name),
+ )
+ };
+
+ lib_rs.insert_str(comment_start, &new_lint);
+
+ fs::write(path, lib_rs).context("writing")
+}
+
+fn write_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
+ fn inner(path: &Path, contents: &[u8]) -> io::Result<()> {
+ OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(path)?
+ .write_all(contents)
+ }
+
+ inner(path.as_ref(), contents.as_ref()).context(format!("writing to file: {}", path.as_ref().display()))
+}
+
+fn to_camel_case(name: &str) -> String {
+ name.split('_')
+ .map(|s| {
+ if s.is_empty() {
+ String::from("")
+ } else {
+ [&s[0..1].to_uppercase(), &s[1..]].concat()
+ }
+ })
+ .collect()
+}
+
+pub(crate) fn get_stabilization_version() -> String {
+ fn parse_manifest(contents: &str) -> Option<String> {
+ let version = contents
+ .lines()
+ .filter_map(|l| l.split_once('='))
+ .find_map(|(k, v)| (k.trim() == "version").then(|| v.trim()))?;
+ let Some(("0", version)) = version.get(1..version.len() - 1)?.split_once('.') else {
+ return None;
+ };
+ let (minor, patch) = version.split_once('.')?;
+ Some(format!(
+ "{}.{}.0",
+ minor.parse::<u32>().ok()?,
+ patch.parse::<u32>().ok()?
+ ))
+ }
+ let contents = fs::read_to_string("Cargo.toml").expect("Unable to read `Cargo.toml`");
+ parse_manifest(&contents).expect("Unable to find package version in `Cargo.toml`")
+}
+
+fn get_test_file_contents(lint_name: &str, header_commands: Option<&str>) -> String {
+ let mut contents = format!(
+ indoc! {"
+ #![warn(clippy::{})]
+
+ fn main() {{
+ // test code goes here
+ }}
+ "},
+ lint_name
+ );
+
+ if let Some(header) = header_commands {
+ contents = format!("{}\n{}", header, contents);
+ }
+
+ contents
+}
+
+fn get_manifest_contents(lint_name: &str, hint: &str) -> String {
+ format!(
+ indoc! {r#"
+ # {}
+
+ [package]
+ name = "{}"
+ version = "0.1.0"
+ publish = false
+
+ [workspace]
+ "#},
+ hint, lint_name
+ )
+}
+
+fn get_lint_file_contents(lint: &LintData<'_>, enable_msrv: bool) -> String {
+ let mut result = String::new();
+
+ let (pass_type, pass_lifetimes, pass_import, context_import) = match lint.pass {
+ "early" => ("EarlyLintPass", "", "use rustc_ast::ast::*;", "EarlyContext"),
+ "late" => ("LateLintPass", "<'_>", "use rustc_hir::*;", "LateContext"),
+ _ => {
+ unreachable!("`pass_type` should only ever be `early` or `late`!");
+ },
+ };
+
+ let lint_name = lint.name;
+ let category = lint.category;
+ let name_camel = to_camel_case(lint.name);
+ let name_upper = lint_name.to_uppercase();
+
+ result.push_str(&if enable_msrv {
+ format!(
+ indoc! {"
+ use clippy_utils::msrvs;
+ {pass_import}
+ use rustc_lint::{{{context_import}, {pass_type}, LintContext}};
+ use rustc_semver::RustcVersion;
+ use rustc_session::{{declare_tool_lint, impl_lint_pass}};
+
+ "},
+ pass_type = pass_type,
+ pass_import = pass_import,
+ context_import = context_import,
+ )
+ } else {
+ format!(
+ indoc! {"
+ {pass_import}
+ use rustc_lint::{{{context_import}, {pass_type}}};
+ use rustc_session::{{declare_lint_pass, declare_tool_lint}};
+
+ "},
+ pass_import = pass_import,
+ pass_type = pass_type,
+ context_import = context_import
+ )
+ });
+
+ let _ = write!(result, "{}", get_lint_declaration(&name_upper, category));
+
+ result.push_str(&if enable_msrv {
+ format!(
+ indoc! {"
+ pub struct {name_camel} {{
+ msrv: Option<RustcVersion>,
+ }}
+
+ impl {name_camel} {{
+ #[must_use]
+ pub fn new(msrv: Option<RustcVersion>) -> Self {{
+ Self {{ msrv }}
+ }}
+ }}
+
+ impl_lint_pass!({name_camel} => [{name_upper}]);
+
+ impl {pass_type}{pass_lifetimes} for {name_camel} {{
+ extract_msrv_attr!({context_import});
+ }}
+
+ // TODO: Add MSRV level to `clippy_utils/src/msrvs.rs` if needed.
+ // TODO: Add MSRV test to `tests/ui/min_rust_version_attr.rs`.
+ // TODO: Update msrv config comment in `clippy_lints/src/utils/conf.rs`
+ "},
+ pass_type = pass_type,
+ pass_lifetimes = pass_lifetimes,
+ name_upper = name_upper,
+ name_camel = name_camel,
+ context_import = context_import,
+ )
+ } else {
+ format!(
+ indoc! {"
+ declare_lint_pass!({name_camel} => [{name_upper}]);
+
+ impl {pass_type}{pass_lifetimes} for {name_camel} {{}}
+ "},
+ pass_type = pass_type,
+ pass_lifetimes = pass_lifetimes,
+ name_upper = name_upper,
+ name_camel = name_camel,
+ )
+ });
+
+ result
+}
+
+fn get_lint_declaration(name_upper: &str, category: &str) -> String {
+ format!(
+ indoc! {r#"
+ declare_clippy_lint! {{
+ /// ### What it does
+ ///
+ /// ### Why is this bad?
+ ///
+ /// ### Example
+ /// ```rust
+ /// // example code where clippy issues a warning
+ /// ```
+ /// Use instead:
+ /// ```rust
+ /// // example code which does not raise clippy warning
+ /// ```
+ #[clippy::version = "{version}"]
+ pub {name_upper},
+ {category},
+ "default lint description"
+ }}
+ "#},
+ version = get_stabilization_version(),
+ name_upper = name_upper,
+ category = category,
+ )
+}
+
+fn create_lint_for_ty(lint: &LintData<'_>, enable_msrv: bool, ty: &str) -> io::Result<()> {
+ match ty {
+ "cargo" => assert_eq!(
+ lint.category, "cargo",
+ "Lints of type `cargo` must have the `cargo` category"
+ ),
+ _ if lint.category == "cargo" => panic!("Lints of category `cargo` must have the `cargo` type"),
+ _ => {},
+ }
+
+ let ty_dir = lint.project_root.join(format!("clippy_lints/src/{}", ty));
+ assert!(
+ ty_dir.exists() && ty_dir.is_dir(),
+ "Directory `{}` does not exist!",
+ ty_dir.display()
+ );
+
+ let lint_file_path = ty_dir.join(format!("{}.rs", lint.name));
+ assert!(
+ !lint_file_path.exists(),
+ "File `{}` already exists",
+ lint_file_path.display()
+ );
+
+ let mod_file_path = ty_dir.join("mod.rs");
+ let context_import = setup_mod_file(&mod_file_path, lint)?;
+
+ let name_upper = lint.name.to_uppercase();
+ let mut lint_file_contents = String::new();
+
+ if enable_msrv {
+ let _ = writedoc!(
+ lint_file_contents,
+ r#"
+ use clippy_utils::{{meets_msrv, msrvs}};
+ use rustc_lint::{{{context_import}, LintContext}};
+ use rustc_semver::RustcVersion;
+
+ use super::{name_upper};
+
+ // TODO: Adjust the parameters as necessary
+ pub(super) fn check(cx: &{context_import}, msrv: Option<RustcVersion>) {{
+ if !meets_msrv(msrv, todo!("Add a new entry in `clippy_utils/src/msrvs`")) {{
+ return;
+ }}
+ todo!();
+ }}
+ "#,
+ context_import = context_import,
+ name_upper = name_upper,
+ );
+ } else {
+ let _ = writedoc!(
+ lint_file_contents,
+ r#"
+ use rustc_lint::{{{context_import}, LintContext}};
+
+ use super::{name_upper};
+
+ // TODO: Adjust the parameters as necessary
+ pub(super) fn check(cx: &{context_import}) {{
+ todo!();
+ }}
+ "#,
+ context_import = context_import,
+ name_upper = name_upper,
+ );
+ }
+
+ write_file(lint_file_path.as_path(), lint_file_contents)?;
+ println!("Generated lint file: `clippy_lints/src/{}/{}.rs`", ty, lint.name);
+ println!(
+ "Be sure to add a call to `{}::check` in `clippy_lints/src/{}/mod.rs`!",
+ lint.name, ty
+ );
+
+ Ok(())
+}
+
+#[allow(clippy::too_many_lines)]
+fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str> {
+ use super::update_lints::{match_tokens, LintDeclSearchResult};
+ use rustc_lexer::TokenKind;
+
+ let lint_name_upper = lint.name.to_uppercase();
+
+ let mut file_contents = fs::read_to_string(path)?;
+ assert!(
+ !file_contents.contains(&lint_name_upper),
+ "Lint `{}` already defined in `{}`",
+ lint.name,
+ path.display()
+ );
+
+ let mut offset = 0usize;
+ let mut last_decl_curly_offset = None;
+ let mut lint_context = None;
+
+ let mut iter = rustc_lexer::tokenize(&file_contents).map(|t| {
+ let range = offset..offset + t.len;
+ offset = range.end;
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &file_contents[range.clone()],
+ range,
+ }
+ });
+
+ // Find both the last lint declaration (declare_clippy_lint!) and the lint pass impl
+ while let Some(LintDeclSearchResult { content, .. }) = iter.find(|result| result.token_kind == TokenKind::Ident) {
+ let mut iter = iter
+ .by_ref()
+ .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+
+ match content {
+ "declare_clippy_lint" => {
+ // matches `!{`
+ match_tokens!(iter, Bang OpenBrace);
+ if let Some(LintDeclSearchResult { range, .. }) =
+ iter.find(|result| result.token_kind == TokenKind::CloseBrace)
+ {
+ last_decl_curly_offset = Some(range.end);
+ }
+ },
+ "impl" => {
+ let mut token = iter.next();
+ match token {
+ // matches <'foo>
+ Some(LintDeclSearchResult {
+ token_kind: TokenKind::Lt,
+ ..
+ }) => {
+ match_tokens!(iter, Lifetime { .. } Gt);
+ token = iter.next();
+ },
+ None => break,
+ _ => {},
+ }
+
+ if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::Ident,
+ content,
+ ..
+ }) = token
+ {
+ // Get the appropriate lint context struct
+ lint_context = match content {
+ "LateLintPass" => Some("LateContext"),
+ "EarlyLintPass" => Some("EarlyContext"),
+ _ => continue,
+ };
+ }
+ },
+ _ => {},
+ }
+ }
+
+ drop(iter);
+
+ let last_decl_curly_offset =
+ last_decl_curly_offset.unwrap_or_else(|| panic!("No lint declarations found in `{}`", path.display()));
+ let lint_context =
+ lint_context.unwrap_or_else(|| panic!("No lint pass implementation found in `{}`", path.display()));
+
+ // Add the lint declaration to `mod.rs`
+ file_contents.replace_range(
+ // Remove the trailing newline, which should always be present
+ last_decl_curly_offset..=last_decl_curly_offset,
+ &format!("\n\n{}", get_lint_declaration(&lint_name_upper, lint.category)),
+ );
+
+ // Add the lint to `impl_lint_pass`/`declare_lint_pass`
+ let impl_lint_pass_start = file_contents.find("impl_lint_pass!").unwrap_or_else(|| {
+ file_contents
+ .find("declare_lint_pass!")
+ .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`/`declare_lint_pass`"))
+ });
+
+ let mut arr_start = file_contents[impl_lint_pass_start..].find('[').unwrap_or_else(|| {
+ panic!("malformed `impl_lint_pass`/`declare_lint_pass`");
+ });
+
+ arr_start += impl_lint_pass_start;
+
+ let mut arr_end = file_contents[arr_start..]
+ .find(']')
+ .expect("failed to find `impl_lint_pass` terminator");
+
+ arr_end += arr_start;
+
+ let mut arr_content = file_contents[arr_start + 1..arr_end].to_string();
+ arr_content.retain(|c| !c.is_whitespace());
+
+ let mut new_arr_content = String::new();
+ for ident in arr_content
+ .split(',')
+ .chain(std::iter::once(&*lint_name_upper))
+ .filter(|s| !s.is_empty())
+ {
+ let _ = write!(new_arr_content, "\n {},", ident);
+ }
+ new_arr_content.push('\n');
+
+ file_contents.replace_range(arr_start + 1..arr_end, &new_arr_content);
+
+ // Just add the mod declaration at the top, it'll be fixed by rustfmt
+ file_contents.insert_str(0, &format!("mod {};\n", &lint.name));
+
+ let mut file = OpenOptions::new()
+ .write(true)
+ .truncate(true)
+ .open(path)
+ .context(format!("trying to open: `{}`", path.display()))?;
+ file.write_all(file_contents.as_bytes())
+ .context(format!("writing to file: `{}`", path.display()))?;
+
+ Ok(lint_context)
+}
+
+#[test]
+fn test_camel_case() {
+ let s = "a_lint";
+ let s2 = to_camel_case(s);
+ assert_eq!(s2, "ALint");
+
+ let name = "a_really_long_new_lint";
+ let name2 = to_camel_case(name);
+ assert_eq!(name2, "AReallyLongNewLint");
+
+ let name3 = "lint__name";
+ let name4 = to_camel_case(name3);
+ assert_eq!(name4, "LintName");
+}
diff --git a/src/tools/clippy/clippy_dev/src/serve.rs b/src/tools/clippy/clippy_dev/src/serve.rs
new file mode 100644
index 000000000..f15f24da9
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/serve.rs
@@ -0,0 +1,65 @@
+use std::ffi::OsStr;
+use std::num::ParseIntError;
+use std::path::Path;
+use std::process::Command;
+use std::thread;
+use std::time::{Duration, SystemTime};
+
+/// # Panics
+///
+/// Panics if the python commands could not be spawned
+pub fn run(port: u16, lint: Option<&String>) -> ! {
+ let mut url = Some(match lint {
+ None => format!("http://localhost:{}", port),
+ Some(lint) => format!("http://localhost:{}/#{}", port, lint),
+ });
+
+ loop {
+ if mtime("util/gh-pages/lints.json") < mtime("clippy_lints/src") {
+ Command::new("cargo")
+ .arg("collect-metadata")
+ .spawn()
+ .unwrap()
+ .wait()
+ .unwrap();
+ }
+ if let Some(url) = url.take() {
+ thread::spawn(move || {
+ Command::new("python3")
+ .arg("-m")
+ .arg("http.server")
+ .arg(port.to_string())
+ .current_dir("util/gh-pages")
+ .spawn()
+ .unwrap();
+ // Give some time for python to start
+ thread::sleep(Duration::from_millis(500));
+ // Launch browser after first export.py has completed and http.server is up
+ let _result = opener::open(url);
+ });
+ }
+ thread::sleep(Duration::from_millis(1000));
+ }
+}
+
+fn mtime(path: impl AsRef<Path>) -> SystemTime {
+ let path = path.as_ref();
+ if path.is_dir() {
+ path.read_dir()
+ .into_iter()
+ .flatten()
+ .flatten()
+ .map(|entry| mtime(&entry.path()))
+ .max()
+ .unwrap_or(SystemTime::UNIX_EPOCH)
+ } else {
+ path.metadata()
+ .and_then(|metadata| metadata.modified())
+ .unwrap_or(SystemTime::UNIX_EPOCH)
+ }
+}
+
+#[allow(clippy::missing_errors_doc)]
+pub fn validate_port(arg: &OsStr) -> Result<(), ParseIntError> {
+ arg.to_string_lossy().parse::<u16>().map(|_| ())
+}
diff --git a/src/tools/clippy/clippy_dev/src/setup/git_hook.rs b/src/tools/clippy/clippy_dev/src/setup/git_hook.rs
new file mode 100644
index 000000000..3fbb77d59
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/setup/git_hook.rs
@@ -0,0 +1,85 @@
+use std::fs;
+use std::path::Path;
+
+use super::verify_inside_clippy_dir;
+
+/// Rusts setup uses `git rev-parse --git-common-dir` to get the root directory of the repo.
+/// I've decided against this for the sake of simplicity and to make sure that it doesn't install
+/// the hook if `clippy_dev` would be used in the rust tree. The hook also references this tool
+/// for formatting and should therefor only be used in a normal clone of clippy
+const REPO_GIT_DIR: &str = ".git";
+const HOOK_SOURCE_FILE: &str = "util/etc/pre-commit.sh";
+const HOOK_TARGET_FILE: &str = ".git/hooks/pre-commit";
+
+pub fn install_hook(force_override: bool) {
+ if !check_precondition(force_override) {
+ return;
+ }
+
+ // So a little bit of a funny story. Git on unix requires the pre-commit file
+ // to have the `execute` permission to be set. The Rust functions for modifying
+ // these flags doesn't seem to work when executed with normal user permissions.
+ //
+ // However, there is a little hack that is also being used by Rust itself in their
+ // setup script. Git saves the `execute` flag when syncing files. This means
+ // that we can check in a file with execution permissions and the sync it to create
+ // a file with the flag set. We then copy this file here. The copy function will also
+ // include the `execute` permission.
+ match fs::copy(HOOK_SOURCE_FILE, HOOK_TARGET_FILE) {
+ Ok(_) => {
+ println!("info: the hook can be removed with `cargo dev remove git-hook`");
+ println!("git hook successfully installed");
+ },
+ Err(err) => eprintln!(
+ "error: unable to copy `{}` to `{}` ({})",
+ HOOK_SOURCE_FILE, HOOK_TARGET_FILE, err
+ ),
+ }
+}
+
+fn check_precondition(force_override: bool) -> bool {
+ if !verify_inside_clippy_dir() {
+ return false;
+ }
+
+ // Make sure that we can find the git repository
+ let git_path = Path::new(REPO_GIT_DIR);
+ if !git_path.exists() || !git_path.is_dir() {
+ eprintln!("error: clippy_dev was unable to find the `.git` directory");
+ return false;
+ }
+
+ // Make sure that we don't override an existing hook by accident
+ let path = Path::new(HOOK_TARGET_FILE);
+ if path.exists() {
+ if force_override {
+ return delete_git_hook_file(path);
+ }
+
+ eprintln!("error: there is already a pre-commit hook installed");
+ println!("info: use the `--force-override` flag to override the existing hook");
+ return false;
+ }
+
+ true
+}
+
+pub fn remove_hook() {
+ let path = Path::new(HOOK_TARGET_FILE);
+ if path.exists() {
+ if delete_git_hook_file(path) {
+ println!("git hook successfully removed");
+ }
+ } else {
+ println!("no pre-commit hook was found");
+ }
+}
+
+fn delete_git_hook_file(path: &Path) -> bool {
+ if let Err(err) = fs::remove_file(path) {
+ eprintln!("error: unable to delete existing pre-commit git hook ({})", err);
+ false
+ } else {
+ true
+ }
+}
diff --git a/src/tools/clippy/clippy_dev/src/setup/intellij.rs b/src/tools/clippy/clippy_dev/src/setup/intellij.rs
new file mode 100644
index 000000000..bf741e6d1
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/setup/intellij.rs
@@ -0,0 +1,223 @@
+use std::fs;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::{Path, PathBuf};
+
+// This module takes an absolute path to a rustc repo and alters the dependencies to point towards
+// the respective rustc subcrates instead of using extern crate xyz.
+// This allows IntelliJ to analyze rustc internals and show proper information inside Clippy
+// code. See https://github.com/rust-lang/rust-clippy/issues/5514 for details
+
+const RUSTC_PATH_SECTION: &str = "[target.'cfg(NOT_A_PLATFORM)'.dependencies]";
+const DEPENDENCIES_SECTION: &str = "[dependencies]";
+
+const CLIPPY_PROJECTS: &[ClippyProjectInfo] = &[
+ ClippyProjectInfo::new("root", "Cargo.toml", "src/driver.rs"),
+ ClippyProjectInfo::new("clippy_lints", "clippy_lints/Cargo.toml", "clippy_lints/src/lib.rs"),
+ ClippyProjectInfo::new("clippy_utils", "clippy_utils/Cargo.toml", "clippy_utils/src/lib.rs"),
+];
+
+/// Used to store clippy project information to later inject the dependency into.
+struct ClippyProjectInfo {
+ /// Only used to display information to the user
+ name: &'static str,
+ cargo_file: &'static str,
+ lib_rs_file: &'static str,
+}
+
+impl ClippyProjectInfo {
+ const fn new(name: &'static str, cargo_file: &'static str, lib_rs_file: &'static str) -> Self {
+ Self {
+ name,
+ cargo_file,
+ lib_rs_file,
+ }
+ }
+}
+
+pub fn setup_rustc_src(rustc_path: &str) {
+ let rustc_source_dir = match check_and_get_rustc_dir(rustc_path) {
+ Ok(path) => path,
+ Err(_) => return,
+ };
+
+ for project in CLIPPY_PROJECTS {
+ if inject_deps_into_project(&rustc_source_dir, project).is_err() {
+ return;
+ }
+ }
+
+ println!("info: the source paths can be removed again with `cargo dev remove intellij`");
+}
+
+fn check_and_get_rustc_dir(rustc_path: &str) -> Result<PathBuf, ()> {
+ let mut path = PathBuf::from(rustc_path);
+
+ if path.is_relative() {
+ match path.canonicalize() {
+ Ok(absolute_path) => {
+ println!("info: the rustc path was resolved to: `{}`", absolute_path.display());
+ path = absolute_path;
+ },
+ Err(err) => {
+ eprintln!("error: unable to get the absolute path of rustc ({})", err);
+ return Err(());
+ },
+ };
+ }
+
+ let path = path.join("compiler");
+ println!("info: looking for compiler sources at: {}", path.display());
+
+ if !path.exists() {
+ eprintln!("error: the given path does not exist");
+ return Err(());
+ }
+
+ if !path.is_dir() {
+ eprintln!("error: the given path is not a directory");
+ return Err(());
+ }
+
+ Ok(path)
+}
+
+fn inject_deps_into_project(rustc_source_dir: &Path, project: &ClippyProjectInfo) -> Result<(), ()> {
+ let cargo_content = read_project_file(project.cargo_file)?;
+ let lib_content = read_project_file(project.lib_rs_file)?;
+
+ if inject_deps_into_manifest(rustc_source_dir, project.cargo_file, &cargo_content, &lib_content).is_err() {
+ eprintln!(
+ "error: unable to inject dependencies into {} with the Cargo file {}",
+ project.name, project.cargo_file
+ );
+ Err(())
+ } else {
+ Ok(())
+ }
+}
+
+/// `clippy_dev` expects to be executed in the root directory of Clippy. This function
+/// loads the given file or returns an error. Having it in this extra function ensures
+/// that the error message looks nice.
+fn read_project_file(file_path: &str) -> Result<String, ()> {
+ let path = Path::new(file_path);
+ if !path.exists() {
+ eprintln!("error: unable to find the file `{}`", file_path);
+ return Err(());
+ }
+
+ match fs::read_to_string(path) {
+ Ok(content) => Ok(content),
+ Err(err) => {
+ eprintln!("error: the file `{}` could not be read ({})", file_path, err);
+ Err(())
+ },
+ }
+}
+
+fn inject_deps_into_manifest(
+ rustc_source_dir: &Path,
+ manifest_path: &str,
+ cargo_toml: &str,
+ lib_rs: &str,
+) -> std::io::Result<()> {
+ // do not inject deps if we have already done so
+ if cargo_toml.contains(RUSTC_PATH_SECTION) {
+ eprintln!(
+ "warn: dependencies are already setup inside {}, skipping file",
+ manifest_path
+ );
+ return Ok(());
+ }
+
+ let extern_crates = lib_rs
+ .lines()
+ // only take dependencies starting with `rustc_`
+ .filter(|line| line.starts_with("extern crate rustc_"))
+ // we have something like "extern crate foo;", we only care about the "foo"
+ // extern crate rustc_middle;
+ // ^^^^^^^^^^^^
+ .map(|s| &s[13..(s.len() - 1)]);
+
+ let new_deps = extern_crates.map(|dep| {
+ // format the dependencies that are going to be put inside the Cargo.toml
+ format!(
+ "{dep} = {{ path = \"{source_path}/{dep}\" }}\n",
+ dep = dep,
+ source_path = rustc_source_dir.display()
+ )
+ });
+
+ // format a new [dependencies]-block with the new deps we need to inject
+ let mut all_deps = String::from("[target.'cfg(NOT_A_PLATFORM)'.dependencies]\n");
+ new_deps.for_each(|dep_line| {
+ all_deps.push_str(&dep_line);
+ });
+ all_deps.push_str("\n[dependencies]\n");
+
+ // replace "[dependencies]" with
+ // [dependencies]
+ // dep1 = { path = ... }
+ // dep2 = { path = ... }
+ // etc
+ let new_manifest = cargo_toml.replacen("[dependencies]\n", &all_deps, 1);
+
+ // println!("{}", new_manifest);
+ let mut file = File::create(manifest_path)?;
+ file.write_all(new_manifest.as_bytes())?;
+
+ println!("info: successfully setup dependencies inside {}", manifest_path);
+
+ Ok(())
+}
+
+pub fn remove_rustc_src() {
+ for project in CLIPPY_PROJECTS {
+ remove_rustc_src_from_project(project);
+ }
+}
+
+fn remove_rustc_src_from_project(project: &ClippyProjectInfo) -> bool {
+ let mut cargo_content = if let Ok(content) = read_project_file(project.cargo_file) {
+ content
+ } else {
+ return false;
+ };
+ let section_start = if let Some(section_start) = cargo_content.find(RUSTC_PATH_SECTION) {
+ section_start
+ } else {
+ println!(
+ "info: dependencies could not be found in `{}` for {}, skipping file",
+ project.cargo_file, project.name
+ );
+ return true;
+ };
+
+ let end_point = if let Some(end_point) = cargo_content.find(DEPENDENCIES_SECTION) {
+ end_point
+ } else {
+ eprintln!(
+ "error: the end of the rustc dependencies section could not be found in `{}`",
+ project.cargo_file
+ );
+ return false;
+ };
+
+ cargo_content.replace_range(section_start..end_point, "");
+
+ match File::create(project.cargo_file) {
+ Ok(mut file) => {
+ file.write_all(cargo_content.as_bytes()).unwrap();
+ println!("info: successfully removed dependencies inside {}", project.cargo_file);
+ true
+ },
+ Err(err) => {
+ eprintln!(
+ "error: unable to open file `{}` to remove rustc dependencies for {} ({})",
+ project.cargo_file, project.name, err
+ );
+ false
+ },
+ }
+}
diff --git a/src/tools/clippy/clippy_dev/src/setup/mod.rs b/src/tools/clippy/clippy_dev/src/setup/mod.rs
new file mode 100644
index 000000000..f691ae4fa
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/setup/mod.rs
@@ -0,0 +1,23 @@
+pub mod git_hook;
+pub mod intellij;
+pub mod vscode;
+
+use std::path::Path;
+
+const CLIPPY_DEV_DIR: &str = "clippy_dev";
+
+/// This function verifies that the tool is being executed in the clippy directory.
+/// This is useful to ensure that setups only modify Clippy's resources. The verification
+/// is done by checking that `clippy_dev` is a sub directory of the current directory.
+///
+/// It will print an error message and return `false` if the directory could not be
+/// verified.
+fn verify_inside_clippy_dir() -> bool {
+ let path = Path::new(CLIPPY_DEV_DIR);
+ if path.exists() && path.is_dir() {
+ true
+ } else {
+ eprintln!("error: unable to verify that the working directory is clippy's directory");
+ false
+ }
+}
diff --git a/src/tools/clippy/clippy_dev/src/setup/vscode.rs b/src/tools/clippy/clippy_dev/src/setup/vscode.rs
new file mode 100644
index 000000000..d59001b2c
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/setup/vscode.rs
@@ -0,0 +1,104 @@
+use std::fs;
+use std::path::Path;
+
+use super::verify_inside_clippy_dir;
+
+const VSCODE_DIR: &str = ".vscode";
+const TASK_SOURCE_FILE: &str = "util/etc/vscode-tasks.json";
+const TASK_TARGET_FILE: &str = ".vscode/tasks.json";
+
+pub fn install_tasks(force_override: bool) {
+ if !check_install_precondition(force_override) {
+ return;
+ }
+
+ match fs::copy(TASK_SOURCE_FILE, TASK_TARGET_FILE) {
+ Ok(_) => {
+ println!("info: the task file can be removed with `cargo dev remove vscode-tasks`");
+ println!("vscode tasks successfully installed");
+ },
+ Err(err) => eprintln!(
+ "error: unable to copy `{}` to `{}` ({})",
+ TASK_SOURCE_FILE, TASK_TARGET_FILE, err
+ ),
+ }
+}
+
+fn check_install_precondition(force_override: bool) -> bool {
+ if !verify_inside_clippy_dir() {
+ return false;
+ }
+
+ let vs_dir_path = Path::new(VSCODE_DIR);
+ if vs_dir_path.exists() {
+ // verify the target will be valid
+ if !vs_dir_path.is_dir() {
+ eprintln!("error: the `.vscode` path exists but seems to be a file");
+ return false;
+ }
+
+ // make sure that we don't override any existing tasks by accident
+ let path = Path::new(TASK_TARGET_FILE);
+ if path.exists() {
+ if force_override {
+ return delete_vs_task_file(path);
+ }
+
+ eprintln!(
+ "error: there is already a `task.json` file inside the `{}` directory",
+ VSCODE_DIR
+ );
+ println!("info: use the `--force-override` flag to override the existing `task.json` file");
+ return false;
+ }
+ } else {
+ match fs::create_dir(vs_dir_path) {
+ Ok(_) => {
+ println!("info: created `{}` directory for clippy", VSCODE_DIR);
+ },
+ Err(err) => {
+ eprintln!(
+ "error: the task target directory `{}` could not be created ({})",
+ VSCODE_DIR, err
+ );
+ },
+ }
+ }
+
+ true
+}
+
+pub fn remove_tasks() {
+ let path = Path::new(TASK_TARGET_FILE);
+ if path.exists() {
+ if delete_vs_task_file(path) {
+ try_delete_vs_directory_if_empty();
+ println!("vscode tasks successfully removed");
+ }
+ } else {
+ println!("no vscode tasks were found");
+ }
+}
+
+fn delete_vs_task_file(path: &Path) -> bool {
+ if let Err(err) = fs::remove_file(path) {
+ eprintln!("error: unable to delete the existing `tasks.json` file ({})", err);
+ return false;
+ }
+
+ true
+}
+
+/// This function will try to delete the `.vscode` directory if it's empty.
+/// It may fail silently.
+fn try_delete_vs_directory_if_empty() {
+ let path = Path::new(VSCODE_DIR);
+ if path.read_dir().map_or(false, |mut iter| iter.next().is_none()) {
+ // The directory is empty. We just try to delete it but allow a silence
+ // fail as an empty `.vscode` directory is still valid
+ let _silence_result = fs::remove_dir(path);
+ } else {
+ // The directory is not empty or could not be read. Either way don't take
+ // any further actions
+ }
+}
diff --git a/src/tools/clippy/clippy_dev/src/update_lints.rs b/src/tools/clippy/clippy_dev/src/update_lints.rs
new file mode 100644
index 000000000..aed38bc28
--- /dev/null
+++ b/src/tools/clippy/clippy_dev/src/update_lints.rs
@@ -0,0 +1,1277 @@
+use crate::clippy_project_root;
+use aho_corasick::AhoCorasickBuilder;
+use indoc::writedoc;
+use itertools::Itertools;
+use rustc_lexer::{tokenize, unescape, LiteralKind, TokenKind};
+use std::collections::{HashMap, HashSet};
+use std::ffi::OsStr;
+use std::fmt::Write;
+use std::fs::{self, OpenOptions};
+use std::io::{self, Read, Seek, SeekFrom, Write as _};
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+use walkdir::{DirEntry, WalkDir};
+
+const GENERATED_FILE_COMMENT: &str = "// This file was generated by `cargo dev update_lints`.\n\
+ // Use that command to update this file and do not edit by hand.\n\
+ // Manual edits will be overwritten.\n\n";
+
+const DOCS_LINK: &str = "https://rust-lang.github.io/rust-clippy/master/index.html";
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum UpdateMode {
+ Check,
+ Change,
+}
+
+/// Runs the `update_lints` command.
+///
+/// This updates various generated values from the lint source code.
+///
+/// `update_mode` indicates if the files should be updated or if updates should be checked for.
+///
+/// # Panics
+///
+/// Panics if a file path could not read from or then written to
+pub fn update(update_mode: UpdateMode) {
+ let (lints, deprecated_lints, renamed_lints) = gather_all();
+ generate_lint_files(update_mode, &lints, &deprecated_lints, &renamed_lints);
+}
+
+fn generate_lint_files(
+ update_mode: UpdateMode,
+ lints: &[Lint],
+ deprecated_lints: &[DeprecatedLint],
+ renamed_lints: &[RenamedLint],
+) {
+ let internal_lints = Lint::internal_lints(lints);
+ let usable_lints = Lint::usable_lints(lints);
+ let mut sorted_usable_lints = usable_lints.clone();
+ sorted_usable_lints.sort_by_key(|lint| lint.name.clone());
+
+ replace_region_in_file(
+ update_mode,
+ Path::new("README.md"),
+ "[There are over ",
+ " lints included in this crate!]",
+ |res| {
+ write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
+ },
+ );
+
+ replace_region_in_file(
+ update_mode,
+ Path::new("book/src/README.md"),
+ "[There are over ",
+ " lints included in this crate!]",
+ |res| {
+ write!(res, "{}", round_to_fifty(usable_lints.len())).unwrap();
+ },
+ );
+
+ replace_region_in_file(
+ update_mode,
+ Path::new("CHANGELOG.md"),
+ "<!-- begin autogenerated links to lint list -->\n",
+ "<!-- end autogenerated links to lint list -->",
+ |res| {
+ for lint in usable_lints
+ .iter()
+ .map(|l| &*l.name)
+ .chain(deprecated_lints.iter().map(|l| &*l.name))
+ .chain(
+ renamed_lints
+ .iter()
+ .map(|l| l.old_name.strip_prefix("clippy::").unwrap_or(&l.old_name)),
+ )
+ .sorted()
+ {
+ writeln!(res, "[`{}`]: {}#{}", lint, DOCS_LINK, lint).unwrap();
+ }
+ },
+ );
+
+ // This has to be in lib.rs, otherwise rustfmt doesn't work
+ replace_region_in_file(
+ update_mode,
+ Path::new("clippy_lints/src/lib.rs"),
+ "// begin lints modules, do not remove this comment, it’s used in `update_lints`\n",
+ "// end lints modules, do not remove this comment, it’s used in `update_lints`",
+ |res| {
+ for lint_mod in usable_lints.iter().map(|l| &l.module).unique().sorted() {
+ writeln!(res, "mod {};", lint_mod).unwrap();
+ }
+ },
+ );
+
+ process_file(
+ "clippy_lints/src/lib.register_lints.rs",
+ update_mode,
+ &gen_register_lint_list(internal_lints.iter(), usable_lints.iter()),
+ );
+ process_file(
+ "clippy_lints/src/lib.deprecated.rs",
+ update_mode,
+ &gen_deprecated(deprecated_lints),
+ );
+
+ let all_group_lints = usable_lints.iter().filter(|l| {
+ matches!(
+ &*l.group,
+ "correctness" | "suspicious" | "style" | "complexity" | "perf"
+ )
+ });
+ let content = gen_lint_group_list("all", all_group_lints);
+ process_file("clippy_lints/src/lib.register_all.rs", update_mode, &content);
+
+ for (lint_group, lints) in Lint::by_lint_group(usable_lints.into_iter().chain(internal_lints)) {
+ let content = gen_lint_group_list(&lint_group, lints.iter());
+ process_file(
+ &format!("clippy_lints/src/lib.register_{}.rs", lint_group),
+ update_mode,
+ &content,
+ );
+ }
+
+ let content = gen_deprecated_lints_test(deprecated_lints);
+ process_file("tests/ui/deprecated.rs", update_mode, &content);
+
+ let content = gen_renamed_lints_test(renamed_lints);
+ process_file("tests/ui/rename.rs", update_mode, &content);
+}
+
+pub fn print_lints() {
+ let (lint_list, _, _) = gather_all();
+ let usable_lints = Lint::usable_lints(&lint_list);
+ let usable_lint_count = usable_lints.len();
+ let grouped_by_lint_group = Lint::by_lint_group(usable_lints.into_iter());
+
+ for (lint_group, mut lints) in grouped_by_lint_group {
+ println!("\n## {}", lint_group);
+
+ lints.sort_by_key(|l| l.name.clone());
+
+ for lint in lints {
+ println!("* [{}]({}#{}) ({})", lint.name, DOCS_LINK, lint.name, lint.desc);
+ }
+ }
+
+ println!("there are {} lints", usable_lint_count);
+}
+
+/// Runs the `rename_lint` command.
+///
+/// This does the following:
+/// * Adds an entry to `renamed_lints.rs`.
+/// * Renames all lint attributes to the new name (e.g. `#[allow(clippy::lint_name)]`).
+/// * Renames the lint struct to the new name.
+/// * Renames the module containing the lint struct to the new name if it shares a name with the
+/// lint.
+///
+/// # Panics
+/// Panics for the following conditions:
+/// * If a file path could not read from or then written to
+/// * If either lint name has a prefix
+/// * If `old_name` doesn't name an existing lint.
+/// * If `old_name` names a deprecated or renamed lint.
+#[allow(clippy::too_many_lines)]
+pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
+ if let Some((prefix, _)) = old_name.split_once("::") {
+ panic!("`{}` should not contain the `{}` prefix", old_name, prefix);
+ }
+ if let Some((prefix, _)) = new_name.split_once("::") {
+ panic!("`{}` should not contain the `{}` prefix", new_name, prefix);
+ }
+
+ let (mut lints, deprecated_lints, mut renamed_lints) = gather_all();
+ let mut old_lint_index = None;
+ let mut found_new_name = false;
+ for (i, lint) in lints.iter().enumerate() {
+ if lint.name == old_name {
+ old_lint_index = Some(i);
+ } else if lint.name == new_name {
+ found_new_name = true;
+ }
+ }
+ let old_lint_index = old_lint_index.unwrap_or_else(|| panic!("could not find lint `{}`", old_name));
+
+ let lint = RenamedLint {
+ old_name: format!("clippy::{}", old_name),
+ new_name: if uplift {
+ new_name.into()
+ } else {
+ format!("clippy::{}", new_name)
+ },
+ };
+
+ // Renamed lints and deprecated lints shouldn't have been found in the lint list, but check just in
+ // case.
+ assert!(
+ !renamed_lints.iter().any(|l| lint.old_name == l.old_name),
+ "`{}` has already been renamed",
+ old_name
+ );
+ assert!(
+ !deprecated_lints.iter().any(|l| lint.old_name == l.name),
+ "`{}` has already been deprecated",
+ old_name
+ );
+
+ // Update all lint level attributes. (`clippy::lint_name`)
+ for file in WalkDir::new(clippy_project_root())
+ .into_iter()
+ .map(Result::unwrap)
+ .filter(|f| {
+ let name = f.path().file_name();
+ let ext = f.path().extension();
+ (ext == Some(OsStr::new("rs")) || ext == Some(OsStr::new("fixed")))
+ && name != Some(OsStr::new("rename.rs"))
+ && name != Some(OsStr::new("renamed_lints.rs"))
+ })
+ {
+ rewrite_file(file.path(), |s| {
+ replace_ident_like(s, &[(&lint.old_name, &lint.new_name)])
+ });
+ }
+
+ renamed_lints.push(lint);
+ renamed_lints.sort_by(|lhs, rhs| {
+ lhs.new_name
+ .starts_with("clippy::")
+ .cmp(&rhs.new_name.starts_with("clippy::"))
+ .reverse()
+ .then_with(|| lhs.old_name.cmp(&rhs.old_name))
+ });
+
+ write_file(
+ Path::new("clippy_lints/src/renamed_lints.rs"),
+ &gen_renamed_lints_list(&renamed_lints),
+ );
+
+ if uplift {
+ write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
+ println!(
+ "`{}` has be uplifted. All the code inside `clippy_lints` related to it needs to be removed manually.",
+ old_name
+ );
+ } else if found_new_name {
+ write_file(Path::new("tests/ui/rename.rs"), &gen_renamed_lints_test(&renamed_lints));
+ println!(
+ "`{}` is already defined. The old linting code inside `clippy_lints` needs to be updated/removed manually.",
+ new_name
+ );
+ } else {
+ // Rename the lint struct and source files sharing a name with the lint.
+ let lint = &mut lints[old_lint_index];
+ let old_name_upper = old_name.to_uppercase();
+ let new_name_upper = new_name.to_uppercase();
+ lint.name = new_name.into();
+
+ // Rename test files. only rename `.stderr` and `.fixed` files if the new test name doesn't exist.
+ if try_rename_file(
+ Path::new(&format!("tests/ui/{}.rs", old_name)),
+ Path::new(&format!("tests/ui/{}.rs", new_name)),
+ ) {
+ try_rename_file(
+ Path::new(&format!("tests/ui/{}.stderr", old_name)),
+ Path::new(&format!("tests/ui/{}.stderr", new_name)),
+ );
+ try_rename_file(
+ Path::new(&format!("tests/ui/{}.fixed", old_name)),
+ Path::new(&format!("tests/ui/{}.fixed", new_name)),
+ );
+ }
+
+ // Try to rename the file containing the lint if the file name matches the lint's name.
+ let replacements;
+ let replacements = if lint.module == old_name
+ && try_rename_file(
+ Path::new(&format!("clippy_lints/src/{}.rs", old_name)),
+ Path::new(&format!("clippy_lints/src/{}.rs", new_name)),
+ ) {
+ // Edit the module name in the lint list. Note there could be multiple lints.
+ for lint in lints.iter_mut().filter(|l| l.module == old_name) {
+ lint.module = new_name.into();
+ }
+ replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
+ replacements.as_slice()
+ } else if !lint.module.contains("::")
+ // Catch cases like `methods/lint_name.rs` where the lint is stored in `methods/mod.rs`
+ && try_rename_file(
+ Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, old_name)),
+ Path::new(&format!("clippy_lints/src/{}/{}.rs", lint.module, new_name)),
+ )
+ {
+ // Edit the module name in the lint list. Note there could be multiple lints, or none.
+ let renamed_mod = format!("{}::{}", lint.module, old_name);
+ for lint in lints.iter_mut().filter(|l| l.module == renamed_mod) {
+ lint.module = format!("{}::{}", lint.module, new_name);
+ }
+ replacements = [(&*old_name_upper, &*new_name_upper), (old_name, new_name)];
+ replacements.as_slice()
+ } else {
+ replacements = [(&*old_name_upper, &*new_name_upper), ("", "")];
+ &replacements[0..1]
+ };
+
+ // Don't change `clippy_utils/src/renamed_lints.rs` here as it would try to edit the lint being
+ // renamed.
+ for (_, file) in clippy_lints_src_files().filter(|(rel_path, _)| rel_path != OsStr::new("renamed_lints.rs")) {
+ rewrite_file(file.path(), |s| replace_ident_like(s, replacements));
+ }
+
+ generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
+ println!("{} has been successfully renamed", old_name);
+ }
+
+ println!("note: `cargo uitest` still needs to be run to update the test results");
+}
+
+const DEFAULT_DEPRECATION_REASON: &str = "default deprecation note";
+/// Runs the `deprecate` command
+///
+/// This does the following:
+/// * Adds an entry to `deprecated_lints.rs`.
+/// * Removes the lint declaration (and the entire file if applicable)
+///
+/// # Panics
+///
+/// If a file path could not read from or written to
+pub fn deprecate(name: &str, reason: Option<&String>) {
+ fn finish(
+ (lints, mut deprecated_lints, renamed_lints): (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>),
+ name: &str,
+ reason: &str,
+ ) {
+ deprecated_lints.push(DeprecatedLint {
+ name: name.to_string(),
+ reason: reason.to_string(),
+ declaration_range: Range::default(),
+ });
+
+ generate_lint_files(UpdateMode::Change, &lints, &deprecated_lints, &renamed_lints);
+ println!("info: `{}` has successfully been deprecated", name);
+
+ if reason == DEFAULT_DEPRECATION_REASON {
+ println!("note: the deprecation reason must be updated in `clippy_lints/src/deprecated_lints.rs`");
+ }
+ println!("note: you must run `cargo uitest` to update the test results");
+ }
+
+ let reason = reason.map_or(DEFAULT_DEPRECATION_REASON, String::as_str);
+ let name_lower = name.to_lowercase();
+ let name_upper = name.to_uppercase();
+
+ let (mut lints, deprecated_lints, renamed_lints) = gather_all();
+ let Some(lint) = lints.iter().find(|l| l.name == name_lower) else { eprintln!("error: failed to find lint `{}`", name); return; };
+
+ let mod_path = {
+ let mut mod_path = PathBuf::from(format!("clippy_lints/src/{}", lint.module));
+ if mod_path.is_dir() {
+ mod_path = mod_path.join("mod");
+ }
+
+ mod_path.set_extension("rs");
+ mod_path
+ };
+
+ let deprecated_lints_path = &*clippy_project_root().join("clippy_lints/src/deprecated_lints.rs");
+
+ if remove_lint_declaration(&name_lower, &mod_path, &mut lints).unwrap_or(false) {
+ declare_deprecated(&name_upper, deprecated_lints_path, reason).unwrap();
+ finish((lints, deprecated_lints, renamed_lints), name, reason);
+ return;
+ }
+
+ eprintln!("error: lint not found");
+}
+
+fn remove_lint_declaration(name: &str, path: &Path, lints: &mut Vec<Lint>) -> io::Result<bool> {
+ fn remove_lint(name: &str, lints: &mut Vec<Lint>) {
+ lints.iter().position(|l| l.name == name).map(|pos| lints.remove(pos));
+ }
+
+ fn remove_test_assets(name: &str) {
+ let test_file_stem = format!("tests/ui/{}", name);
+ let path = Path::new(&test_file_stem);
+
+ // Some lints have their own directories, delete them
+ if path.is_dir() {
+ fs::remove_dir_all(path).ok();
+ return;
+ }
+
+ // Remove all related test files
+ fs::remove_file(path.with_extension("rs")).ok();
+ fs::remove_file(path.with_extension("stderr")).ok();
+ fs::remove_file(path.with_extension("fixed")).ok();
+ }
+
+ fn remove_impl_lint_pass(lint_name_upper: &str, content: &mut String) {
+ let impl_lint_pass_start = content.find("impl_lint_pass!").unwrap_or_else(|| {
+ content
+ .find("declare_lint_pass!")
+ .unwrap_or_else(|| panic!("failed to find `impl_lint_pass`"))
+ });
+ let mut impl_lint_pass_end = content[impl_lint_pass_start..]
+ .find(']')
+ .expect("failed to find `impl_lint_pass` terminator");
+
+ impl_lint_pass_end += impl_lint_pass_start;
+ if let Some(lint_name_pos) = content[impl_lint_pass_start..impl_lint_pass_end].find(&lint_name_upper) {
+ let mut lint_name_end = impl_lint_pass_start + (lint_name_pos + lint_name_upper.len());
+ for c in content[lint_name_end..impl_lint_pass_end].chars() {
+ // Remove trailing whitespace
+ if c == ',' || c.is_whitespace() {
+ lint_name_end += 1;
+ } else {
+ break;
+ }
+ }
+
+ content.replace_range(impl_lint_pass_start + lint_name_pos..lint_name_end, "");
+ }
+ }
+
+ if path.exists() {
+ if let Some(lint) = lints.iter().find(|l| l.name == name) {
+ if lint.module == name {
+ // The lint name is the same as the file, we can just delete the entire file
+ fs::remove_file(path)?;
+ } else {
+ // We can't delete the entire file, just remove the declaration
+
+ if let Some(Some("mod.rs")) = path.file_name().map(OsStr::to_str) {
+ // Remove clippy_lints/src/some_mod/some_lint.rs
+ let mut lint_mod_path = path.to_path_buf();
+ lint_mod_path.set_file_name(name);
+ lint_mod_path.set_extension("rs");
+
+ fs::remove_file(lint_mod_path).ok();
+ }
+
+ let mut content =
+ fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read `{}`", path.to_string_lossy()));
+
+ eprintln!(
+ "warn: you will have to manually remove any code related to `{}` from `{}`",
+ name,
+ path.display()
+ );
+
+ assert!(
+ content[lint.declaration_range.clone()].contains(&name.to_uppercase()),
+ "error: `{}` does not contain lint `{}`'s declaration",
+ path.display(),
+ lint.name
+ );
+
+ // Remove lint declaration (declare_clippy_lint!)
+ content.replace_range(lint.declaration_range.clone(), "");
+
+ // Remove the module declaration (mod xyz;)
+ let mod_decl = format!("\nmod {};", name);
+ content = content.replacen(&mod_decl, "", 1);
+
+ remove_impl_lint_pass(&lint.name.to_uppercase(), &mut content);
+ fs::write(path, content).unwrap_or_else(|_| panic!("failed to write to `{}`", path.to_string_lossy()));
+ }
+
+ remove_test_assets(name);
+ remove_lint(name, lints);
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+}
+
+fn declare_deprecated(name: &str, path: &Path, reason: &str) -> io::Result<()> {
+ let mut file = OpenOptions::new().write(true).open(path)?;
+
+ file.seek(SeekFrom::End(0))?;
+
+ let version = crate::new_lint::get_stabilization_version();
+ let deprecation_reason = if reason == DEFAULT_DEPRECATION_REASON {
+ "TODO"
+ } else {
+ reason
+ };
+
+ writedoc!(
+ file,
+ "
+
+ declare_deprecated_lint! {{
+ /// ### What it does
+ /// Nothing. This lint has been deprecated.
+ ///
+ /// ### Deprecation reason
+ /// {}
+ #[clippy::version = \"{}\"]
+ pub {},
+ \"{}\"
+ }}
+
+ ",
+ deprecation_reason,
+ version,
+ name,
+ reason,
+ )
+}
+
+/// Replace substrings if they aren't bordered by identifier characters. Returns `None` if there
+/// were no replacements.
+fn replace_ident_like(contents: &str, replacements: &[(&str, &str)]) -> Option<String> {
+ fn is_ident_char(c: u8) -> bool {
+ matches!(c, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')
+ }
+
+ let searcher = AhoCorasickBuilder::new()
+ .dfa(true)
+ .match_kind(aho_corasick::MatchKind::LeftmostLongest)
+ .build_with_size::<u16, _, _>(replacements.iter().map(|&(x, _)| x.as_bytes()))
+ .unwrap();
+
+ let mut result = String::with_capacity(contents.len() + 1024);
+ let mut pos = 0;
+ let mut edited = false;
+ for m in searcher.find_iter(contents) {
+ let (old, new) = replacements[m.pattern()];
+ result.push_str(&contents[pos..m.start()]);
+ result.push_str(
+ if !is_ident_char(contents.as_bytes().get(m.start().wrapping_sub(1)).copied().unwrap_or(0))
+ && !is_ident_char(contents.as_bytes().get(m.end()).copied().unwrap_or(0))
+ {
+ edited = true;
+ new
+ } else {
+ old
+ },
+ );
+ pos = m.end();
+ }
+ result.push_str(&contents[pos..]);
+ edited.then_some(result)
+}
+
+fn round_to_fifty(count: usize) -> usize {
+ count / 50 * 50
+}
+
+fn process_file(path: impl AsRef<Path>, update_mode: UpdateMode, content: &str) {
+ if update_mode == UpdateMode::Check {
+ let old_content =
+ fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read from {}: {}", path.as_ref().display(), e));
+ if content != old_content {
+ exit_with_failure();
+ }
+ } else {
+ fs::write(&path, content.as_bytes())
+ .unwrap_or_else(|e| panic!("Cannot write to {}: {}", path.as_ref().display(), e));
+ }
+}
+
+fn exit_with_failure() {
+ println!(
+ "Not all lints defined properly. \
+ Please run `cargo dev update_lints` to make sure all lints are defined properly."
+ );
+ std::process::exit(1);
+}
+
+/// Lint data parsed from the Clippy source code.
+#[derive(Clone, PartialEq, Eq, Debug)]
+struct Lint {
+ name: String,
+ group: String,
+ desc: String,
+ module: String,
+ declaration_range: Range<usize>,
+}
+
+impl Lint {
+ #[must_use]
+ fn new(name: &str, group: &str, desc: &str, module: &str, declaration_range: Range<usize>) -> Self {
+ Self {
+ name: name.to_lowercase(),
+ group: group.into(),
+ desc: remove_line_splices(desc),
+ module: module.into(),
+ declaration_range,
+ }
+ }
+
+ /// Returns all non-deprecated lints and non-internal lints
+ #[must_use]
+ fn usable_lints(lints: &[Self]) -> Vec<Self> {
+ lints
+ .iter()
+ .filter(|l| !l.group.starts_with("internal"))
+ .cloned()
+ .collect()
+ }
+
+ /// Returns all internal lints (not `internal_warn` lints)
+ #[must_use]
+ fn internal_lints(lints: &[Self]) -> Vec<Self> {
+ lints.iter().filter(|l| l.group == "internal").cloned().collect()
+ }
+
+ /// Returns the lints in a `HashMap`, grouped by the different lint groups
+ #[must_use]
+ fn by_lint_group(lints: impl Iterator<Item = Self>) -> HashMap<String, Vec<Self>> {
+ lints.map(|lint| (lint.group.to_string(), lint)).into_group_map()
+ }
+}
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+struct DeprecatedLint {
+ name: String,
+ reason: String,
+ declaration_range: Range<usize>,
+}
+impl DeprecatedLint {
+ fn new(name: &str, reason: &str, declaration_range: Range<usize>) -> Self {
+ Self {
+ name: name.to_lowercase(),
+ reason: remove_line_splices(reason),
+ declaration_range,
+ }
+ }
+}
+
+struct RenamedLint {
+ old_name: String,
+ new_name: String,
+}
+impl RenamedLint {
+ fn new(old_name: &str, new_name: &str) -> Self {
+ Self {
+ old_name: remove_line_splices(old_name),
+ new_name: remove_line_splices(new_name),
+ }
+ }
+}
+
+/// Generates the code for registering a group
+fn gen_lint_group_list<'a>(group_name: &str, lints: impl Iterator<Item = &'a Lint>) -> String {
+ let mut details: Vec<_> = lints.map(|l| (&l.module, l.name.to_uppercase())).collect();
+ details.sort_unstable();
+
+ let mut output = GENERATED_FILE_COMMENT.to_string();
+
+ let _ = writeln!(
+ output,
+ "store.register_group(true, \"clippy::{0}\", Some(\"clippy_{0}\"), vec![",
+ group_name
+ );
+ for (module, name) in details {
+ let _ = writeln!(output, " LintId::of({}::{}),", module, name);
+ }
+ output.push_str("])\n");
+
+ output
+}
+
+/// Generates the `register_removed` code
+#[must_use]
+fn gen_deprecated(lints: &[DeprecatedLint]) -> String {
+ let mut output = GENERATED_FILE_COMMENT.to_string();
+ output.push_str("{\n");
+ for lint in lints {
+ let _ = write!(
+ output,
+ concat!(
+ " store.register_removed(\n",
+ " \"clippy::{}\",\n",
+ " \"{}\",\n",
+ " );\n"
+ ),
+ lint.name, lint.reason,
+ );
+ }
+ output.push_str("}\n");
+
+ output
+}
+
+/// Generates the code for registering lints
+#[must_use]
+fn gen_register_lint_list<'a>(
+ internal_lints: impl Iterator<Item = &'a Lint>,
+ usable_lints: impl Iterator<Item = &'a Lint>,
+) -> String {
+ let mut details: Vec<_> = internal_lints
+ .map(|l| (false, &l.module, l.name.to_uppercase()))
+ .chain(usable_lints.map(|l| (true, &l.module, l.name.to_uppercase())))
+ .collect();
+ details.sort_unstable();
+
+ let mut output = GENERATED_FILE_COMMENT.to_string();
+ output.push_str("store.register_lints(&[\n");
+
+ for (is_public, module_name, lint_name) in details {
+ if !is_public {
+ output.push_str(" #[cfg(feature = \"internal\")]\n");
+ }
+ let _ = writeln!(output, " {}::{},", module_name, lint_name);
+ }
+ output.push_str("])\n");
+
+ output
+}
+
+fn gen_deprecated_lints_test(lints: &[DeprecatedLint]) -> String {
+ let mut res: String = GENERATED_FILE_COMMENT.into();
+ for lint in lints {
+ writeln!(res, "#![warn(clippy::{})]", lint.name).unwrap();
+ }
+ res.push_str("\nfn main() {}\n");
+ res
+}
+
+fn gen_renamed_lints_test(lints: &[RenamedLint]) -> String {
+ let mut seen_lints = HashSet::new();
+ let mut res: String = GENERATED_FILE_COMMENT.into();
+ res.push_str("// run-rustfix\n\n");
+ for lint in lints {
+ if seen_lints.insert(&lint.new_name) {
+ writeln!(res, "#![allow({})]", lint.new_name).unwrap();
+ }
+ }
+ seen_lints.clear();
+ for lint in lints {
+ if seen_lints.insert(&lint.old_name) {
+ writeln!(res, "#![warn({})]", lint.old_name).unwrap();
+ }
+ }
+ res.push_str("\nfn main() {}\n");
+ res
+}
+
+fn gen_renamed_lints_list(lints: &[RenamedLint]) -> String {
+ const HEADER: &str = "\
+ // This file is managed by `cargo dev rename_lint`. Prefer using that when possible.\n\n\
+ #[rustfmt::skip]\n\
+ pub static RENAMED_LINTS: &[(&str, &str)] = &[\n";
+
+ let mut res = String::from(HEADER);
+ for lint in lints {
+ writeln!(res, " (\"{}\", \"{}\"),", lint.old_name, lint.new_name).unwrap();
+ }
+ res.push_str("];\n");
+ res
+}
+
+/// Gathers all lints defined in `clippy_lints/src`
+fn gather_all() -> (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>) {
+ let mut lints = Vec::with_capacity(1000);
+ let mut deprecated_lints = Vec::with_capacity(50);
+ let mut renamed_lints = Vec::with_capacity(50);
+
+ for (rel_path, file) in clippy_lints_src_files() {
+ let path = file.path();
+ let contents =
+ fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
+ let module = rel_path
+ .components()
+ .map(|c| c.as_os_str().to_str().unwrap())
+ .collect::<Vec<_>>()
+ .join("::");
+
+ // If the lints are stored in mod.rs, we get the module name from
+ // the containing directory:
+ let module = if let Some(module) = module.strip_suffix("::mod.rs") {
+ module
+ } else {
+ module.strip_suffix(".rs").unwrap_or(&module)
+ };
+
+ match module {
+ "deprecated_lints" => parse_deprecated_contents(&contents, &mut deprecated_lints),
+ "renamed_lints" => parse_renamed_contents(&contents, &mut renamed_lints),
+ _ => parse_contents(&contents, module, &mut lints),
+ }
+ }
+ (lints, deprecated_lints, renamed_lints)
+}
+
+fn clippy_lints_src_files() -> impl Iterator<Item = (PathBuf, DirEntry)> {
+ let root_path = clippy_project_root().join("clippy_lints/src");
+ let iter = WalkDir::new(&root_path).into_iter();
+ iter.map(Result::unwrap)
+ .filter(|f| f.path().extension() == Some(OsStr::new("rs")))
+ .map(move |f| (f.path().strip_prefix(&root_path).unwrap().to_path_buf(), f))
+}
+
+macro_rules! match_tokens {
+ ($iter:ident, $($token:ident $({$($fields:tt)*})? $(($capture:ident))?)*) => {
+ {
+ $($(let $capture =)? if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::$token $({$($fields)*})?,
+ content: _x,
+ ..
+ }) = $iter.next() {
+ _x
+ } else {
+ continue;
+ };)*
+ #[allow(clippy::unused_unit)]
+ { ($($($capture,)?)*) }
+ }
+ }
+}
+
+pub(crate) use match_tokens;
+
+pub(crate) struct LintDeclSearchResult<'a> {
+ pub token_kind: TokenKind,
+ pub content: &'a str,
+ pub range: Range<usize>,
+}
+
+/// Parse a source file looking for `declare_clippy_lint` macro invocations.
+fn parse_contents(contents: &str, module: &str, lints: &mut Vec<Lint>) {
+ let mut offset = 0usize;
+ let mut iter = tokenize(contents).map(|t| {
+ let range = offset..offset + t.len;
+ offset = range.end;
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &contents[range.clone()],
+ range,
+ }
+ });
+
+ while let Some(LintDeclSearchResult { range, .. }) = iter.find(
+ |LintDeclSearchResult {
+ token_kind, content, ..
+ }| token_kind == &TokenKind::Ident && *content == "declare_clippy_lint",
+ ) {
+ let start = range.start;
+
+ let mut iter = iter
+ .by_ref()
+ .filter(|t| !matches!(t.token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. }));
+ // matches `!{`
+ match_tokens!(iter, Bang OpenBrace);
+ match iter.next() {
+ // #[clippy::version = "version"] pub
+ Some(LintDeclSearchResult {
+ token_kind: TokenKind::Pound,
+ ..
+ }) => {
+ match_tokens!(iter, OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket Ident);
+ },
+ // pub
+ Some(LintDeclSearchResult {
+ token_kind: TokenKind::Ident,
+ ..
+ }) => (),
+ _ => continue,
+ }
+
+ let (name, group, desc) = match_tokens!(
+ iter,
+ // LINT_NAME
+ Ident(name) Comma
+ // group,
+ Ident(group) Comma
+ // "description"
+ Literal{..}(desc)
+ );
+
+ if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::CloseBrace,
+ range,
+ ..
+ }) = iter.next()
+ {
+ lints.push(Lint::new(name, group, desc, module, start..range.end));
+ }
+ }
+}
+
+/// Parse a source file looking for `declare_deprecated_lint` macro invocations.
+fn parse_deprecated_contents(contents: &str, lints: &mut Vec<DeprecatedLint>) {
+ let mut offset = 0usize;
+ let mut iter = tokenize(contents).map(|t| {
+ let range = offset..offset + t.len;
+ offset = range.end;
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &contents[range.clone()],
+ range,
+ }
+ });
+
+ while let Some(LintDeclSearchResult { range, .. }) = iter.find(
+ |LintDeclSearchResult {
+ token_kind, content, ..
+ }| token_kind == &TokenKind::Ident && *content == "declare_deprecated_lint",
+ ) {
+ let start = range.start;
+
+ let mut iter = iter.by_ref().filter(|LintDeclSearchResult { ref token_kind, .. }| {
+ !matches!(token_kind, TokenKind::Whitespace | TokenKind::LineComment { .. })
+ });
+ let (name, reason) = match_tokens!(
+ iter,
+ // !{
+ Bang OpenBrace
+ // #[clippy::version = "version"]
+ Pound OpenBracket Ident Colon Colon Ident Eq Literal{..} CloseBracket
+ // pub LINT_NAME,
+ Ident Ident(name) Comma
+ // "description"
+ Literal{kind: LiteralKind::Str{..},..}(reason)
+ );
+
+ if let Some(LintDeclSearchResult {
+ token_kind: TokenKind::CloseBrace,
+ range,
+ ..
+ }) = iter.next()
+ {
+ lints.push(DeprecatedLint::new(name, reason, start..range.end));
+ }
+ }
+}
+
+fn parse_renamed_contents(contents: &str, lints: &mut Vec<RenamedLint>) {
+ for line in contents.lines() {
+ let mut offset = 0usize;
+ let mut iter = tokenize(line).map(|t| {
+ let range = offset..offset + t.len;
+ offset = range.end;
+
+ LintDeclSearchResult {
+ token_kind: t.kind,
+ content: &line[range.clone()],
+ range,
+ }
+ });
+
+ let (old_name, new_name) = match_tokens!(
+ iter,
+ // ("old_name",
+ Whitespace OpenParen Literal{kind: LiteralKind::Str{..},..}(old_name) Comma
+ // "new_name"),
+ Whitespace Literal{kind: LiteralKind::Str{..},..}(new_name) CloseParen Comma
+ );
+ lints.push(RenamedLint::new(old_name, new_name));
+ }
+}
+
+/// Removes the line splices and surrounding quotes from a string literal
+fn remove_line_splices(s: &str) -> String {
+ let s = s
+ .strip_prefix('r')
+ .unwrap_or(s)
+ .trim_matches('#')
+ .strip_prefix('"')
+ .and_then(|s| s.strip_suffix('"'))
+ .unwrap_or_else(|| panic!("expected quoted string, found `{}`", s));
+ let mut res = String::with_capacity(s.len());
+ unescape::unescape_literal(s, unescape::Mode::Str, &mut |range, _| res.push_str(&s[range]));
+ res
+}
+
+/// Replaces a region in a file delimited by two lines matching regexes.
+///
+/// `path` is the relative path to the file on which you want to perform the replacement.
+///
+/// See `replace_region_in_text` for documentation of the other options.
+///
+/// # Panics
+///
+/// Panics if the path could not read or then written
+fn replace_region_in_file(
+ update_mode: UpdateMode,
+ path: &Path,
+ start: &str,
+ end: &str,
+ write_replacement: impl FnMut(&mut String),
+) {
+ let contents = fs::read_to_string(path).unwrap_or_else(|e| panic!("Cannot read from `{}`: {}", path.display(), e));
+ let new_contents = match replace_region_in_text(&contents, start, end, write_replacement) {
+ Ok(x) => x,
+ Err(delim) => panic!("Couldn't find `{}` in file `{}`", delim, path.display()),
+ };
+
+ match update_mode {
+ UpdateMode::Check if contents != new_contents => exit_with_failure(),
+ UpdateMode::Check => (),
+ UpdateMode::Change => {
+ if let Err(e) = fs::write(path, new_contents.as_bytes()) {
+ panic!("Cannot write to `{}`: {}", path.display(), e);
+ }
+ },
+ }
+}
+
+/// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
+/// were found, or the missing delimiter if not.
+fn replace_region_in_text<'a>(
+ text: &str,
+ start: &'a str,
+ end: &'a str,
+ mut write_replacement: impl FnMut(&mut String),
+) -> Result<String, &'a str> {
+ let (text_start, rest) = text.split_once(start).ok_or(start)?;
+ let (_, text_end) = rest.split_once(end).ok_or(end)?;
+
+ let mut res = String::with_capacity(text.len() + 4096);
+ res.push_str(text_start);
+ res.push_str(start);
+ write_replacement(&mut res);
+ res.push_str(end);
+ res.push_str(text_end);
+
+ Ok(res)
+}
+
+fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
+ match fs::OpenOptions::new().create_new(true).write(true).open(new_name) {
+ Ok(file) => drop(file),
+ Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false,
+ Err(e) => panic_file(e, new_name, "create"),
+ };
+ match fs::rename(old_name, new_name) {
+ Ok(()) => true,
+ Err(e) => {
+ drop(fs::remove_file(new_name));
+ if e.kind() == io::ErrorKind::NotFound {
+ false
+ } else {
+ panic_file(e, old_name, "rename");
+ }
+ },
+ }
+}
+
+#[allow(clippy::needless_pass_by_value)]
+fn panic_file(error: io::Error, name: &Path, action: &str) -> ! {
+ panic!("failed to {} file `{}`: {}", action, name.display(), error)
+}
+
+fn rewrite_file(path: &Path, f: impl FnOnce(&str) -> Option<String>) {
+ let mut file = fs::OpenOptions::new()
+ .write(true)
+ .read(true)
+ .open(path)
+ .unwrap_or_else(|e| panic_file(e, path, "open"));
+ let mut buf = String::new();
+ file.read_to_string(&mut buf)
+ .unwrap_or_else(|e| panic_file(e, path, "read"));
+ if let Some(new_contents) = f(&buf) {
+ file.rewind().unwrap_or_else(|e| panic_file(e, path, "write"));
+ file.write_all(new_contents.as_bytes())
+ .unwrap_or_else(|e| panic_file(e, path, "write"));
+ file.set_len(new_contents.len() as u64)
+ .unwrap_or_else(|e| panic_file(e, path, "write"));
+ }
+}
+
+fn write_file(path: &Path, contents: &str) {
+ fs::write(path, contents).unwrap_or_else(|e| panic_file(e, path, "write"));
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_contents() {
+ static CONTENTS: &str = r#"
+ declare_clippy_lint! {
+ #[clippy::version = "Hello Clippy!"]
+ pub PTR_ARG,
+ style,
+ "really long \
+ text"
+ }
+
+ declare_clippy_lint!{
+ #[clippy::version = "Test version"]
+ pub DOC_MARKDOWN,
+ pedantic,
+ "single line"
+ }
+ "#;
+ let mut result = Vec::new();
+ parse_contents(CONTENTS, "module_name", &mut result);
+ for r in &mut result {
+ r.declaration_range = Range::default();
+ }
+
+ let expected = vec![
+ Lint::new(
+ "ptr_arg",
+ "style",
+ "\"really long text\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "doc_markdown",
+ "pedantic",
+ "\"single line\"",
+ "module_name",
+ Range::default(),
+ ),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_parse_deprecated_contents() {
+ static DEPRECATED_CONTENTS: &str = r#"
+ /// some doc comment
+ declare_deprecated_lint! {
+ #[clippy::version = "I'm a version"]
+ pub SHOULD_ASSERT_EQ,
+ "`assert!()` will be more flexible with RFC 2011"
+ }
+ "#;
+
+ let mut result = Vec::new();
+ parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
+ for r in &mut result {
+ r.declaration_range = Range::default();
+ }
+
+ let expected = vec![DeprecatedLint::new(
+ "should_assert_eq",
+ "\"`assert!()` will be more flexible with RFC 2011\"",
+ Range::default(),
+ )];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_usable_lints() {
+ let lints = vec![
+ Lint::new(
+ "should_assert_eq2",
+ "Not Deprecated",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "should_assert_eq2",
+ "internal",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new(
+ "should_assert_eq2",
+ "internal_style",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ ];
+ let expected = vec![Lint::new(
+ "should_assert_eq2",
+ "Not Deprecated",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ )];
+ assert_eq!(expected, Lint::usable_lints(&lints));
+ }
+
+ #[test]
+ fn test_by_lint_group() {
+ let lints = vec![
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new(
+ "should_assert_eq2",
+ "group2",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ ),
+ Lint::new("incorrect_match", "group1", "\"abc\"", "module_name", Range::default()),
+ ];
+ let mut expected: HashMap<String, Vec<Lint>> = HashMap::new();
+ expected.insert(
+ "group1".to_string(),
+ vec![
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("incorrect_match", "group1", "\"abc\"", "module_name", Range::default()),
+ ],
+ );
+ expected.insert(
+ "group2".to_string(),
+ vec![Lint::new(
+ "should_assert_eq2",
+ "group2",
+ "\"abc\"",
+ "module_name",
+ Range::default(),
+ )],
+ );
+ assert_eq!(expected, Lint::by_lint_group(lints.into_iter()));
+ }
+
+ #[test]
+ fn test_gen_deprecated() {
+ let lints = vec![
+ DeprecatedLint::new(
+ "should_assert_eq",
+ "\"has been superseded by should_assert_eq2\"",
+ Range::default(),
+ ),
+ DeprecatedLint::new("another_deprecated", "\"will be removed\"", Range::default()),
+ ];
+
+ let expected = GENERATED_FILE_COMMENT.to_string()
+ + &[
+ "{",
+ " store.register_removed(",
+ " \"clippy::should_assert_eq\",",
+ " \"has been superseded by should_assert_eq2\",",
+ " );",
+ " store.register_removed(",
+ " \"clippy::another_deprecated\",",
+ " \"will be removed\",",
+ " );",
+ "}",
+ ]
+ .join("\n")
+ + "\n";
+
+ assert_eq!(expected, gen_deprecated(&lints));
+ }
+
+ #[test]
+ fn test_gen_lint_group_list() {
+ let lints = vec![
+ Lint::new("abc", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
+ Lint::new("internal", "internal_style", "\"abc\"", "module_name", Range::default()),
+ ];
+ let expected = GENERATED_FILE_COMMENT.to_string()
+ + &[
+ "store.register_group(true, \"clippy::group1\", Some(\"clippy_group1\"), vec![",
+ " LintId::of(module_name::ABC),",
+ " LintId::of(module_name::INTERNAL),",
+ " LintId::of(module_name::SHOULD_ASSERT_EQ),",
+ "])",
+ ]
+ .join("\n")
+ + "\n";
+
+ let result = gen_lint_group_list("group1", lints.iter());
+
+ assert_eq!(expected, result);
+ }
+}