Repository navigation
fix(getter/groovy): gate the Super arm on its Wildcard parent, as Java does #1419
Description
Correction (2026-09-11): the premise below is wrong —
Groovy::Superis emitted, in a wildcard type bound (? super T), where it correctly bills as an operator. Both proposed options would break something. See the correction comment for the measurement and the right fix (a Java-style parent gate).
Summary
big-code-analysis-ast/src/getter/groovy.rs:137 lists Super in the
operator arm of get_op_type. The Groovy grammar never emits
Groovy::Super (id 74) for a super reference — it emits a plain
identifier (id 1) — so the arm is dead, and super is classified as an
operand via the identifier path.
That happens to be the right answer since #1380, which made a
super-reference an operand in Java, C# and Kotlin to match the eleven
other languages. But it is right by accident: the only thing keeping
Groovy on the correct side is that its grammar does not produce the kind
the arm names. A grammar bump that starts emitting a dedicated super
node would silently flip Groovy to operator and reintroduce exactly the
inconsistency #1380 closed — with no test failing, because no fixture
exercises a kind that is currently unreachable.
Measured
At the pinned grammars, on the integration branch carrying the #1380 fix:
class B extends A {
B() { super(1) }
def h() { super.g() }
}$ bca dump --no-config -p g.groovy | rg ': super'
│ │ ├─ {identifier:1} from (2, 9) to (2, 14) : super
│ │ │ ├─ {identifier:1} from (3, 13) to (3, 18) : super
$ bca ops --no-config -p g.groovy
|- operators
| |- () |- . |- def |- extends |- {}
|- operands
| |- 1 |- A |- B |- g |- h `- superBoth the constructor-delegation spelling and the qualified-access
spelling reach identifier. A.super.h() and super::h were checked
too, with the same result.
The enum variant exists (language_groovy.rs:88 Super = 74, rendered
at :404), so the arm compiles and reads as live.
Why this is the §2 case
.claude/rules/grammar-dispatch.md §2 covers the shape: a variant that
exists in the enum but the parser never emits. Its prescription is to
keep the defensive arm and pin its unreachability with an assertion
naming the rule, "otherwise a future grammar that promotes the rule
changes behaviour invisibly". That is precisely the risk here, and there
is no such pin.
Options
- Pin it. Keep the arm and add
!ast_has_kind_id(&parser, Groovy::Super as u16)asserting the kind is
unreachable, so a grammar bump that promotes it fails loudly instead of
silently reclassifying. - Move it. If the arm is kept for the day the grammar does emit it,
moveSuperto the operand arm so the defensive path agrees with the
live path — the answer fix(getter): self- and super-references split three ways across languages #1380 settled for every other language.
Option 2 plus option 1's assertion is probably right: agreement between
the two paths, and a loud failure if the grammar changes.
Check This at the same time — Groovy's this is also a plain
identifier and the same question applies.
Found while fixing #1380, which moved This / Super / Base to the
operand arm in Java, C# and Kotlin. Not fixed there because Groovy needed
no behaviour change and a dead-arm cleanup is a different claim from a
reclassification.
Resolution
Fixed in 7f2a1feb (branch fix/issue-1419, integration branch
fix/batch-2026-09-14).
Super is pulled out of the ungated operator alternation in
big-code-analysis-ast/src/getter/groovy.rs and given Java's parent
gate:
Super if ancestors.parent_has_kind(node, Wildcard as u16) => TokenRole::Operator,
Super => TokenRole::Operand,Groovy::Wildcard = 233 has no numeric-suffix alias. The gate uses
Ancestors::parent_has_kind — the same O(1) chain lookup Java's
ancestors.parent(node).map(...) performs, and the spelling the
SLASH slashy-string guard in the same match already uses.
No metric moves. The corrected premise in the comments above is
confirmed against the grammar source: dekobon-tree-sitter-groovy 0.2.2
spells 'super' only inside the wildcard production, so every
super-reference was already a plain identifier and already an operand.
The fix is forward compatibility — it removes the grammar accident that
a bump routing a reference to kind 74 would have turned into a
misclassification. The workspace suite is green with no
integration-snapshot drift.
Tests. groovy_wildcard_super_bound_stays_an_operator gains a
reference-direction assertion over all five super-reference spellings;
its bound half now exercises the gate's operator branch and was verified
by inverting the gate (perturbation, not a file-level revert). The
gate's operand branch is unreachable under the pin — only error recovery
on invalid Groovy (List<? super>) detaches the token from its
wildcard — so it is left untested per grammar-dispatch section 6,
with the existing node census as the drift marker.
Side effect. .rustfmt-bail-baseline.txt ratcheted for
getter/groovy.rs 5 → 7: two more arms in a match that already bails on
pre-existing in-pattern section comments, exactly as the same split did
for Java.
make pre-commit → BCA_GATE: pass.
Activity
dekobon commentedon Sep 11, 2026
Correction — the premise of this issue is wrong, and both proposed options would break something
I filed this issue claiming Groovy's
Superoperator arm "never fires" becausethe grammar never emits
Groovy::Super(id 74). That is false. I measuredonly receiver positions (
super(1),super.h(),A.super.h(),super::h),where the grammar does emit a plain
identifier, and wrongly generalised toevery position.
The grammar emits
Groovy::Superin a wildcard type bound:So the arm is live, and it bills
superas an operator in exactly theposition where #1380 decided
supershould stay an operator:? super Tisthe mirror of
? extends T, and Java keeps it an operator behind a parent gate(
java_wildcard_super_bound_stays_an_operator). Groovy's current behaviour istherefore already correct for both positions — the wildcard
superis anoperator via this arm, and the receiver
superis an operand via theidentifier path.
Why both options in the body are wrong
!ast_has_kind_id(Groovy::Super)assertion fails on any wildcard fixture, because the kind is reachable.
Superto the operand arm) — makes? super Tanoperand, diverging from Java and reintroducing the very inconsistency fix(getter): self- and super-references split three ways across languages #1380
closed.
What the real latent risk is, and the right fix
The only remaining exposure is narrower than this issue described: if a grammar
bump started emitting
Groovy::Superin receiver position, the arm would billthat as an operator. The fix is the same parent gate Java uses —
Superundera
Wildcardparent is an operator, and an operand everywhere else — plus agroovy_wildcard_super_bound_stays_an_operatortest mirroring Java's. Thatmakes Groovy correct by construction rather than by grammar accident, in both
positions.
That gate is being applied on the
fix/batch-2026-09-10branch as part of thewhole-branch
/code-reviewremediation. The comment I added togetter/groovy.rsin4c5af945repeated this issue's false premise ("neverfires") and is being corrected in the same pass.
Found by the
/code-review maxpass over that branch.[-]fix(getter/groovy): the dead Super operator arm is unpinned and would flip on a grammar bump[/-][+]fix(getter/groovy): gate the Super arm on its Wildcard parent, as Java does[/+]dekobon commentedon Sep 11, 2026
Closing: the premise was wrong — the arm is live and correct
I filed this, and I tested too narrowly. It says Groovy's
Superoperator arm "never fires" because the grammar spells a super-reference
as a plain
identifier. That holds for every reference form I checked(
super(1),super.h(),A.super.h(),super::h). But I neverchecked a wildcard bound, and there the grammar does emit
Groovy::Super:So the arm fires for every
? super T. There, billingsuperas anoperator is right: it mirrors
? extends T'sextends, which is alsoan operator, and it matches the decision #1380 gated Java to
(
java_wildcard_super_bound_stays_an_operator).Both proposed remedies were wrong
!ast_has_kind_id(…, Groovy::Super)) wouldfail on any file that uses a wildcard, because the kind is reachable.
Superto the operand arm would bill a wildcard bound as anoperand and split Groovy from Java's pinned wildcard decision.
What was actually worth doing is done in #1424
4c5af945is corrected ingetter/groovy.rs.groovy_wildcard_super_bound_stays_an_operatortest pins thewildcard-bound direction.
pinned by
tests/parity/self_reference_operand_parity.rs. That testalready catches the grammar-bump flip this issue worried about.
Groovy is on the correct side for both uses by construction, so there is
no defect to fix. Closing as not planned. Credit to the max-effort review
of #1424 for catching the wildcard case.
dekobon commentedon Sep 11, 2026
Reopening — I closed this too early, and the gate was not applied
Correcting my own closing comment above, which was wrong on one point.
The 18:28 comment reframed this issue: the premise was wrong, but one
real item remained — gate Groovy's
Superon itsWildcardparent, asJava does, so Groovy is correct by construction rather than by grammar
accident. It said that gate "is being applied on the
fix/batch-2026-09-10branch". It was not. Only two of the threepieces landed in #1424:
getter/groovy.rscorrectedgroovy_wildcard_super_bound_stays_an_operatortestSupergated on aWildcardparentSuperstill sits in the ungated operator arm(
big-code-analysis-ast/src/getter/groovy.rs:152). I missed thereframing, closed the issue on its original premise, and wrongly said
everything worth doing was done in #1424.
Current state is correct, and guarded in both directions
? super T) emitsGroovy::Superand is billedas an operator. That is right, and the new test pins it.
identifierand is billed as an operand.That is right, and
tests/parity/self_reference_operand_parity.rspinsit.
So nothing is miscounted today. If a grammar bump started emitting
Groovy::Superfor a reference, the ungated arm would bill it as anoperator. The parity test would then fail loudly rather than let it
through silently.
What remains
Gate the arm like Java does (
getter/java.rs, #1380):Superunder aWildcardparent is an operator; everywhere else it is an operand. Thatturns the parity test's future failure into a correct classification.
It is a small forward-compatibility change. Moving
Superout of theshared or-pattern into its own arm will raise Groovy's entry in
.rustfmt-bail-baseline.txt, as the same move did for Java.Left open for that.
fix: correct false claims found by the whole-branch review
dekobon commentedon Sep 14, 2026
Fixed in
7f2a1febonfix/issue-1419(integration branchfix/batch-2026-09-14).Root cause
GroovyCode::get_op_typelistedSuperin an ungated operatoralternation. The corrected premise in the comments above holds: nothing
is miscounted today, because the pinned
dekobon-tree-sitter-groovy 0.2.2grammar spells'super'in exactly one production —— so every super-reference (
super(1),super.h(),A.super.h(),super::h,super?.h()) parses as a plainidentifier(kind 1) andreaches the operand arm, while the one shape that does emit
Groovy::Super(kind 74) is the wildcard bound, where operator is theright answer. The exposure was purely forward-looking: a grammar bump
routing a reference to kind 74 would have billed it as an operator.
Changes
big-code-analysis-ast/src/getter/groovy.rs—Superpulled out of thebig operator alternation and given Java's parent gate:
Groovy::Wildcard = 233is the only wildcard variant inlanguage_groovy.rs— no numeric-suffix alias, per grammar-dispatchsection 1. The gate uses
Ancestors::parent_has_kindrather than Java'sancestors.parent(node).map(|p| p.kind_id().into())match: it is thesame O(1) chain lookup, it is what the
SLASHslashy-string guard threearms above already calls, and it compares a
u16instead of convertingto the enum first. Java's arm is left alone — equivalent, and out of
scope here.
The stale rationale comments in
getter/groovy.rsandtests/parity/self_reference_operand_parity.rs(both of which describedthe accident and pointed at this issue) are rewritten to describe the
gate that now exists.
No behaviour change
The full workspace suite is green with no integration-snapshot drift
(
tests/repositories/big-code-analysis-outputreports a clean tree),which is the expected result for a fix whose only reachable input is
already classified correctly.
Languages affected
Groovy only. Java already carried the gate (#1380). No other language in
the workspace spells a super-reference as a dedicated keyword token that
this arm shape would apply to.
Tests
src/metrics/halstead.rs,groovy_wildcard_super_bound_stays_an_operator:Verified by perturbation rather than by file-level revert (the file
carried the uncommitted fix): inverting the gate to
Some(Wildcard) => TokenRole::Operandfails it with`? super String` must keep `super` an operator; operators were ["()", ",", "<", ">", "?", "extends", "{}"]The exact inverse edit restores it;
git diff --statconfirmednothing else moved.
message updated — it no longer describes an "ungated operator arm".
assert_keywords_are_operands_only::<GroovyParser>over a fixturecarrying all five super-reference spellings plus
this.x. The fixtureparses with no
ERROR/MISSINGnode (verified bybca dump). Thispins valid-input behaviour the gate must not move; it is additive to
the parity row, which covers only
super.h()andthis.x.tests/parity/self_reference_operand_parity.rs(24 tests) passesunchanged.
The untested branch, deliberately
The gate's operand branch is unreachable under the pin. The only input
that reaches it is error recovery on invalid Groovy —
class A { void m(List<? super> a) { } }puts{super:74}under an{ERROR:65535}parent. Per grammar-dispatch section 6, pinning thatshape's numbers would make the grammar's present over-permissiveness the
contract and invert into a bug-lock when it tightens, so no test was
added for it and the arm's comment says why. The node census is what
fails by name if a bump ever makes the branch live.
Side effect
.rustfmt-bail-baseline.txt:getter/groovy.rs5 → 7. SplittingSuperinto its own arm adds two arms to a match that already bails — cause 1,
pre-existing, from the in-pattern section comments in the big operator
alternation.
.claude/rules/formatting.mdrules out clearing those, sothe baseline is ratcheted with
./utils/check-rustfmt-bail.py --update.The same move produced the same effect for Java. Both new arms were
checked by hand for width (93 and 40 columns) since
cargo fmtcannotsee into a bailing match.
Validation
make pre-commit→BCA_GATE: pass (gate=pre-commit).