Summary
src/metrics/abc.rs's mod tests carries no feature gate, so 2,598 of
3,118 tests in it fail under cargo test -p big-code-analysis --no-default-features --features rust. Every per-language test in the module
is compiled regardless of whether its grammar is enabled.
Why it matters
.claude/rules/testing.md requires a #[cfg(any(feature = ...))] union gate
on any test whose fixture table is feature-dependent, so a build enabling
none of its rows drops the test rather than failing it. Individual modules
inside abc.rs now honour that — the newer ones added across 2026-09 carry
both the union gate and a non-vacuity guard. The enclosing module does
not, so the discipline is being applied test-by-test inside a container that
defeats it.
The practical effect is that a minimal-feature build cannot be used to verify
anything about ABC: the signal is buried under ~2,600 unrelated failures that
are bootstrap artifacts rather than regressions — the same class of
misleading-red as the missing-corpora case #1171 fixed.
A second trap in the same area
A workspace-wide cargo test --no-default-features --features X does not
isolate features at all: big-code-analysis-cli and big-code-analysis-web
depend on the library with default features, and cargo unifies them, so
everything is silently re-enabled. Three different feature sets reported an
identical 4 passed; 3428 filtered out before this was noticed — the
uniform-number tell from .claude/rules/testing.md.
Only -p big-code-analysis isolates. Any feature-gate verification run
without it proves nothing, and proves it while looking green. Worth a line in
.claude/rules/testing.md alongside the existing gate guidance.
Suggested shape
Gate mod tests on the union of every language feature its contents name, or
split the per-language sections into already-gated child modules and gate the
parent on their union. Verify against a subset enabling none of them — note
rust,typescript is not such a subset for any table with a TypeScript or TSX
row, so a single non-listed language (--features go) is the reproducer.
Related
RESOLVED
All three items below are done on fix/1472-gate-per-language-tests
(12 commits, not pushed). Detail in the closing comment. In short:
- The 2,594 ungated tests — re-measured as 3,025 across
src/,
big-code-analysis-ast/src/ and tests/; 3,235 markers now, all but
a handful derived.
- The gate that derives the marker —
utils/check-test-lang-gates.py,
wired into make pre-commit / make ci. It reproduces all 167 gates
written by hand in this tree, which is what licensed generating the
rest.
Subject.carries_tests — fixed, with the matcher on the
qualified module path.
The feature-matrix CI legs now run the suite rather than only
compiling it, which is what #1285 was waiting for.
One correction to the body below: the "30 of 3,482" figure does not
reproduce. 80 of 3,469 src/ tests carried a feature cfg of their
own.
Update — the CI half is closed; three items remain
PR #1476 closed the CI half of this issue. .github/workflows/ci.yml's
feature-matrix job now documents that it runs clippy and not the suite,
and a new utils/check-feature-gates.py runs in each leg asserting that a
union-gated test subject is absent under a feature set enabling none of
its rows. What remains:
1. The 2,594 ungated per-language lib tests (the original issue)
Unchanged. Measured on fix/batch-2026-09-14: --no-default-features --features rust fails 2,642 of 3,238 tests, 2,594 of them in the lib
target. Root cause is a contract violation, not a missing cfg in the
abstract:
panicked at big-code-analysis-ast/src/node.rs:84:
invariant: the caller checked LANG::is_enabled, or reached this through
AnyParser: LanguageDisabled(Python)
ParserTrait::new's own doc says the same and directs callers to check
LANG::is_enabled first. Only 30 of 3,482 #[test] fns in src/ do.
By module (halved for nextest's double-reporting): abc 525, loc 363,
cognitive 346, halstead 211, cyclomatic 207, npm 187, npa 180, wmc 167,
nargs 136, nexits 111, nom 77, tokens 36, mi 7.
Two approaches were considered and rejected during #1476; record them so
they are not re-proposed:
- Gate each test. Semantically right, ~2,600 attributes. The hazard is
that a cross-language parity test needs all(feature = …) where a
per-language test needs one feature, and a mechanical rewrite that gets
that backwards produces a test that silently stops running.
- Make the helpers self-skip on
is_enabled(). Only 3 of ~13 helpers
take a runtime LANG; the rest take a type parameter, and ParserTrait
exposes no language accessor, so it would need an additive change to a
published trait. The disqualifier is independent of that: it would turn
~2,600 tests into silent no-ops under partial features, which
.claude/rules/testing.md treats as worse than no test.
2. check-feature-gates.py cannot see a missing gate
A scanner of declared markers reports a clean tree when the marker is
deleted, because deleting it deletes the subject. That is the
hidden_literal_supertypes shape (fixed in 0e15c49c on #1476), and only
its compile half — unused imports at -D warnings — was caught, by clippy.
The gate catches the defeated direction (a union present but weakened,
e.g. a non-feature disjunct), verified both ways during #1476. Catching the
missing direction needs a different gate: derive what the marker should
be from the union of the item's inner #[cfg(feature = …)] rows and compare
it against what is declared. That is the natural companion to item 1, since
the same derivation would drive a mechanical gating pass.
3. A latent false pass in Subject.carries_tests
utils/check-feature-gates.py:265 classifies a mod as test-bearing only
when it carries its own bare cfg(test) predicate:
return (self.kind == "mod" and self.is_cfg_test) or (
self.kind == "fn" and self.is_test_fn
)
A union-gated mod nested inside an already-#[cfg(test)] parent has no
test in its own attribute, so it is classified compile-time only and
never compared against the nextest listing — a false pass in exactly the
#1220 shape the gate exists to catch. No current subject hits it (all 34 are
top-level #[cfg(test)] mod), so it is latent.
The naive repair is wrong: subject_matcher anchors on the bare name
((?:^|::)name(?:::|$)), so admitting any mod makes a subject named
tests match every ::tests:: path in the crate and fail every disjoint
leg. Narrowing it needs the subject's full module path rather than its bare
name.
Also noticed, adjacent
src/test_support.rs:321 — assert_csharp_fixture_spells carries no
#[cfg(feature = …)] while its sibling assert_perl_fixture_spells (:329)
carries #[cfg(feature = "perl")]. Both are correct today: the C# callers
are ungated and the Perl ones are gated. It becomes dead_code at
-D warnings the moment a C# test module is gated the way
perl_statement_modifier_parity is — i.e. the first time item 1 is worked
on. Adding the gate before then would break the build.
Summary
src/metrics/abc.rs'smod testscarries no feature gate, so 2,598 of3,118 tests in it fail under
cargo test -p big-code-analysis --no-default-features --features rust. Every per-language test in the moduleis compiled regardless of whether its grammar is enabled.
Why it matters
.claude/rules/testing.mdrequires a#[cfg(any(feature = ...))]union gateon any test whose fixture table is feature-dependent, so a build enabling
none of its rows drops the test rather than failing it. Individual modules
inside
abc.rsnow honour that — the newer ones added across 2026-09 carryboth the union gate and a non-vacuity guard. The enclosing module does
not, so the discipline is being applied test-by-test inside a container that
defeats it.
The practical effect is that a minimal-feature build cannot be used to verify
anything about ABC: the signal is buried under ~2,600 unrelated failures that
are bootstrap artifacts rather than regressions — the same class of
misleading-red as the missing-corpora case #1171 fixed.
A second trap in the same area
A workspace-wide
cargo test --no-default-features --features Xdoes notisolate features at all:
big-code-analysis-cliandbig-code-analysis-webdepend on the library with default features, and cargo unifies them, so
everything is silently re-enabled. Three different feature sets reported an
identical
4 passed; 3428 filtered outbefore this was noticed — theuniform-number tell from
.claude/rules/testing.md.Only
-p big-code-analysisisolates. Any feature-gate verification runwithout it proves nothing, and proves it while looking green. Worth a line in
.claude/rules/testing.mdalongside the existing gate guidance.Suggested shape
Gate
mod testson the union of every language feature its contents name, orsplit the per-language sections into already-gated child modules and gate the
parent on their union. Verify against a subset enabling none of them — note
rust,typescriptis not such a subset for any table with a TypeScript or TSXrow, so a single non-listed language (
--features go) is the reproducer.Related
RESOLVED
All three items below are done on
fix/1472-gate-per-language-tests(12 commits, not pushed). Detail in the closing comment. In short:
src/,big-code-analysis-ast/src/andtests/; 3,235 markers now, all buta handful derived.
utils/check-test-lang-gates.py,wired into
make pre-commit/make ci. It reproduces all 167 gateswritten by hand in this tree, which is what licensed generating the
rest.
Subject.carries_tests— fixed, with the matcher on thequalified module path.
The
feature-matrixCI legs now run the suite rather than onlycompiling it, which is what #1285 was waiting for.
One correction to the body below: the "30 of 3,482" figure does not
reproduce. 80 of 3,469
src/tests carried a featurecfgof theirown.
Update — the CI half is closed; three items remain
PR #1476 closed the CI half of this issue.
.github/workflows/ci.yml'sfeature-matrixjob now documents that it runs clippy and not the suite,and a new
utils/check-feature-gates.pyruns in each leg asserting that aunion-gated test subject is absent under a feature set enabling none of
its rows. What remains:
1. The 2,594 ungated per-language lib tests (the original issue)
Unchanged. Measured on
fix/batch-2026-09-14:--no-default-features --features rustfails 2,642 of 3,238 tests, 2,594 of them in the libtarget. Root cause is a contract violation, not a missing
cfgin theabstract:
ParserTrait::new's own doc says the same and directs callers to checkLANG::is_enabledfirst. Only 30 of 3,482#[test]fns insrc/do.By module (halved for nextest's double-reporting): abc 525, loc 363,
cognitive 346, halstead 211, cyclomatic 207, npm 187, npa 180, wmc 167,
nargs 136, nexits 111, nom 77, tokens 36, mi 7.
Two approaches were considered and rejected during #1476; record them so
they are not re-proposed:
that a cross-language parity test needs
all(feature = …)where aper-language test needs one feature, and a mechanical rewrite that gets
that backwards produces a test that silently stops running.
is_enabled(). Only 3 of ~13 helperstake a runtime
LANG; the rest take a type parameter, andParserTraitexposes no language accessor, so it would need an additive change to a
published trait. The disqualifier is independent of that: it would turn
~2,600 tests into silent no-ops under partial features, which
.claude/rules/testing.mdtreats as worse than no test.2.
check-feature-gates.pycannot see a missing gateA scanner of declared markers reports a clean tree when the marker is
deleted, because deleting it deletes the subject. That is the
hidden_literal_supertypesshape (fixed in0e15c49con #1476), and onlyits compile half — unused imports at
-D warnings— was caught, by clippy.The gate catches the defeated direction (a union present but weakened,
e.g. a non-feature disjunct), verified both ways during #1476. Catching the
missing direction needs a different gate: derive what the marker should
be from the union of the item's inner
#[cfg(feature = …)]rows and compareit against what is declared. That is the natural companion to item 1, since
the same derivation would drive a mechanical gating pass.
3. A latent false pass in
Subject.carries_testsutils/check-feature-gates.py:265classifies amodas test-bearing onlywhen it carries its own bare
cfg(test)predicate:A union-gated
modnested inside an already-#[cfg(test)]parent has notestin its own attribute, so it is classifiedcompile-time onlyandnever compared against the nextest listing — a false pass in exactly the
#1220 shape the gate exists to catch. No current subject hits it (all 34 are
top-level
#[cfg(test)] mod), so it is latent.The naive repair is wrong:
subject_matcheranchors on the bare name(
(?:^|::)name(?:::|$)), so admitting anymodmakes a subject namedtestsmatch every::tests::path in the crate and fail every disjointleg. Narrowing it needs the subject's full module path rather than its bare
name.
Also noticed, adjacent
src/test_support.rs:321—assert_csharp_fixture_spellscarries no#[cfg(feature = …)]while its siblingassert_perl_fixture_spells(:329)carries
#[cfg(feature = "perl")]. Both are correct today: the C# callersare ungated and the Perl ones are gated. It becomes
dead_codeat-D warningsthe moment a C# test module is gated the wayperl_statement_modifier_parityis — i.e. the first time item 1 is workedon. Adding the gate before then would break the build.