Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "codem8"
version = "0.7.3"
version = "0.7.4"
edition = "2021"
rust-version = "1.85"
license = "MIT"
Expand Down
74 changes: 37 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# CodeM8

CodeM8 is a Rust command-line application for deterministic source code reports.
It can detect duplicated line-based code blocks in a repository:
It can report functions whose cognitive or cyclomatic complexity exceeds
configurable limits:

```bash
codem8 --report-complexity
```

CodeM8 can also detect duplicated line-based code blocks in a repository:

```bash
codem8 --report-duplicate
Expand All @@ -12,19 +19,12 @@ trims source lines, ignores empty lines, hashes normalized lines with XXH3
128-bit, classifies syntax-only lines as block-only, groups repeated blocks, and
prints a stable plain-text report sorted by duplicate weight.

CodeM8 can also report functions whose cognitive or cyclomatic complexity
exceeds configurable limits:

```bash
codem8 --report-complexity
```

## Installation

Install `codem8` from the GitHub source with Cargo:

```bash
cargo install --git https://github.com/b4prog/CodeM8 codem8
cargo install --locked --git https://github.com/b4prog/CodeM8 codem8
```

Build from a local checkout with Cargo:
Expand All @@ -42,21 +42,21 @@ cargo install --locked --path .
Run from the local checkout without installing:

```bash
cargo run -- --report-duplicate
cargo run -- --report-complexity
```

## Usage

Analyze supported source files from the current directory:
Analyze function complexity for languages supported by `rust-code-analysis`:

```bash
codem8 --report-duplicate
codem8 --report-complexity
```

Analyze function complexity for languages supported by `rust-code-analysis`:
Analyze supported source files from the current directory for duplicate code:

```bash
codem8 --report-complexity
codem8 --report-duplicate
```

Restrict analysis to specific extensions:
Expand All @@ -78,20 +78,20 @@ Analyze files changed on the current local Git branch compared to the origin
base branch:

```bash
codem8 --report-duplicate -git-branch
codem8 --report-complexity -git-branch
```

The duplicate and complexity reports are mutually exclusive; run one report per
The complexity and duplicate reports are mutually exclusive; run one report per
command.

Reports exit with a non-zero status when they detect issues: duplicate blocks
for `--report-duplicate`, or functions above the configured limits for
`--report-complexity`.
Reports exit with a non-zero status when they detect issues: functions above the
configured limits for `--report-complexity`, or duplicate blocks for
`--report-duplicate`.

Include analyzed files, report metrics, and timing information:

```bash
codem8 --report-duplicate -verbose
codem8 --report-complexity -verbose
```

Set complexity thresholds:
Expand All @@ -100,6 +100,23 @@ Set complexity thresholds:
codem8 --report-complexity -max-cognitive-complexity=15 -max-cyclomatic-complexity=10
```

## Complexity Report

The complexity report uses `rust-code-analysis` and only applies to file
extensions supported by that crate. It reports `SpaceKind::Function` entries
whose cognitive complexity exceeds the configured cognitive limit or whose
cyclomatic complexity exceeds the configured cyclomatic limit.

The default maximum cognitive complexity is 15, and the default maximum
cyclomatic complexity is 10. Use `-max-cognitive-complexity=<value>` and
`-max-cyclomatic-complexity=<value>` to adjust them.

Use `-git-branch` to analyze complexity only in supported files changed on the
current local branch. The same origin branch resolution and `-files` exclusion
rules used by the duplicate report apply.

Use `-verbose` to list analyzed files and timing information.

## Duplicate Report

By default, CodeM8 analyzes all registered source file extensions. Recursive
Expand Down Expand Up @@ -133,23 +150,6 @@ occurrence count, and timings for discovery, file processing, and duplicate
detection. Character counts are used internally for scoring and sorting, but are
not printed.

## Complexity Report

The complexity report uses `rust-code-analysis` and only applies to file
extensions supported by that crate. It reports `SpaceKind::Function` entries
whose cognitive complexity exceeds the configured cognitive limit or whose
cyclomatic complexity exceeds the configured cyclomatic limit.

The default maximum cognitive complexity is 15, and the default maximum
cyclomatic complexity is 10. Use `-max-cognitive-complexity=<value>` and
`-max-cyclomatic-complexity=<value>` to adjust them.

Use `-git-branch` to analyze complexity only in supported files changed on the
current local branch. The same origin branch resolution and `-files` exclusion
rules used by the duplicate report apply.

Use `-verbose` to list analyzed files and timing information.

## Development

