Type safe CLI implementation for clippy-dev

Use the derive feature of `clap` to generate CLI of clippy-dev. Adding new
commands will be easier in the future and we get better compile time checking
through exhaustive matching.
This commit is contained in:
Philipp Krones 2024-05-02 13:51:10 +02:00
parent 993d8ae2a7
commit a0d562a183
No known key found for this signature in database
GPG Key ID: 1CA0DF2AF59D68A5
5 changed files with 269 additions and 332 deletions

View File

@ -1,11 +1,12 @@
[package]
name = "clippy_dev"
description = "Clippy developer tooling"
version = "0.0.1"
edition = "2021"
[dependencies]
aho-corasick = "1.0"
clap = "4.1.4"
clap = { version = "4.1.4", features = ["derive"] }
indoc = "1.0"
itertools = "0.12"
opener = "0.6"

View File

@ -2,350 +2,292 @@
// warn on lints, that are included in `rust-lang/rust`s bootstrap
#![warn(rust_2018_idioms, unused_lifetimes)]
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap::{Args, Parser, Subcommand};
use clippy_dev::{dogfood, fmt, lint, new_lint, serve, setup, update_lints};
use indoc::indoc;
use std::convert::Infallible;
fn main() {
let matches = get_clap_config();
let dev = Dev::parse();
match matches.subcommand() {
Some(("bless", _)) => {
match dev.command {
DevCommand::Bless => {
eprintln!("use `cargo bless` to automatically replace `.stderr` and `.fixed` files as tests are being run");
},
Some(("dogfood", matches)) => {
dogfood::dogfood(
matches.get_flag("fix"),
matches.get_flag("allow-dirty"),
matches.get_flag("allow-staged"),
);
},
Some(("fmt", matches)) => {
fmt::run(matches.get_flag("check"), matches.get_flag("verbose"));
},
Some(("update_lints", matches)) => {
if matches.get_flag("print-only") {
DevCommand::Dogfood {
fix,
allow_dirty,
allow_staged,
} => dogfood::dogfood(fix, allow_dirty, allow_staged),
DevCommand::Fmt { check, verbose } => fmt::run(check, verbose),
DevCommand::UpdateLints { print_only, check } => {
if print_only {
update_lints::print_lints();
} else if matches.get_flag("check") {
} else if check {
update_lints::update(update_lints::UpdateMode::Check);
} else {
update_lints::update(update_lints::UpdateMode::Change);
}
},
Some(("new_lint", matches)) => {
match new_lint::create(
matches.get_one::<String>("pass").unwrap(),
matches.get_one::<String>("name"),
matches.get_one::<String>("category").map(String::as_str),
matches.get_one::<String>("type").map(String::as_str),
matches.get_flag("msrv"),
) {
Ok(()) => update_lints::update(update_lints::UpdateMode::Change),
Err(e) => eprintln!("Unable to create lint: {e}"),
}
DevCommand::NewLint {
pass,
name,
category,
r#type,
msrv,
} => match new_lint::create(&pass, &name, &category, r#type.as_deref(), 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(("git-hook", matches)) => {
if matches.get_flag("remove") {
setup::git_hook::remove_hook();
} else {
setup::git_hook::install_hook(matches.get_flag("force-override"));
}
},
Some(("intellij", matches)) => {
if matches.get_flag("remove") {
DevCommand::Setup(SetupCommand { subcommand }) => match subcommand {
SetupSubcommand::Intellij { remove, repo_path } => {
if remove {
setup::intellij::remove_rustc_src();
} else {
setup::intellij::setup_rustc_src(
matches
.get_one::<String>("rustc-repo-path")
.expect("this field is mandatory and therefore always valid"),
);
setup::intellij::setup_rustc_src(&repo_path);
}
},
Some(("toolchain", matches)) => {
setup::toolchain::create(
matches.get_flag("force"),
matches.get_flag("release"),
matches.get_one::<String>("name").unwrap(),
);
SetupSubcommand::GitHook { remove, force_override } => {
if remove {
setup::git_hook::remove_hook();
} else {
setup::git_hook::install_hook(force_override);
}
},
Some(("vscode-tasks", matches)) => {
if matches.get_flag("remove") {
SetupSubcommand::Toolchain { force, release, name } => setup::toolchain::create(force, release, &name),
SetupSubcommand::VscodeTasks { remove, force_override } => {
if remove {
setup::vscode::remove_tasks();
} else {
setup::vscode::install_tasks(matches.get_flag("force-override"));
setup::vscode::install_tasks(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(),
_ => {},
DevCommand::Remove(RemoveCommand { subcommand }) => match subcommand {
RemoveSubcommand::Intellij => setup::intellij::remove_rustc_src(),
RemoveSubcommand::GitHook => setup::git_hook::remove_hook(),
RemoveSubcommand::VscodeTasks => setup::vscode::remove_tasks(),
},
Some(("serve", matches)) => {
let port = *matches.get_one::<u16>("port").unwrap();
let lint = matches.get_one::<String>("lint");
serve::run(port, lint);
},
Some(("lint", matches)) => {
let path = matches.get_one::<String>("path").unwrap();
let args = matches.get_many::<String>("args").into_iter().flatten();
lint::run(path, args);
},
Some(("rename_lint", matches)) => {
let old_name = matches.get_one::<String>("old_name").unwrap();
let new_name = matches.get_one::<String>("new_name").unwrap_or(old_name);
let uplift = matches.get_flag("uplift");
update_lints::rename(old_name, new_name, uplift);
},
Some(("deprecate", matches)) => {
let name = matches.get_one::<String>("name").unwrap();
let reason = matches.get_one("reason");
update_lints::deprecate(name, reason);
},
_ => {},
DevCommand::Serve { port, lint } => serve::run(port, lint),
DevCommand::Lint { path, args } => lint::run(&path, args.iter()),
DevCommand::RenameLint {
old_name,
new_name,
uplift,
} => update_lints::rename(&old_name, new_name.as_ref().unwrap_or(&old_name), uplift),
DevCommand::Deprecate { name, reason } => update_lints::deprecate(&name, reason.as_deref()),
}
}
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")
.action(ArgAction::SetTrue)
.help("Include files updated before clippy was built"),
),
Command::new("dogfood").about("Runs the dogfood test").args([
Arg::new("fix")
.long("fix")
.action(ArgAction::SetTrue)
.help("Apply the suggestions when possible"),
Arg::new("allow-dirty")
.long("allow-dirty")
.action(ArgAction::SetTrue)
.help("Fix code even if the working directory has changes")
.requires("fix"),
Arg::new("allow-staged")
.long("allow-staged")
.action(ArgAction::SetTrue)
.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")
.action(ArgAction::SetTrue)
.help("Use the rustfmt --check option"),
Arg::new("verbose")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue)
.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")
.action(ArgAction::SetTrue)
.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")
.action(ArgAction::SetTrue)
.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")
.value_parser(["early", "late"])
.conflicts_with("type")
.default_value("late"),
Arg::new("name")
.short('n')
.long("name")
.help("Name of the new lint in snake case, ex: fn_too_long")
.required(true)
.value_parser(|name: &str| Ok::<_, Infallible>(name.replace('-', "_"))),
Arg::new("category")
.short('c')
.long("category")
.help("What category the lint belongs to")
.default_value("nursery")
.value_parser([
"style",
"correctness",
"suspicious",
"complexity",
"perf",
"pedantic",
"restriction",
"cargo",
"nursery",
"internal",
]),
Arg::new("type").long("type").help("What directory the lint belongs in"),
Arg::new("msrv")
.long("msrv")
.action(ArgAction::SetTrue)
.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("git-hook")
.about("Add a pre-commit git hook that formats your code to make it look pretty")
.args([
Arg::new("remove")
.long("remove")
.action(ArgAction::SetTrue)
.help("Remove the pre-commit hook added with 'cargo dev setup git-hook'"),
Arg::new("force-override")
.long("force-override")
.short('f')
.action(ArgAction::SetTrue)
.help("Forces the override of an existing git pre-commit hook"),
]),
Command::new("intellij")
.about("Alter dependencies so Intellij Rust can find rustc internals")
.args([
Arg::new("remove")
.long("remove")
.action(ArgAction::SetTrue)
.help("Remove the dependencies added with 'cargo dev setup intellij'"),
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")
.value_name("path")
.conflicts_with("remove")
.required(true),
]),
Command::new("toolchain")
.about("Install a rustup toolchain pointing to the local clippy build")
.args([
Arg::new("force")
.long("force")
.short('f')
.action(ArgAction::SetTrue)
.help("Override an existing toolchain"),
Arg::new("release")
.long("release")
.short('r')
.action(ArgAction::SetTrue)
.help("Point to --release clippy binaries"),
Arg::new("name")
.long("name")
.default_value("clippy")
.help("The name of the created toolchain"),
]),
Command::new("vscode-tasks")
.about("Add several tasks to vscode for formatting, validation and testing")
.args([
Arg::new("remove")
.long("remove")
.action(ArgAction::SetTrue)
.help("Remove the tasks added with 'cargo dev setup vscode-tasks'"),
Arg::new("force-override")
.long("force-override")
.short('f')
.action(ArgAction::SetTrue)
.help("Forces the override of existing vscode tasks"),
]),
]),
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")
.action(ArgAction::SetTrue)
.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')
.help("The reason for deprecation"),
]),
])
.get_matches()
#[derive(Parser)]
#[command(name = "dev", about)]
struct Dev {
#[command(subcommand)]
command: DevCommand,
}
#[derive(Subcommand)]
enum DevCommand {
/// Bless the test output changes
Bless,
/// Runs the dogfood test
Dogfood {
#[arg(long)]
/// Apply the suggestions when possible
fix: bool,
#[arg(long, requires = "fix")]
/// Fix code even if the working directory has changes
allow_dirty: bool,
#[arg(long, requires = "fix")]
/// Fix code even if the working directory has staged changes
allow_staged: bool,
},
/// Run rustfmt on all projects and tests
Fmt {
#[arg(long)]
/// Use the rustfmt --check option
check: bool,
#[arg(short, long)]
/// Echo commands run
verbose: bool,
},
#[command(name = "update_lints")]
/// Updates lint registration and information from the source code
///
/// 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
UpdateLints {
#[arg(long)]
/// Print a table of lints to STDOUT
///
/// This does not include deprecated and internal lints. (Does not modify any files)
print_only: bool,
#[arg(long)]
/// Checks that `cargo dev update_lints` has been run. Used on CI.
check: bool,
},
#[command(name = "new_lint")]
/// Create a new lint and run `cargo dev update_lints`
NewLint {
#[arg(short, long, value_parser = ["early", "late"], conflicts_with = "type", default_value = "late")]
/// Specify whether the lint runs during the early or late pass
pass: String,
#[arg(
short,
long,
value_parser = |name: &str| Ok::<_, Infallible>(name.replace('-', "_")),
)]
/// Name of the new lint in snake case, ex: `fn_too_long`
name: String,
#[arg(
short,
long,
value_parser = [
"style",
"correctness",
"suspicious",
"complexity",
"perf",
"pedantic",
"restriction",
"cargo",
"nursery",
"internal",
],
default_value = "nursery",
)]
/// What category the lint belongs to
category: String,
#[arg(long)]
/// What directory the lint belongs in
r#type: Option<String>,
#[arg(long)]
/// Add MSRV config code to the lint
msrv: bool,
},
/// Support for setting up your personal development environment
Setup(SetupCommand),
/// Support for removing changes done by the setup command
Remove(RemoveCommand),
/// Launch a local 'ALL the Clippy Lints' website in a browser
Serve {
#[arg(short, long, default_value = "8000")]
/// Local port for the http server
port: u16,
#[arg(long)]
/// Which lint's page to load initially (optional)
lint: Option<String>,
},
#[allow(clippy::doc_markdown)]
/// Manually run clippy on a file or package
///
/// ## Examples
///
/// Lint a single file: {n}
/// cargo dev lint tests/ui/attrs.rs
///
/// Lint a package directory: {n}
/// cargo dev lint tests/ui-cargo/wildcard_dependencies/fail {n}
/// cargo dev lint ~/my-project
///
/// Run rustfix: {n}
/// cargo dev lint ~/my-project -- --fix
///
/// Set lint levels: {n}
/// cargo dev lint file.rs -- -W clippy::pedantic {n}
/// cargo dev lint ~/my-project -- -- -W clippy::pedantic
Lint {
/// The path to a file or package directory to lint
path: String,
/// Pass extra arguments to cargo/clippy-driver
args: Vec<String>,
},
#[command(name = "rename_lint")]
/// Rename a lint
RenameLint {
/// The name of the lint to rename
old_name: String,
#[arg(required_unless_present = "uplift")]
/// The new name of the lint
new_name: Option<String>,
#[arg(long)]
/// This lint will be uplifted into rustc
uplift: bool,
},
/// Deprecate the given lint
Deprecate {
/// The name of the lint to deprecate
name: String,
#[arg(long, short)]
/// The reason for deprecation
reason: Option<String>,
},
}
#[derive(Args)]
struct SetupCommand {
#[command(subcommand)]
subcommand: SetupSubcommand,
}
#[derive(Subcommand)]
enum SetupSubcommand {
/// Alter dependencies so Intellij Rust can find rustc internals
Intellij {
#[arg(long)]
/// Remove the dependencies added with 'cargo dev setup intellij'
remove: bool,
#[arg(long, short, conflicts_with = "remove")]
/// The path to a rustc repo that will be used for setting the dependencies
repo_path: String,
},
/// Add a pre-commit git hook that formats your code to make it look pretty
GitHook {
#[arg(long)]
/// Remove the pre-commit hook added with 'cargo dev setup git-hook'
remove: bool,
#[arg(long, short)]
/// Forces the override of an existing git pre-commit hook
force_override: bool,
},
/// Install a rustup toolchain pointing to the local clippy build
Toolchain {
#[arg(long, short)]
/// Override an existing toolchain
force: bool,
#[arg(long, short)]
/// Point to --release clippy binary
release: bool,
#[arg(long, default_value = "clippy")]
/// Name of the toolchain
name: String,
},
/// Add several tasks to vscode for formatting, validation and testing
VscodeTasks {
#[arg(long)]
/// Remove the tasks added with 'cargo dev setup vscode-tasks'
remove: bool,
#[arg(long, short)]
/// Forces the override of existing vscode tasks
force_override: bool,
},
}
#[derive(Args)]
struct RemoveCommand {
#[command(subcommand)]
subcommand: RemoveSubcommand,
}
#[derive(Subcommand)]
enum RemoveSubcommand {
/// Remove the dependencies added with 'cargo dev setup intellij'
Intellij,
/// Remove the pre-commit git hook
GitHook,
/// Remove the tasks added with 'cargo dev setup vscode-tasks'
VscodeTasks,
}

View File

@ -36,22 +36,16 @@ fn context<C: AsRef<str>>(self, text: C) -> Self {
/// # Errors
///
/// This function errors out if the files couldn't be created or written to.
pub fn create(
pass: &String,
lint_name: Option<&String>,
category: Option<&str>,
mut ty: Option<&str>,
msrv: bool,
) -> io::Result<()> {
if category == Some("cargo") && ty.is_none() {
pub fn create(pass: &str, name: &str, category: &str, mut ty: Option<&str>, msrv: bool) -> io::Result<()> {
if category == "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,
name: lint_name.expect("`name` argument is validated by clap"),
category: category.expect("`category` argument is validated by clap"),
name,
category,
ty,
project_root: clippy_project_root(),
};

View File

@ -8,7 +8,7 @@
/// # Panics
///
/// Panics if the python commands could not be spawned
pub fn run(port: u16, lint: Option<&String>) -> ! {
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}"),

View File

@ -314,7 +314,7 @@ pub fn rename(old_name: &str, new_name: &str, uplift: bool) {
/// # Panics
///
/// If a file path could not read from or written to
pub fn deprecate(name: &str, reason: Option<&String>) {
pub fn deprecate(name: &str, reason: Option<&str>) {
fn finish(
(lints, mut deprecated_lints, renamed_lints): (Vec<Lint>, Vec<DeprecatedLint>, Vec<RenamedLint>),
name: &str,
@ -335,7 +335,7 @@ fn finish(
println!("note: you must run `cargo uitest` to update the test results");
}
let reason = reason.map_or(DEFAULT_DEPRECATION_REASON, String::as_str);
let reason = reason.unwrap_or(DEFAULT_DEPRECATION_REASON);
let name_lower = name.to_lowercase();
let name_upper = name.to_uppercase();