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
16 changes: 8 additions & 8 deletions .bca-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ value = 7.0
[[entry]]
path = "big-code-analysis-cli/src/commands/check.rs"
qualified = "emit_check_results"
start_line = 510
start_line = 560
metric = "nargs"
value = 8.0

[[entry]]
path = "big-code-analysis-cli/src/commands/check.rs"
qualified = "filter_by_baseline"
start_line = 321
start_line = 371
metric = "nargs"
value = 8.0

Expand All @@ -114,7 +114,7 @@ path = "big-code-analysis-cli/src/commands/check.rs"
qualified = "run_check"
start_line = 13
metric = "halstead.effort"
value = 69823.99851963262
value = 70257.02522730803

[[entry]]
path = "big-code-analysis-cli/src/commands/check/effective_config.rs"
Expand Down Expand Up @@ -196,21 +196,21 @@ value = 6.0
[[entry]]
path = "big-code-analysis-cli/src/dispatch.rs"
qualified = "dispatch_exemptions"
start_line = 536
start_line = 548
metric = "nexits"
value = 5.0

[[entry]]
path = "big-code-analysis-cli/src/dispatch.rs"
qualified = "dispatch_metrics"
start_line = 191
start_line = 203
metric = "nargs"
value = 7.0

[[entry]]
path = "big-code-analysis-cli/src/dispatch.rs"
qualified = "dispatch_ops"
start_line = 252
start_line = 264
metric = "nargs"
value = 8.0

Expand Down Expand Up @@ -266,7 +266,7 @@ value = 7.0
[[entry]]
path = "big-code-analysis-cli/src/lib.rs"
qualified = "legacy_hint"
start_line = 519
start_line = 529
metric = "halstead.effort"
value = 59351.805921378844

Expand Down Expand Up @@ -982,7 +982,7 @@ path = "src/tools.rs"
qualified = "read_file_with_eol"
start_line = 140
metric = "nexits"
value = 6.0
value = 7.0

[[entry]]
path = "src/vcs/bus_factor.rs"
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,39 @@ for historical reference.

### Fixed

