From 698f8c2f01ea549d77d7dc3338a12e04c11057b9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 14:02:58 +0200 Subject: Adding upstream version 1.64.0+dfsg1. Signed-off-by: Daniel Baumann --- src/tools/clippy/clippy_dev/Cargo.toml | 21 + src/tools/clippy/clippy_dev/src/bless.rs | 60 + src/tools/clippy/clippy_dev/src/dogfood.rs | 33 + src/tools/clippy/clippy_dev/src/fmt.rs | 226 ++++ src/tools/clippy/clippy_dev/src/lib.rs | 58 + src/tools/clippy/clippy_dev/src/lint.rs | 55 + src/tools/clippy/clippy_dev/src/main.rs | 314 +++++ src/tools/clippy/clippy_dev/src/new_lint.rs | 575 ++++++++++ src/tools/clippy/clippy_dev/src/serve.rs | 65 ++ src/tools/clippy/clippy_dev/src/setup/git_hook.rs | 85 ++ src/tools/clippy/clippy_dev/src/setup/intellij.rs | 223 ++++ src/tools/clippy/clippy_dev/src/setup/mod.rs | 23 + src/tools/clippy/clippy_dev/src/setup/vscode.rs | 104 ++ src/tools/clippy/clippy_dev/src/update_lints.rs | 1277 +++++++++++++++++++++ 14 files changed, 3119 insertions(+) create mode 100644 src/tools/clippy/clippy_dev/Cargo.toml create mode 100644 src/tools/clippy/clippy_dev/src/bless.rs create mode 100644 src/tools/clippy/clippy_dev/src/dogfood.rs create mode 100644 src/tools/clippy/clippy_dev/src/fmt.rs create mode 100644 src/tools/clippy/clippy_dev/src/lib.rs create mode 100644 src/tools/clippy/clippy_dev/src/lint.rs create mode 100644 src/tools/clippy/clippy_dev/src/main.rs create mode 100644 src/tools/clippy/clippy_dev/src/new_lint.rs create mode 100644 src/tools/clippy/clippy_dev/src/serve.rs create mode 100644 src/tools/clippy/clippy_dev/src/setup/git_hook.rs create mode 100644 src/tools/clippy/clippy_dev/src/setup/intellij.rs create mode 100644 src/tools/clippy/clippy_dev/src/setup/mod.rs create mode 100644 src/tools/clippy/clippy_dev/src/setup/vscode.rs create mode 100644 src/tools/clippy/clippy_dev/src/update_lints.rs (limited to 'src/tools/clippy/clippy_dev') 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> = + 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 { + 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 for CliError { + fn from(error: io::Error) -> Self { + Self::IoError(error) + } +} + +impl From 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 { + 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, dir: impl AsRef, args: &[impl AsRef]) -> 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, + dir: impl AsRef, + args: &[impl AsRef], +) -> Result { + 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 { + 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) -> Result { + 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) { + 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) { + 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::("pass"), + matches.get_one::("name"), + matches.get_one::("category").map(String::as_str), + matches.get_one::("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::("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::("port").unwrap(); + let lint = matches.get_one::("lint"); + serve::run(port, lint); + }, + Some(("lint", matches)) => { + let path = matches.get_one::("path").unwrap(); + let args = matches.get_many::("args").into_iter().flatten(); + lint::run(path, args); + }, + Some(("rename_lint", matches)) => { + let old_name = matches.get_one::("old_name").unwrap(); + let new_name = matches.get_one::("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::("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>(self, text: C) -> Self; +} + +impl Context for io::Result { + fn context>(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>(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, 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 { + 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::().ok()?, + patch.parse::().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, + }} + + impl {name_camel} {{ + #[must_use] + pub fn new(msrv: Option) -> 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) {{ + 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) -> 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::().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 { + 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 { + 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"), + "\n", + "", + |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, Vec, Vec), + 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) -> io::Result { + fn remove_lint(name: &str, lints: &mut Vec) { + 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 { + 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::(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, 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, +} + +impl Lint { + #[must_use] + fn new(name: &str, group: &str, desc: &str, module: &str, declaration_range: Range) -> 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 { + 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 { + 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) -> HashMap> { + 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, +} +impl DeprecatedLint { + fn new(name: &str, reason: &str, declaration_range: Range) -> 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) -> 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, + usable_lints: impl Iterator, +) -> 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, Vec, Vec) { + 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::>() + .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 { + 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, +} + +/// Parse a source file looking for `declare_clippy_lint` macro invocations. +fn parse_contents(contents: &str, module: &str, lints: &mut Vec) { + 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) { + 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) { + 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 { + 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) { + 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> = 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); + } +} -- cgit v1.2.3