Run the full local verification set:
Expand Down
64 changes: 63 additions & 1 deletion src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ struct ClapCli {
verbose: u8,
#[arg(long = "codem8-git-branch", action = ArgAction::Count)]
git_branch: u8,
#[arg(long = "codem8-git-branch-strict", action = ArgAction::Count)]
git_branch_strict: u8,
#[arg(
long = "codem8-file-extension",
value_name = "extensions",
Expand Down Expand Up @@ -62,14 +64,17 @@ where
let report = selected_report(&parsed)?;
validate_repeated_options(&parsed)?;
let git_branch = parsed.git_branch != 0;
let files = selected_files(&parsed, git_branch)?;
let git_branch_strict = parsed.git_branch_strict != 0;
let files = selected_files(&parsed, git_branch || git_branch_strict)?;
validate_git_branch_modes(git_branch, git_branch_strict)?;
validate_complexity_limits(report, &parsed)?;
Ok(CliConfig {
report,
verbose: parsed.verbose != 0,
file_extensions: selected_file_extensions(&parsed),
files,
git_branch,
git_branch_strict,
max_cognitive_complexity: parsed
.max_cognitive_complexity
.unwrap_or(DEFAULT_MAX_COGNITIVE_COMPLEXITY),
Expand Down Expand Up @@ -109,6 +114,11 @@ fn validate_repeated_options(parsed: &ClapCli) -> Result<()> {
"git branch mode was provided more than once",
));
}
if parsed.git_branch_strict > 1 {
return Err(CodeM8Error::new(
"strict 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",
Expand All @@ -122,6 +132,15 @@ fn validate_repeated_options(parsed: &ClapCli) -> Result<()> {
Ok(())
}

fn validate_git_branch_modes(git_branch: bool, git_branch_strict: bool) -> Result<()> {
if git_branch && git_branch_strict {
return Err(CodeM8Error::new(
"git branch mode and strict git branch mode are mutually exclusive",
));
}
Ok(())
}

fn selected_files(parsed: &ClapCli, git_branch: bool) -> Result<Option<Vec<PathBuf>>> {
let files = parsed.files.first().cloned();
if git_branch && files.is_some() {
Expand Down Expand Up @@ -260,6 +279,8 @@ fn normalized_clap_arg(arg: String) -> Result<String> {
Ok("--codem8-verbose".to_owned())
} else if arg == "-git-branch" {
Ok("--codem8-git-branch".to_owned())
} else if arg == "-git-branch-strict" {
Ok("--codem8-git-branch-strict".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=") {
Expand Down Expand Up @@ -289,6 +310,7 @@ mod tests {
assert_eq!(config.file_extensions, supported_file_extensions());
assert_eq!(config.files, None);
assert!(!config.git_branch);
assert!(!config.git_branch_strict);
assert_eq!(
config.max_cognitive_complexity,
DEFAULT_MAX_COGNITIVE_COMPLEXITY
Expand Down Expand Up @@ -336,6 +358,16 @@ mod tests {
fn parses_git_branch_duplicate_report_config() {
let config = parse_args(["--report-duplicate", "-git-branch"]).expect("config parses");
assert!(config.git_branch);
assert!(!config.git_branch_strict);
assert_eq!(config.files, None);
}

#[test]
fn parses_strict_git_branch_duplicate_report_config() {
let config =
parse_args(["--report-duplicate", "-git-branch-strict"]).expect("config parses");
assert!(!config.git_branch);
assert!(config.git_branch_strict);
assert_eq!(config.files, None);
}

Expand Down Expand Up @@ -387,6 +419,7 @@ mod tests {
"--file-extension=js",
"--files=src/a.ts",
"--git-branch",
"--git-branch-strict",
"--max-cognitive-complexity=20",
"--max-cyclomatic-complexity=12",
] {
Expand Down Expand Up @@ -461,6 +494,26 @@ mod tests {
.contains("git branch mode was provided more than once"));
}

#[test]
fn rejects_repeated_strict_git_branch_arguments() {
let error = parse_args([
"--report-duplicate",
"-git-branch-strict",
"-git-branch-strict",
])
.expect_err("repeated strict git branch mode fails");
assert!(error
.to_string()
.contains("strict git branch mode was provided more than once"));
}

#[test]
fn rejects_git_branch_with_strict_git_branch() {
let error = parse_args(["--report-duplicate", "-git-branch", "-git-branch-strict"])
.expect_err("exclusive git branch modes fail");
assert!(error.to_string().contains("mutually exclusive"));
}

#[test]
fn rejects_git_branch_with_explicit_files() {
let error = parse_args(["--report-duplicate", "-git-branch", "-files=a.ts"])
Expand All @@ -470,6 +523,15 @@ mod tests {
.contains("git branch mode cannot be combined with explicit files"));
}

#[test]
fn rejects_strict_git_branch_with_explicit_files() {
let error = parse_args(["--report-duplicate", "-git-branch-strict", "-files=a.ts"])
.expect_err("exclusive strict file modes fail");
assert!(error
.to_string()
.contains("git branch mode cannot be combined with explicit files"));
}

#[test]
fn parses_explicit_file_list() {
let files = parse_file_list("src/a.ts, ./src/b.ts").expect("files parse");
Expand Down
Loading
Loading