- `bca check` no longer exits `0` when input files could not be read
(#1060). The counter backing the "no input files matched" guard was
bumped before the read was attempted, so a tree whose every file was
unreadable (permission denied, a broken symlink, a container
volume-mount mismatch) reported `error processing <path>: …` on
stderr and then exited `0` — the worst failure mode a CI gate has.
The counter now moves only for files that were actually read, and
read failures are tallied separately: any input file that failed to
read exits `1` (tool error, distinct from `2` = gate breach) with a
summary line, because a partially analysed gate is not a passing
gate. Like the pre-existing empty-input guard, the check runs before
the gate is evaluated and is not suppressed by `--no-fail`, which
suppresses threshold failures rather than broken input. `bca init`
scaffolds its baseline through the same walk, so it inherits the
guard and refuses to pin a baseline that would silently under-record
the debt in a file it could not read. Both guards' messages are now
prefixed `bca:` rather than `bca check:`, which misattributed an
`init` failure to a subcommand the user never ran. Other subcommands
are unchanged for now; extending the same contract past `check` is
tracked separately.

`read_file_with_eol` is fixed on the same issue: its "≤ 3 bytes is
not worth parsing" shortcut returned `Ok(None)` from a bare `stat`,
and `stat` succeeds on a file the process cannot open — so a tiny
unreadable file was indistinguishable from an empty one and the
permission error the function documents never surfaced. `bca check`
on a 3-byte unreadable file therefore exited 0 with no diagnostic at
all, not even the per-file `error processing` line. The shortcut now
confirms readability by opening the file first; the open is skipped
for anything that is not a regular file, so the function still never
blocks on a FIFO. Behaviour is unchanged for readable files of any
size.

- `Ops::operators` and `Ops::operands` are now sorted in
byte-lexicographic order, so `bca ops` produces identical bytes for
identical input (#1091). Both vectors were collected from `HashMap`
Expand Down
11 changes: 10 additions & 1 deletion big-code-analysis-book/src/commands/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ fails the pipeline before the change lands.
|------|---------|
| `0` | All functions within thresholds (or `--no-fail` set). |
| `2` | At least one threshold exceeded. |
| `1` | Tool error (bad arguments, unreadable config, unknown metric). |
| `1` | Tool error (bad arguments, unreadable config or input, unknown metric). |

`1` is reserved so CI can distinguish a regression (`2`) from a tool
misconfiguration (`1`).

A gate that could not read all of its input has no verdict to report,
so two input problems exit `1` rather than `0`: nothing matched
`--paths` / `--include` / `--exclude`, and any input file that failed
to read (permission denied, a broken symlink, a path that vanished
mid-walk). Each unreadable file is named on stderr as `error processing
<path>: …` before the summary line. Both checks run before the gate is
evaluated and are not suppressed by `--no-fail`, which suppresses
threshold failures, not broken input.

### Tiered exit codes (`--exit-codes=tiered`) {#tiered-exit-codes---exit-codestiered}

`--exit-codes=tiered` (or `[check] exit_codes = "tiered"` in
Expand Down
84 changes: 69 additions & 15 deletions big-code-analysis-cli/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,9 @@ pub(crate) fn run_check(
// `format_remediation_block` needs the resolved `--paths` /
// `--exclude` set to compose a copy-paste-safe refresh command.
let globals_for_remediation = globals.clone();
let (violations, files_dispatched) = run_check_walk(globals, &args, preproc, set);

if files_dispatched.load(Ordering::Relaxed) == 0 {
// No files survived `--paths` expansion + `--include`/`--exclude`
// filtering. Treat this as a tool error (exit 1), not a clean
// pass (exit 0): a typo in `--paths` would otherwise silently
// green-light CI.
die("bca check: no input files matched; check --paths, --include, --exclude");
}
let walk = run_check_walk(globals, &args, preproc, set);
enforce_usable_input(&walk);
let violations = walk.violations;

// Drop offenders from `[check.exclude]` files (#378) before *any*
// downstream consumer sees them — so `--write-baseline` never
Expand Down Expand Up @@ -163,24 +157,80 @@ pub(crate) fn run_check(
}
}

/// What the check walk produced: the sorted violations plus the two
/// post-walk tallies `run_check` consults before it trusts the gate
/// verdict. Named fields rather than a tuple because the counters are
/// the same primitive type and mean opposite things — swapping them
/// would turn "nothing matched" into "everything was unreadable".
struct CheckWalk {
violations: Vec<Violation>,
/// Files whose contents were read and handed to the pre-dispatch
/// filters. Zero means nothing survived `--paths` expansion plus
/// `--include` / `--exclude` filtering.
files_dispatched: usize,
/// Files the runner could not read at all (#1060). Non-zero means
/// the gate saw less than its input set.
read_failures: usize,
}

/// Fail the run when the walk did not see a usable input set — any file
/// that could not be read at all, or no files whatsoever. Both are tool
/// errors (exit 1) rather than gate results (exit 2), because a gate
/// that analysed less than its input has no verdict to report, and both
/// fire before the gate is evaluated, so neither is suppressed by
/// `--no-fail` (which suppresses threshold failures, not broken input)
/// and neither lets `--write-baseline` record a partial run. Mirrors
/// `enforce_explicit_unrecognized` on the analyze side.
///
/// Both messages are prefixed `bca:` rather than `bca check:`: `bca
/// init` scaffolds its baseline through `run_check`
/// (`commands::init::scaffold_baseline`), so naming the subcommand here
/// misattributes the failure to a command the user never ran.
fn enforce_usable_input(walk: &CheckWalk) {
if walk.read_failures > 0 {
// The consumer has already printed one `error processing <path>:
// …` line per failure. Checked before the empty-input guard so
// an all-unreadable run names its real cause instead of blaming
// the path filters (#1060).
let noun = if walk.read_failures == 1 {
"file"
} else {
"files"
};
die(format_args!(
"bca: {} input {noun} could not be read (see the errors \
above); refusing to trust a partially analysed input set",
walk.read_failures
));
}

if walk.files_dispatched == 0 {
// No files survived `--paths` expansion + `--include`/`--exclude`
// filtering. Treat this as a tool error (exit 1), not a clean
// pass (exit 0): a typo in `--paths` would otherwise silently
// green-light CI.
die("bca: no input files matched; check --paths, --include, --exclude");
}
}

/// Run the parallel walker with a check-flavoured `Config`, collect
/// every emitted `Violation`, and sort them by `(path, start_line,
/// metric)` so CI diff tooling sees identical output across runs over
/// the same tree. Returns the sorted vector plus the
/// `files_dispatched` counter so the caller can detect the "no inputs
/// matched" case.
pub(crate) fn run_check_walk(
/// the same tree.
fn run_check_walk(
globals: GlobalOpts,
args: &CheckArgs,
preproc: Option<Arc<PreprocResults>>,
set: Arc<ThresholdSet>,
) -> (Vec<Violation>, Arc<AtomicUsize>) {
) -> CheckWalk {
let (tx, rx) = std::sync::mpsc::channel();
let files_dispatched = Arc::new(AtomicUsize::new(0));
let read_failures = Arc::new(AtomicUsize::new(0));
let cfg = Config {
threshold_set: Some(set),
check_tx: Some(Mutex::new(tx)),
files_dispatched: Some(Arc::clone(&files_dispatched)),
read_failures: Some(Arc::clone(&read_failures)),
suppression_policy: SuppressionPolicy::from_no_suppress(args.no_suppress),
report_suppressed: args.report_suppressed,
// Compute body hashes during the walk only when fuzzy matching
Expand All @@ -204,7 +254,11 @@ pub(crate) fn run_check_walk(
.then(a.metric.cmp(b.metric))
});

(violations, files_dispatched)
CheckWalk {
violations,
files_dispatched: files_dispatched.load(Ordering::Relaxed),
read_failures: read_failures.load(Ordering::Relaxed),
}
}

/// Serialize and write the collected violations as a baseline TOML
Expand Down
Loading