LCOV - code coverage report
Current view: top level - cargo-dylint/src - main.rs (source / functions) Hit Total Coverage
Test: unnamed Lines: 150 163 92.0 %
Date: 2024-10-23 19:20:50 Functions: 13 44 29.5 %

          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             : }

Generated by: LCOV version 1.14