Skip to content

fix(dupes): honor scalar config fields when CLI flags are omitted#290

Merged
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ryota-murakami:fix/dupes-config-scalars-not-read
May 6, 2026
Merged

fix(dupes): honor scalar config fields when CLI flags are omitted#290
BartWaardenburg merged 1 commit intofallow-rs:mainfrom
ryota-murakami:fix/dupes-config-scalars-not-read

Conversation

@ryota-murakami
Copy link
Copy Markdown
Contributor

Summary

Standalone fallow dupes ignores duplicates.minLines, duplicates.minTokens, duplicates.threshold, duplicates.mode, and duplicates.skipLocal declared in .fallowrc.jsonc whenever the matching CLI flag is omitted. The Command::Dupes clap definitions use default_value = "5" (etc.), so min_lines: usize is always populated with the clap default — build_dupes_config then overwrites the toml value unconditionally.

Reproduction

mkdir /tmp/repro && cd /tmp/repro
cat > a.ts << 'TS'
function foo() {
  const a = 1
  const b = 2
  const c = 3
  return a + b + c
}
TS
cp a.ts b.ts && sed -i '' 's/foo/bar/' b.ts
cat > .fallowrc.jsonc << 'JSON'
{ "duplicates": { "minLines": 100, "minTokens": 1000 } }
JSON

# Before this PR: still reports the 4-line clone, ignoring config
fallow dupes

The same problem affects mode, threshold, and skipLocal. audit/combined/programmatic paths are not affected because they merge config into their own structs before constructing DupesOptions.

Root cause

crates/cli/src/dupes.rs::build_dupes_config overwrote each toml scalar with the corresponding opts.* value:

min_tokens: opts.min_tokens,           // clap default of 50 wins, always
min_lines: opts.min_lines,             // clap default of 5 wins, always
threshold: opts.threshold,             // clap default of 0.0 wins, always
skip_local: opts.skip_local,           // CLI false stomps config true

Option<T> with default_value is the only way to distinguish user-passed from clap-supplied default with non-Optional types — so we can't fix this without touching the CLI signature.

Fix

  • Switch mode, min_tokens, min_lines, threshold on DupesOptions and the Command::Dupes clap args to Option<T> and drop their default_values.
  • build_dupes_config now uses opts.x.unwrap_or(toml_dupes.x) for those four fields.
  • skip_local adopts the OR-merge already used for cross_language/ignore_imports, so config-driven skipLocal: true is honored.
  • DupesResult.threshold is now sourced from the merged dupes_config.threshold (was opts.threshold), so the failure gate honors .fallowrc.jsonc too.
  • audit.rs, combined.rs, and programmatic.rs already have explicit values from their own merges; they forward each as Some(...) (no behavior change at those entry points).

Help text now reads:

--min-lines <MIN_LINES>
    Minimum line count for a clone
    (defaults to the value in `.fallowrc.jsonc`, or `5` if unset).

Test plan

  • Updated existing build_config_uses_cli_* tests to wrap CLI values in Some(...)
  • Added regression tests for each fallback path:
    • falls_back_to_toml_min_lines_when_cli_unset
    • falls_back_to_toml_min_tokens_when_cli_unset
    • falls_back_to_toml_threshold_when_cli_unset
    • falls_back_to_toml_mode_when_cli_unset
    • cli_min_lines_overrides_toml
    • skip_local_or_merges_with_toml
  • cargo fmt --all
  • cargo clippy --workspace --all-targets -- -D warnings (clean)
  • cargo test --workspace (all suites green)
  • End-to-end repro project verified — config minLines: 100 now suppresses the toy clone, and --min-lines 100 still overrides config

Compatibility

  • CLI surface: --mode, --min-tokens, --min-lines, --threshold still accept the same values; the only observable difference is "flag omitted" no longer wins over config. Users who relied on the old behavior can migrate by either setting duplicates.* = <prior_default> in their config or by passing the flag explicitly.
  • Public Rust API (DuplicationOptions): unchanged. Callers still pass concrete values; we forward each as Some(...) internally.

`fallow dupes` ignored `duplicates.minLines`, `duplicates.minTokens`,
`duplicates.threshold`, `duplicates.mode`, and `duplicates.skipLocal`
in `.fallowrc.jsonc` when the matching CLI flag was not passed. The
`Command::Dupes` clap definitions used `default_value = "5"` (etc.),
so `min_lines: usize` was always populated with the clap default and
`build_dupes_config` overwrote the toml value unconditionally.

Switch the four scalar fields (`mode`, `min_tokens`, `min_lines`,
`threshold`) on `DupesOptions` and the `Command::Dupes` clap args to
`Option<T>` and drop their `default_value`s. `build_dupes_config` now
falls back to `toml_dupes.x` when the CLI value is `None`. The
`skip_local` boolean adopts the same OR-merge already used for
`cross_language` / `ignore_imports` so config-driven `skipLocal: true`
is honored too.

Audit, combined, and programmatic call sites already merge config
into their own structs before constructing `DupesOptions`, so they
forward each value as an explicit `Some(...)` (no behavior change at
those entry points).

The standalone `DupesResult.threshold` is now sourced from the merged
`dupes_config.threshold` rather than the unmerged `opts.threshold`,
so `.fallowrc.jsonc`'s `threshold` actually drives the failure gate.

Tests:
- existing `build_config_uses_cli_*` tests updated to wrap CLI values
  in `Some(...)`
- new regression tests for each fallback path:
  `falls_back_to_toml_min_lines_when_cli_unset`,
  `falls_back_to_toml_min_tokens_when_cli_unset`,
  `falls_back_to_toml_threshold_when_cli_unset`,
  `falls_back_to_toml_mode_when_cli_unset`,
  `cli_min_lines_overrides_toml`,
  `skip_local_or_merges_with_toml`
@BartWaardenburg BartWaardenburg merged commit 4798ec5 into fallow-rs:main May 6, 2026
1 check passed
@BartWaardenburg
Copy link
Copy Markdown
Collaborator

Released in v2.66.0. Thanks @ryota-murakami.

@ryota-murakami ryota-murakami deleted the fix/dupes-config-scalars-not-read branch May 6, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants