diff --git a/Cargo.lock b/Cargo.lock index 449f13b..e20583f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "bstr" version = "1.12.3" @@ -21,16 +71,63 @@ dependencies = [ "serde_core", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "codem8" -version = "0.3.0" +version = "0.4.0" dependencies = [ + "clap", "ignore", "rayon", "regex", "xxhash-rust", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -75,6 +172,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "ignore" version = "0.4.26" @@ -91,6 +194,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "log" version = "0.4.33" @@ -103,6 +212,12 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -199,6 +314,12 @@ dependencies = [ "syn", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.118" @@ -216,6 +337,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index f356e2f..f45a274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "codem8" -version = "0.3.0" +version = "0.4.0" edition = "2021" +rust-version = "1.85" license = "MIT" description = "A deterministic source code analysis CLI for duplicate code reports." repository = "https://github.com/b4prog/CodeM8" @@ -9,6 +10,7 @@ keywords = ["cli", "duplicate-detection", "source-code", "analysis"] categories = ["command-line-utilities", "development-tools"] [dependencies] +clap = { version = "4.6.1", features = ["derive"] } ignore = "0.4" rayon = "1" regex = "1" diff --git a/src/cli.rs b/src/cli.rs index 9b3dc17..1c71e1b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,8 @@ use std::fmt::Write as _; use std::path::PathBuf; +use clap::{ArgAction, Parser}; + use crate::error::{CodeM8Error, Result}; use crate::language::supported_file_extensions; @@ -71,6 +73,31 @@ pub struct CliConfig { pub git_branch: bool, } +#[derive(Debug, Parser)] +#[command(name = "codem8", disable_help_flag = true, disable_version_flag = true)] +struct ClapCli { + #[arg(long = "report-duplicate", action = ArgAction::Count)] + report_duplicate: u8, + #[arg(long = "codem8-verbose", action = ArgAction::Count)] + verbose: u8, + #[arg(long = "codem8-git-branch", action = ArgAction::Count)] + git_branch: u8, + #[arg( + long = "codem8-file-extension", + value_name = "extensions", + value_parser = parse_file_extensions, + action = ArgAction::Append + )] + file_extensions: Vec>, + #[arg( + long = "codem8-files", + value_name = "paths", + value_parser = parse_file_list, + action = ArgAction::Append + )] + files: Vec>, +} + #[must_use] pub fn help_text() -> String { let version = codem8_version_from_cargo_lock().unwrap_or("unknown"); @@ -113,56 +140,48 @@ where I: IntoIterator, S: Into, { - let mut report_duplicate = false; - let mut verbose = false; - let mut file_extensions = None; - let mut files = None; - let mut git_branch = false; - for arg in args { - let arg = arg.into(); - if arg == "--report-duplicate" { - report_duplicate = true; - } else if arg == "-verbose" { - verbose = true; - } else if arg == "-git-branch" { - if git_branch { - return Err(CodeM8Error::new( - "git branch mode was provided more than once", - )); - } - git_branch = true; - } else if let Some(value) = arg.strip_prefix("-file-extension=") { - if file_extensions.is_some() { - return Err(CodeM8Error::new( - "file extensions were provided more than once", - )); - } - file_extensions = Some(parse_file_extensions(value)?); - } else if let Some(value) = arg.strip_prefix("-files=") { - if files.is_some() { - return Err(CodeM8Error::new( - "explicit files were provided more than once", - )); - } - files = Some(parse_file_list(value)?); - } else { - return Err(CodeM8Error::new(format!("unknown argument: {arg}"))); - } - } - if !report_duplicate { + let parsed = ClapCli::try_parse_from(normalized_clap_args(args)?) + .map_err(|error| CodeM8Error::new(error.to_string().trim().to_owned()))?; + if parsed.report_duplicate == 0 { return Err(CodeM8Error::with_help( "no report switch provided; pass --report-duplicate", )); } + if parsed.report_duplicate > 1 { + return Err(CodeM8Error::new( + "report switch was provided more than once", + )); + } + if parsed.git_branch > 1 { + return Err(CodeM8Error::new( + "git branch mode was provided more than once", + )); + } + if parsed.file_extensions.len() > 1 { + return Err(CodeM8Error::new( + "file extensions were provided more than once", + )); + } + if parsed.files.len() > 1 { + return Err(CodeM8Error::new( + "explicit files were provided more than once", + )); + } + let git_branch = parsed.git_branch != 0; + let files = parsed.files.into_iter().next(); if git_branch && files.is_some() { return Err(CodeM8Error::new( "git branch mode cannot be combined with explicit files", )); } Ok(CliConfig { - report_duplicate, - verbose, - file_extensions: file_extensions.unwrap_or_else(supported_file_extensions), + report_duplicate: parsed.report_duplicate != 0, + verbose: parsed.verbose != 0, + file_extensions: parsed + .file_extensions + .into_iter() + .next() + .unwrap_or_else(supported_file_extensions), files, git_branch, }) @@ -226,6 +245,34 @@ fn is_help_argument(arg: &str) -> bool { matches!(arg, "help" | "-h") } +fn normalized_clap_args(args: I) -> Result> +where + I: IntoIterator, + S: Into, +{ + let mut normalized = vec!["codem8".to_owned()]; + for arg in args { + normalized.push(normalized_clap_arg(arg.into())?); + } + Ok(normalized) +} + +fn normalized_clap_arg(arg: String) -> Result { + if arg == "-verbose" { + Ok("--codem8-verbose".to_owned()) + } else if arg == "-git-branch" { + Ok("--codem8-git-branch".to_owned()) + } else if let Some(value) = arg.strip_prefix("-file-extension=") { + Ok(format!("--codem8-file-extension={value}")) + } else if let Some(value) = arg.strip_prefix("-files=") { + Ok(format!("--codem8-files={value}")) + } else if arg.starts_with("--") && arg != "--report-duplicate" { + Err(CodeM8Error::new(format!("unknown argument: {arg}"))) + } else { + Ok(arg) + } +} + fn codem8_version_from_cargo_lock() -> Option<&'static str> { cargo_lock_packages(CARGO_LOCK) .find(|package| package.name == "codem8") @@ -400,6 +447,15 @@ version = "0.4.2" .contains("file extensions were provided more than once")); } + #[test] + fn rejects_repeated_report_switches() { + let error = parse_args(["--report-duplicate", "--report-duplicate"]) + .expect_err("repeated report switch fails"); + assert!(error + .to_string() + .contains("report switch was provided more than once")); + } + #[test] fn rejects_repeated_explicit_file_arguments() { let error = parse_args(["--report-duplicate", "-files=a.ts", "-files=b.ts"]) diff --git a/src/model.rs b/src/model.rs index 2a1b195..9cc99e7 100644 --- a/src/model.rs +++ b/src/model.rs @@ -44,7 +44,7 @@ pub struct DuplicateBlock { impl DuplicateBlock { #[must_use] - pub const fn line_count(&self) -> usize { + pub fn line_count(&self) -> usize { self.normalized_lines.len() }