Repository navigation
fix(cli/check): [check.exclude] re-anchoring re-reads --paths-from, breaking stdin lists #1306
Description
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-fromhere (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.
- Materialize once and thread the result, as the issue directs.
Resolve--paths-frominto the effective seed list inrun_check_walk
before the walk, and pass that list toapply_check_excludein
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. - Preserve the fast path the comment defends.
CheckExcludes::resolve
returningNoneis still the first thingapply_check_excludedoes,
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. - Keep the remediation footer showing the user's original
--paths-fromspelling. 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. - 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. - 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-excludeconfigured, 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. - 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. - 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::resolveearly return is untouched: the
no-exclude run still does no glob-set build and no
--check-exclude-fromread. The replacement comment says that
explicitly, so the re-read is not invited back. - The remediation footer renders from the
GlobalOptsclone 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
dekobon commentedon Aug 9, 2026
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 andexpand_seed_paths(walk.rs:399) has already drained it.The plan keeps that fast path intact (the
CheckExcludes::resolveearly 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.dekobon commentedon Aug 26, 2026
Fixed on
fix/issue-1306(commitac61a5fd)Root cause
Confirmed by reproduction before the fix, with the shape from the
issue body:
The same run with
--paths-from <file>exemptedexcluded.rscorrectly, which localises the defect to the stdin source rather than
to the anchoring itself.
apply_check_excluderebuilt the seed list it anchors violationsagainst by re-reading
--paths-from.read_paths_from(
path_io.rs) resolves-to stdin, andexpand_seed_paths(
walk.rs) had already drained it during the walk, so the second readreturned an empty list. Every violation then anchored against the
--pathsset alone — empty in the canonicalgit 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_walktakesglobalsbymutand materializes--paths-fromintoglobals.pathsonce, before the walk. The readexpand_seed_pathswas going to perform is simply moved, notduplicated; the resolved list leaves in the new
CheckWalk::seeds.apply_check_excludedrops itspaths/paths_frompair for asingle
seeds: &[PathBuf], supplied fromCheckWalk::seeds. Thesecond read is gone by construction — no rejection of
-, nospecial case.
CheckExcludes::resolveearly return is unchanged and still thefirst thing the function does, so the no-exclude run does no
glob-set build and no
--check-exclude-fromread. The rationalecomment the old re-read carried is replaced rather than deleted: what
the fast path actually saves is those two, not the seed-list clone.
GlobalOptsclone incollect_check_violationsis taken beforethe 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 ofthe #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 thefooter 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) andcheck_skipped_inputs::paths_from_file_is_honoured; both were keptgreen throughout.
Test-via-revert: restoring the old re-read as a precise inverse
edit in
collect_check_violations(rebuild the seed list fromglobals_kept.paths+ a secondread_paths_from) failed exactly onetest — 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-commitis green (BCA_GATE: pass).Edge cases checked
--paths-from -with empty stdin,--pathscombined with--paths-from, a nonexistent--paths-fromfile (stillerror: --paths-from nope.txt: No such file or directory, exit 1),and no
--paths-fromat all — all behave as they did before thechange. Non-UTF-8 path bytes go through the same unchanged
read_paths_from.docs(changelog): consolidate entries from batch fix
docs(lessons): record batch #1306/#1263/#1301/#1247 lessons
fix(cli): materialize --paths-from once for check
docs(changelog): consolidate entries from batch fix
docs(lessons): record batch #1306/#1263/#1301/#1247 lessons