Skip to content

fix(cli/check): [check.exclude] re-anchoring re-reads --paths-from, breaking stdin lists #1306

Closed

Description

@dekobon

Surfaced during the #1055 review (branch fix/1055-check-gate-visibility, PR #1305); pre-existing #497 plumbing, not introduced there.

apply_check_exclude re-reads --paths-from to re-anchor [check.exclude] globs against the seed list. read_paths_from treats - as stdin, and by that point the walk has already consumed it, so the second read returns an empty list.

Reproduction shape

git diff --name-only | bca check --paths-from - --check-exclude 'tests/**'

The walk reads the file list from stdin and analyzes it. apply_check_exclude then reads an already-EOF stdin, gets zero seeds, and anchors every violation against the (empty) --paths set — so the tests/** exemption silently fails to match and the gate fails on files the project exempted.

Suggested direction

run_check_walk used to materialize --paths-from into globals.paths up front (removed when the walk went back to a single resolve); re-introducing that materialization and threading the post-materialization globals to apply_check_exclude retires the second read entirely. The remediation footer should keep showing the user's original --paths-from spelling (it renders from a pre-materialization clone today).

The stdin integration test added for #1055 (paths_from_stdin_is_read_once) pins the walk half only — it configures no check-exclude, so this branch stays uncovered until fixed.


Resolution Plan

Confirmed on main (d65b3049), and the code carries the design
decision that causes it in a comment. apply_check_exclude
(big-code-analysis-cli/src/commands/check.rs:474) re-reads the seed
list at :502:

Re-reading --paths-from here (rather than plumbing the resolved list
back from the walk) keeps the hot no-exclude path above
allocation-free; this branch is the rare configured case.

read_paths_from (big-code-analysis-cli/src/path_io.rs:253) resolves
- to stdin via read_paths_from_bytes, and expand_seed_paths
(walk.rs:399) has already drained it. The second read returns an empty
list, so every violation anchors against the --paths set alone and a
[check.exclude] glob silently fails to match.

  1. Materialize once and thread the result, as the issue directs.
    Resolve --paths-from into the effective seed list in run_check_walk
    before the walk, and pass that list to apply_check_exclude in
    place of the (paths, paths_from) pair. The second read then has
    nothing to re-read and the stdin case disappears by construction
    rather than by a special case.
  2. Preserve the fast path the comment defends. CheckExcludes::resolve
    returning None is still the first thing apply_check_exclude does,
    so the no-exclude run does no extra work; what changes is that the
    materialized list is produced by the walk, which was going to produce
    it anyway. Say that in the replacement comment — deleting the old one
    without answering it invites the re-read back.
  3. Keep the remediation footer showing the user's original
    --paths-from spelling.
    It renders from a pre-materialization clone
    today; keep that clone explicitly rather than relying on the
    materialization happening later than the render. A test asserting the
    footer still prints --paths-from - after the change is what pins it.
  4. Do not fix this by rejecting - in the second read. Erroring, or
    warning, on a consumed stdin turns a silent wrong answer into a loud
    wrong answer; the seed list is recoverable and should be recovered.
  5. Extend paths_from_stdin_is_read_once — the fix(cli/check): @generated marker and committed .gitignore silently bypass the gate #1055 test that pins
    the walk half — with a --check-exclude configured, which is the
    branch the issue notes stays uncovered. The assertion is that the
    exempted file is absent from the violations, and that the run's exit
    code reflects it.
  6. Add the non-stdin regression too. A --paths-from <file> list
    with a [check.exclude] glob must keep working identically; the fix(cli): bca diff --since subtree mis-pairs; [check.exclude] ignores --paths-from seeds #497
    plumbing this replaces existed to fix exactly that case, and a fix
    that repairs stdin while regressing the file case trades one silent
    wrong gate for another.
  7. Test-via-revert: restore the re-read and confirm the new stdin
    test fails while the file test still passes. That difference is the
    evidence the fix is aimed at the right branch.

Assessment

Dimension Rating
Difficulty Medium
Complexity Medium
Priority High

Resolution

Fixed on branch fix/issue-1306, commit ac61a5fd.

run_check_walk now resolves --paths-from into globals.paths once,
before the walk, and carries the materialized list out in the new
CheckWalk::seeds. apply_check_exclude takes that list
(seeds: &[PathBuf]) in place of the (paths, paths_from) pair, so
the second read_paths_from — the one that returned an empty list
because the walk had already drained stdin — no longer exists.

  • The CheckExcludes::resolve early return is untouched: the
    no-exclude run still does no glob-set build and no
    --check-exclude-from read. The replacement comment says that
    explicitly, so the re-read is not invited back.
  • The remediation footer renders from the GlobalOpts clone taken
    before the walk and keeps printing --paths-from -; a new assertion
    pins it.
  • New test check_exclude_anchors_paths_from_stdin_seeds
    (tests/check/check_exclude.rs), the stdin sibling of the fix(cli): bca diff --since subtree mis-pairs; [check.exclude] ignores --paths-from seeds #497 case.
    Test-via-revert: restoring the re-read fails exactly that test while
    the --paths-from <file> cases stay green.

Activity

  1. added
    bugSomething isn't working
    clibca CLI surface
    on Aug 9, 2026
  2. dekobon commented on Aug 9, 2026

    @dekobon
    OwnerAuthor

    Added resolution plan and assessment ratings (difficulty Medium / complexity Medium / priority High). Confirmed on main (d65b304), and the code carries the decision that causes it: the comment at commands/check.rs:494-499 states the re-read exists to keep the hot no-exclude path allocation-free. read_paths_from (path_io.rs:253) resolves - to stdin and expand_seed_paths (walk.rs:399) has already drained it.

    The plan keeps that fast path intact (the CheckExcludes::resolve early return is unchanged) and adds two things: the footer's pre-materialization clone needs an explicit test, and the non-stdin --paths-from <file> case needs its own regression — the #497 plumbing being replaced existed for exactly that, and a fix that repairs stdin while regressing the file case swaps one silent wrong gate for another.

  3. dekobon commented on Aug 26, 2026

    @dekobon
    OwnerAuthor

    Fixed on fix/issue-1306 (commit ac61a5fd)

    Root cause

    Confirmed by reproduction before the fix, with the shape from the
    issue body:

    $ printf '%s\n' "$corpus" | bca check --no-config --paths-from - \
        --threshold cyclomatic=1 --check-exclude './excluded.rs'
    excluded.rs:1-1: excluded_offender: cyclomatic = 4 (limit 1)   # exempted, still failed
    kept.rs:1-1: kept_offender: cyclomatic = 4 (limit 1)
    EXIT=2
    

    The same run with --paths-from <file> exempted excluded.rs
    correctly, which localises the defect to the stdin source rather than
    to the anchoring itself.

    apply_check_exclude rebuilt the seed list it anchors violations
    against by re-reading --paths-from. read_paths_from
    (path_io.rs) resolves - to stdin, and expand_seed_paths
    (walk.rs) had already drained it during the walk, so the second read
    returned an empty list. Every violation then anchored against the
    --paths set alone — empty in the canonical
    git diff --name-only | bca check --paths-from - invocation — and the
    [check.exclude] glob silently failed to match.

    Changes

    big-code-analysis-cli/src/commands/check.rs:

    • run_check_walk takes globals by mut and materializes
      --paths-from into globals.paths once, before the walk. The read
      expand_seed_paths was going to perform is simply moved, not
      duplicated; the resolved list leaves in the new CheckWalk::seeds.
    • apply_check_exclude drops its paths / paths_from pair for a
      single seeds: &[PathBuf], supplied from CheckWalk::seeds. The
      second read is gone by construction — no rejection of -, no
      special case.
    • The CheckExcludes::resolve early return is unchanged and still the
      first thing the function does, so the no-exclude run does no
      glob-set build and no --check-exclude-from read. The rationale
      comment the old re-read carried is replaced rather than deleted: what
      the fast path actually saves is those two, not the seed-list clone.
    • The GlobalOpts clone in collect_check_violations is taken before
      the walk and now carries a comment saying so, since it is what keeps
      the remediation footer printing the caller's --paths-from -
      spelling instead of the expanded list.

    Tests

    big-code-analysis-cli/tests/check/check_exclude.rs:
    check_exclude_anchors_paths_from_stdin_seeds — the stdin sibling of
    the #497 test, same sibling-tempdir corpus (which is what keeps the
    seed absolute). Asserts the exempted offender is absent, the kept one
    present and still failing the gate (exit 2), the
    skipped 1 violations via [check.exclude] line, and that the
    footer still echoes --paths-from - without having grown a
    --paths <corpus> entry from the materialization.

    The non-stdin regression the plan asks for already exists as
    check_exclude_anchors_paths_from_seeds (#497) and
    check_skipped_inputs::paths_from_file_is_honoured; both were kept
    green throughout.

    Test-via-revert: restoring the old re-read as a precise inverse
    edit in collect_check_violations (rebuild the seed list from
    globals_kept.paths + a second read_paths_from) failed exactly one
    test — the new stdin one — with the file-list tests still passing.
    Harness cross-check: the runner exited 101, matching the reported
    single failure.

    Full make pre-commit is green (BCA_GATE: pass).

    Edge cases checked

    --paths-from - with empty stdin, --paths combined with
    --paths-from, a nonexistent --paths-from file (still
    error: --paths-from nope.txt: No such file or directory, exit 1),
    and no --paths-from at all — all behave as they did before the
    change. Non-UTF-8 path bytes go through the same unchanged
    read_paths_from.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclibca CLI surface

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions