Line data Source code
1 : use clap::{crate_version, ArgAction, Parser};
2 : use std::{
3 : ffi::{OsStr, OsString},
4 : fmt::Debug,
5 : };
6 :
7 : #[derive(Debug, Parser)]
8 : #[clap(bin_name = "cargo", display_name = "cargo")]
9 : struct Opts {
10 : #[clap(subcommand)]
11 : subcmd: CargoSubcommand,
12 : }
13 :
14 : #[derive(Debug, Parser)]
15 : enum CargoSubcommand {
16 : Dylint(Dylint),
17 : }
18 :
19 : #[allow(clippy::struct_excessive_bools)]
20 : #[derive(Debug, Parser)]
21 : #[clap(
22 : version = crate_version!(),
23 : args_conflicts_with_subcommands = true,
24 : after_help = r#"ENVIRONMENT VARIABLES:
25 :
26 : DYLINT_DRIVER_PATH (default: $HOME/.dylint_drivers) is the directory where Dylint stores rustc
27 : drivers.
28 :
29 : DYLINT_LIBRARY_PATH (default: none) is a colon-separated list of directories where Dylint searches
30 : for libraries.
31 :
32 : DYLINT_RUSTFLAGS (default: none) is a space-separated list of flags that Dylint passes to `rustc`
33 : when checking the packages in the workspace.
34 :
35 : METADATA EXAMPLE:
36 :
37 : [workspace.metadata.dylint]
38 : libraries = [
39 : { git = "https://github.com/trailofbits/dylint", pattern = "examples/*/*" },
40 : { path = "libs/*" },
41 : ]
42 : "#,
43 : )]
44 : // smoelius: Please keep the last four fields `args`, `operation`, `lib_sel`, and `output`, in that
45 : // order. Please keep all other fields sorted.
46 : struct Dylint {
47 : #[clap(long, help = "Automatically apply lint suggestions")]
48 0 : fix: bool,
49 :
50 : #[clap(long, help = "Continue if `cargo check` fails")]
51 0 : keep_going: bool,
52 :
53 : #[clap(long, help = "Do not check other packages within the workspace")]
54 0 : no_deps: bool,
55 :
56 : #[clap(
57 : action = ArgAction::Append,
58 : number_of_values = 1,
59 : short,
60 : long = "package",
61 : value_name = "SPEC",
62 : help = "Package to check"
63 : )]
64 2 : packages: Vec<String>,
65 :
66 : #[clap(long, help = "Check all packages in the workspace")]
67 0 : workspace: bool,
68 :
69 : #[clap(last = true, help = "Arguments for `cargo check`")]
70 1 : args: Vec<String>,
71 :
72 : #[clap(subcommand)]
73 : operation: Option<Operation>,
74 :
75 : #[clap(flatten)]
76 : lib_sel: LibrarySelection,
77 :
78 : #[clap(flatten)]
79 : output: OutputOptions,
80 : }
81 :
82 : #[derive(Debug, Parser)]
83 : enum Operation {
84 : #[clap(
85 : about = "List libraries or lints",
86 : long_about = "If no libraries are named, list the name, toolchain, and location of all \
87 : discovered libraries.
88 :
89 : If at least one library is named, list the name, level, and description of all lints in all named \
90 : libraries.
91 :
92 : Combine with `--all` to list all lints in all discovered libraries."
93 : )]
94 : List {
95 : #[clap(flatten)]
96 : lib_sel: LibrarySelection,
97 : },
98 :
99 : #[clap(
100 : about = "Create a new library package",
101 : long_about = "Create a new library package at <PATH>"
102 : )]
103 : New {
104 : #[clap(long, help = "Put the package in its own workspace")]
105 0 : isolate: bool,
106 :
107 : #[clap(help = "Path to library package")]
108 0 : path: String,
109 : },
110 :
111 : #[clap(
112 : about = "Upgrade library package",
113 : long_about = "Upgrade the library package at <PATH> to the latest version of \
114 : `clippy_utils`"
115 : )]
116 : Upgrade {
117 : #[clap(long, hide = true)]
118 0 : allow_downgrade: bool,
119 :
120 : #[clap(
121 : long,
122 : value_name = "VERSION",
123 : help = "Upgrade to the version of `clippy_utils` with tag `rust-<VERSION>`"
124 : )]
125 : rust_version: Option<String>,
126 :
127 : #[clap(help = "Path to library package")]
128 0 : path: String,
129 : },
130 : }
131 :
132 : #[derive(Debug, Parser)]
133 : #[cfg_attr(feature = "__clap_headings", clap(next_help_heading = Some("Library Selection")))]
134 : struct LibrarySelection {
135 : #[clap(long, help = "Load all discovered libraries")]
136 0 : all: bool,
137 :
138 : #[clap(
139 : long,
140 : requires("git"),
141 : help = "Branch to use when downloading library packages"
142 : )]
143 : branch: Option<String>,
144 :
145 : #[clap(
146 : long,
147 : value_name = "URL",
148 : conflicts_with("paths"),
149 : help = "Git url containing library packages"
150 : )]
151 : git: Option<String>,
152 :
153 : #[clap(
154 : action = ArgAction::Append,
155 : number_of_values = 1,
156 : long = "lib-path",
157 : value_name = "PATH",
158 : help = "Library path to load lints from"
159 : )]
160 0 : lib_paths: Vec<String>,
161 :
162 : #[clap(
163 : action = ArgAction::Append,
164 : number_of_values = 1,
165 : long = "lib",
166 : value_name = "NAME",
167 : help = "Library name to load lints from. A file with a name of the form \"DLL_PREFIX \
168 : <NAME> '@' TOOLCHAIN DLL_SUFFIX\" is searched for in the directories listed in \
169 : DYLINT_LIBRARY_PATH, and in the `target/release` directories produced by building the \
170 : current workspace's metadata entries (see example below)."
171 : )]
172 9 : libs: Vec<String>,
173 :
174 : #[clap(
175 : long,
176 : value_name = "PATH",
177 : help = "Path to Cargo.toml. Note: if the manifest uses metadata, then `--manifest-path \
178 : <PATH>` must appear before `--`, not after."
179 : )]
180 : manifest_path: Option<String>,
181 :
182 : #[clap(long, help = "Do not build metadata entries")]
183 0 : no_build: bool,
184 :
185 : #[clap(long, help = "Ignore metadata entirely")]
186 0 : no_metadata: bool,
187 :
188 : #[clap(
189 : action = ArgAction::Append,
190 : number_of_values = 1,
191 : long = "path",
192 : value_name = "PATH",
193 : conflicts_with("git"),
194 : help = "Path containing library packages"
195 : )]
196 3 : paths: Vec<String>,
197 :
198 : #[clap(
199 : long,
200 : help = "Subdirectories of the `--git` or `--path` argument containing library packages"
201 : )]
202 : pattern: Option<String>,
203 :
204 : #[clap(
205 : long,
206 : requires("git"),
207 : help = "Specific commit to use when downloading library packages"
208 : )]
209 : rev: Option<String>,
210 :
211 : #[clap(
212 : long,
213 : requires("git"),
214 : help = "Tag to use when downloading library packages"
215 : )]
216 : tag: Option<String>,
217 : }
218 :
219 : #[derive(Debug, Parser)]
220 : #[cfg_attr(feature = "__clap_headings", clap(next_help_heading = Some("Output Options")))]
221 : struct OutputOptions {
222 : #[clap(long, value_name = "PATH", help = "Path to pipe stderr to")]
223 : pipe_stderr: Option<String>,
224 :
225 : #[clap(long, value_name = "PATH", help = "Path to pipe stdout to")]
226 : pipe_stdout: Option<String>,
227 :
228 : #[clap(
229 : global = true,
230 : short,
231 : long,
232 : help = "Do not show warnings or progress running commands besides `cargo check` and \
233 : `cargo fix`"
234 : )]
235 0 : quiet: bool,
236 : }
237 :
238 : impl From<Dylint> for dylint::opts::Dylint {
239 41 : fn from(opts: Dylint) -> Self {
240 41 : let Dylint {
241 41 : fix,
242 41 : keep_going,
243 41 : no_deps,
244 41 : packages,
245 41 : workspace,
246 41 : args,
247 41 : operation,
248 41 : mut lib_sel,
249 41 : output:
250 41 : OutputOptions {
251 41 : pipe_stderr,
252 41 : pipe_stdout,
253 41 : quiet,
254 41 : },
255 41 : } = opts;
256 41 : let operation = match operation {
257 27 : None => dylint::opts::Operation::Check({
258 27 : dylint::opts::Check {
259 27 : lib_sel: lib_sel.into(),
260 27 : fix,
261 27 : keep_going,
262 27 : no_deps,
263 27 : packages,
264 27 : workspace,
265 27 : args,
266 27 : }
267 27 : }),
268 10 : Some(Operation::List { lib_sel: other }) => {
269 10 : lib_sel.absorb(other);
270 10 : dylint::opts::Operation::List(dylint::opts::List {
271 10 : lib_sel: lib_sel.into(),
272 10 : })
273 : }
274 1 : Some(Operation::New { isolate, path }) => {
275 1 : dylint::opts::Operation::New(dylint::opts::New { isolate, path })
276 : }
277 : Some(Operation::Upgrade {
278 3 : allow_downgrade,
279 3 : rust_version,
280 3 : path,
281 3 : }) => dylint::opts::Operation::Upgrade(dylint::opts::Upgrade {
282 3 : allow_downgrade,
283 3 : rust_version,
284 3 : path,
285 3 : }),
286 : };
287 41 : Self {
288 41 : pipe_stderr,
289 41 : pipe_stdout,
290 41 : quiet,
291 41 : operation,
292 41 : }
293 41 : }
294 : }
295 :
296 : macro_rules! option_absorb {
297 : ($this:expr, $other:expr) => {
298 : if $other.is_some() {
299 : assert!(
300 : $this.is_none(),
301 : "`--{}` used multiple times",
302 : stringify!($other).replace("_", "-")
303 : );
304 : *$this = $other;
305 : }
306 : };
307 : }
308 :
309 : impl LibrarySelection {
310 10 : pub fn absorb(&mut self, other: Self) {
311 10 : let Self {
312 10 : all,
313 10 : branch,
314 10 : git,
315 10 : lib_paths,
316 10 : libs,
317 10 : manifest_path,
318 10 : no_build,
319 10 : no_metadata,
320 10 : paths,
321 10 : pattern,
322 10 : rev,
323 10 : tag,
324 10 : } = other;
325 10 : self.all |= all;
326 10 : option_absorb!(&mut self.branch, branch);
327 10 : option_absorb!(&mut self.git, git);
328 10 : self.lib_paths.extend(lib_paths);
329 10 : self.libs.extend(libs);
330 10 : option_absorb!(&mut self.manifest_path, manifest_path);
331 10 : self.no_build |= no_build;
332 10 : self.no_metadata |= no_metadata;
333 10 : self.paths.extend(paths);
334 10 : option_absorb!(&mut self.pattern, pattern);
335 10 : option_absorb!(&mut self.rev, rev);
336 10 : option_absorb!(&mut self.tag, tag);
337 10 : }
338 : }
339 :
340 : impl From<LibrarySelection> for dylint::opts::LibrarySelection {
341 37 : fn from(lib_sel: LibrarySelection) -> Self {
342 37 : let LibrarySelection {
343 37 : all,
344 37 : branch,
345 37 : git,
346 37 : lib_paths,
347 37 : libs,
348 37 : manifest_path,
349 37 : no_build,
350 37 : no_metadata,
351 37 : paths,
352 37 : pattern,
353 37 : rev,
354 37 : tag,
355 37 : } = lib_sel;
356 37 : Self {
357 37 : all,
358 37 : branch,
359 37 : git,
360 37 : lib_paths,
361 37 : libs,
362 37 : manifest_path,
363 37 : no_build,
364 37 : no_metadata,
365 37 : paths,
366 37 : pattern,
367 37 : rev,
368 37 : tag,
369 37 : }
370 37 : }
371 : }
372 :
373 43 : fn main() -> dylint::ColorizedResult<()> {
374 43 : env_logger::try_init().unwrap_or_else(|error| {
375 43 : dylint::__warn(
376 43 : &dylint::opts::Dylint::default(),
377 43 : &format!("`env_logger` already initialized: {error}"),
378 43 : );
379 43 : });
380 43 :
381 43 : let args: Vec<_> = std::env::args().map(OsString::from).collect();
382 43 :
383 43 : cargo_dylint(&args)
384 43 : }
385 :
386 43 : fn cargo_dylint<T: AsRef<OsStr>>(args: &[T]) -> dylint::ColorizedResult<()> {
387 43 : match Opts::parse_from(args).subcmd {
388 43 : CargoSubcommand::Dylint(opts) => dylint::run(&dylint::opts::Dylint::from(opts)),
389 43 : }
390 43 : .map_err(dylint::ColorizedError::new)
391 43 : }
392 :
393 : #[cfg(test)]
394 : mod tests {
395 : use super::*;
396 : use assert_cmd::prelude::*;
397 : use clap::CommandFactory;
398 :
399 : #[test]
400 1 : fn verify_cli() {
401 1 : Opts::command().debug_assert();
402 1 : }
403 :
404 : #[test]
405 1 : fn usage() {
406 1 : std::process::Command::cargo_bin("cargo-dylint")
407 1 : .unwrap()
408 1 : .args(["dylint", "--help"])
409 1 : .assert()
410 1 : .success()
411 1 : .stdout(predicates::str::contains("Usage: cargo dylint"));
412 1 : }
413 :
414 : #[test]
415 1 : fn version() {
416 1 : std::process::Command::cargo_bin("cargo-dylint")
417 1 : .unwrap()
418 1 : .args(["dylint", "--version"])
419 1 : .assert()
420 1 : .success()
421 1 : .stdout(format!("cargo-dylint {}\n", env!("CARGO_PKG_VERSION")));
422 1 : }
423 :
424 : // `no_env_logger_warning` fails if [`std::process::Command::new`] is replaced with
425 : // [`assert_cmd::cargo::CommandCargoExt::cargo_bin`]. I don't understand why.
426 : //
427 : // [`assert_cmd::cargo::CommandCargoExt::cargo_bin`]: https://docs.rs/assert_cmd/latest/assert_cmd/cargo/trait.CommandCargoExt.html#tymethod.cargo_bin
428 : // [`std::process::Command::new`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.new
429 : //
430 : // smoelius: I am switching to `assert_cmd::cargo::CommandCargoExt::cargo_bin` and disabling
431 : // this test. `cargo run` without a `--features=...` argument can cause `cargo-dylint` to be
432 : // rebuilt with the wrong features.
433 : #[cfg(any())]
434 : #[test]
435 : fn no_env_logger_warning() {
436 : std::process::Command::cargo_bin("cargo-dylint")
437 : .unwrap()
438 : .arg("dylint")
439 : .assert()
440 : .failure()
441 : .stderr(predicates::str::contains("`env_logger` already initialized").not());
442 : }
443 : }
|