1use std::process::Command;
2
3use anyhow::{bail, Context, Result};
4use clap::{Parser, Subcommand};
5
6#[derive(Parser)]
7#[command(name = "cargo xtask")]
8struct Args {
9 #[command(subcommand)]
10 command: CliCommand,
11}
12
13#[derive(Subcommand)]
14enum CliCommand {
15 /// Runs `cargo clippy`.
16 Clippy(ClippyArgs),
17}
18
19fn main() -> Result<()> {
20 let args = Args::parse();
21
22 match args.command {
23 CliCommand::Clippy(args) => run_clippy(args),
24 }
25}
26
27#[derive(Parser)]
28struct ClippyArgs {
29 /// Automatically apply lint suggestions (`clippy --fix`).
30 #[arg(long)]
31 fix: bool,
32
33 /// The package to run Clippy against (`cargo -p <PACKAGE> clippy`).
34 #[arg(long, short)]
35 package: Option<String>,
36}
37
38fn run_clippy(args: ClippyArgs) -> Result<()> {
39 let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
40
41 let mut clippy_command = Command::new(&cargo);
42 clippy_command.arg("clippy");
43
44 if let Some(package) = args.package.as_ref() {
45 clippy_command.args(["--package", package]);
46 } else {
47 clippy_command.arg("--workspace");
48 }
49
50 clippy_command
51 .arg("--release")
52 .arg("--all-targets")
53 .arg("--all-features");
54
55 if args.fix {
56 clippy_command.arg("--fix");
57 }
58
59 clippy_command.arg("--");
60
61 // Deny all warnings.
62 // We don't do this yet on Windows, as it still has some warnings present.
63 #[cfg(not(target_os = "windows"))]
64 clippy_command.args(["--deny", "warnings"]);
65
66 /// These are all of the rules that currently have violations in the Zed
67 /// codebase.
68 ///
69 /// We'll want to drive this list down by either:
70 /// 1. fixing violations of the rule and begin enforcing it
71 /// 2. deciding we want to allow the rule permanently, at which point
72 /// we should codify that separately in this task.
73 ///
74 /// This list shouldn't be added to; it should only get shorter.
75 const MIGRATORY_RULES_TO_ALLOW: &[&str] = &[
76 // There are a bunch of rules currently failing in the `style` group, so
77 // allow all of those, for now.
78 "clippy::style",
79 // Individual rules that have violations in the codebase:
80 "clippy::almost_complete_range",
81 "clippy::arc_with_non_send_sync",
82 "clippy::await_holding_lock",
83 "clippy::borrow_deref_ref",
84 "clippy::borrowed_box",
85 "clippy::cast_abs_to_unsigned",
86 "clippy::cmp_owned",
87 "clippy::crate_in_macro_def",
88 "clippy::derivable_impls",
89 "clippy::derive_ord_xor_partial_ord",
90 "clippy::eq_op",
91 "clippy::implied_bounds_in_impls",
92 "clippy::iter_kv_map",
93 "clippy::iter_overeager_cloned",
94 "clippy::let_underscore_future",
95 "clippy::map_entry",
96 "clippy::needless_update",
97 "clippy::never_loop",
98 "clippy::non_canonical_clone_impl",
99 "clippy::non_canonical_partial_ord_impl",
100 "clippy::redundant_closure_call",
101 "clippy::reversed_empty_ranges",
102 "clippy::single_range_in_vec_init",
103 "clippy::suspicious_to_owned",
104 "clippy::type_complexity",
105 "clippy::unnecessary_to_owned",
106 ];
107
108 // When fixing violations automatically for a single package we don't care
109 // about the rules we're already violating, since it may be possible to
110 // have them fixed automatically.
111 let ignore_suppressed_rules = args.fix && args.package.is_some();
112 if !ignore_suppressed_rules {
113 for rule in MIGRATORY_RULES_TO_ALLOW {
114 clippy_command.args(["--allow", rule]);
115 }
116 }
117
118 // Deny `dbg!` and `todo!`s.
119 clippy_command
120 .args(["--deny", "clippy::dbg_macro"])
121 .args(["--deny", "clippy::todo"]);
122
123 eprintln!(
124 "running: {cargo} {}",
125 clippy_command
126 .get_args()
127 .map(|arg| arg.to_str().unwrap())
128 .collect::<Vec<_>>()
129 .join(" ")
130 );
131
132 let exit_status = clippy_command
133 .spawn()
134 .context("failed to spawn child process")?
135 .wait()
136 .context("failed to wait for child process")?;
137
138 if !exit_status.success() {
139 bail!("clippy failed: {}", exit_status);
140 }
141
142 Ok(())
143}