You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Groovy's indexing and navigation expressions score zero ABC conditions in
a boolean slot where a bare identifier scores one. C# and Kotlin both count
their analogues, so this is a per-language asymmetry rather than a policy
question.
Against fix/batch-2026-09-13, release binary rebuilt from HEAD:
Groovy source
abc.conditions
cyclomatic
def ctl(a) { if (a) { return 1 }; return 0 }
1
2
def sub(l) { if (l[0]) { … } }
0
2
def ssub(l) { if (l?[0]) { … } }
0
2
def safenav(a) { if (a?.b) { … } }
0
3
def dfa(a) { if (a.@b) { … } }
0
2
Siblings classify the analogue:
C# if (l[0]) → 1, via Csharp::ElementAccessExpression in csharp_bool_terminal_kinds!()
Kotlin if (l[0]) → 1 via IndexExpression; if (o?.b) → 1 via NavigationExpression, which covers . and ?. in one kind
a?.b is the worst case: Groovy cyclomatic counts ?. as a decision
(src/metrics/cyclomatic/groovy.rs:32), so ABC sits two below its own
decision count on an entirely idiomatic predicate.
Where
groovy_bool_terminal_kinds! (big-code-analysis-ast/src/macros/kind_sets.rs). field_access is listed; these five are not, and all are alternatives of _expression in dekobon-tree-sitter-groovy 0.2.2:
That sweep's stated completeness evidence was a grammar.json sweep — but
scoped to operator tokens, which structurally cannot see a missing node kind. And the macro's own comment asserted the analogue did not
exist ("the dekobon Groovy grammar has no await or array_access
analogues"), which is false and is the sort of claim that stops the next
reader looking. That sentence is corrected on the PR #1459 branch.
The right audit table is the _expression alternative list: 38 alternatives
against the set's 10.
Corpus
No Groovy corpus (.gradle files sit outside every test glob), so zero
snapshot movement.
Tests
Per §11 a fixture per spelling, each against an identifier control, in both
walker paths — the && chain and the if predicate. The own_production_bool_constructs module added in #1449 is the natural home.
All five kinds join groovy_bool_terminal_kinds!(). Re-measured against
a release binary rebuilt from the branch tip, not the issue's numbers:
Groovy source
abc.conditions before
after
cyclomatic
if (a) (control)
1
1
2
if (l[0])
0
1
2
if (l?[0])
0
1
2
if (a?.b)
0
1
3
if (a??.b)
0
1
3
if (a.@b)
0
1
2
Each also measured in the && chain slot, where every row goes 1 → 2
against the control's 2. a??.b was not in the issue's table and has
the same defect.
Verification
Aliases (§1) — none. subscript_expression (248), safe_subscript_expression (249), safe_navigation_expression (235), safe_chain_dot_expression (236) and direct_field_access_expression
(239) each own exactly one variant in language_groovy.rs; no
numeric-suffix sibling maps to any of those names.
Hidden rules (§2) — none begins with _, and all five were
observed in a bca dump of real source rather than read off node-types.json.
Double count (§5) — ABC's groovy_count_token_condition lists no
navigation operator. ?. / ??. are cyclomatic decisions only, and ?[ is its own token (QMARKLBRACK, id 84 — dumped, not assumed),
so the ternary-gated QMARK arm cannot see it. The post-fix
measurements above are 1, not 2.
Second defect fixed in the same pass
groovy_count_condition routed everyunary_expression to groovy_inspect_container, which peeled only the ! spelling — so the
arm claimed ~a / -a / +a and the peel dropped them (§7). This is
the identical divergence #1459 fixed in Kotlin. The slot now asks groovy_wrapper_operand(condition).is_some(), so the two cannot drift.
The peel also reads the unary operand by grammar field (operand)
rather than child(1), which fixes an observable case: if (! /*c*/ a)
scored 0 and now scores 1. parenthesized_expression names nothing in node-types.json, so it keeps the positional read and if ( /*c*/ a)
still scores 0 — measured, pinned by a test with a delete-me-when-fixed
message, and the same gap Kotlin records.
The _expression audit (39 alternatives, not 38)
Walked in full. Beyond the five fixed, nothing else is a live gap:
Deliberately via the token arm: identity_expression, regex_find_expression, regex_match_expression, elvis_expression, binary_expression, ternary_expression.
Scored through their own nested condition: switch_expression.
Remaining, all judged out: list_literal, map_literal, closure, object_creation_expression, range_expression, power_expression, update_expression, unary_update_expression, method_pointer_expression, method_reference_expression, string_literal, null_literal, spaceship_expression, assignment_expression, spread_dot_expression. Each is either a
constant-truth value in a predicate slot (closure, new Foo(), a.&b, a::b are always truthy; null always falsy) or degenerate
there, and none has a sibling-language precedent.
Further gaps recorded, not fixed:
spread_dot_expression (a*.b) is the closest call of the fifteen —
it is a navigation form like the five fixed, but it yields a list and
its Groovy-truth value is emptiness, not the navigation itself. No
sibling language counts a spread. Recorded here rather than added
blind.
Not an ABC gap, but found while checking §5: ?[ is not a Groovy
cyclomatic decision while ?. and ??. are. l?[0] short-circuits
exactly as a?.b does — impl_cyclomatic_java_like! lists QMARKDOT and QMARKQMARKDOT but not QMARKLBRACK. That is a
cyclomatic under-count on its own terms.
Tests
In src/metrics/abc.rs, module own_production_bool_constructs:
Groovy row gains l[0], l?[0], a.@b; expected_constructs 7 → 10.
These run through both slots and both existing tests.
groovy_safe_navigation_closes_the_two_below_gap — a?.b / a??.b
cannot ride that row, whose contract is the control's cyclomatic, so
they get their own test asserting control conditions against control
cyclomatic + 1. Asserting the offset makes it a §5 double-count
guard too.
Every one verified by perturbation. Removing each of the five kinds
fails the test naming that construct; reverting the field read to child(1) fails the comment test; widening the peel to accept ~ / -
/ + fails the arithmetic test. The slot-restates-the-list perturbation
is not observable — confirmed by running it — which is recorded in
that test's doc comment rather than claimed otherwise.
Corpus
Confirmed, not assumed: zero .snap.new under tests/repositories/big-code-analysis-output/ after the full suite, and
the submodule is clean. No corpus carries a Groovy file.
make pre-commit: BCA_GATE: pass (gate=pre-commit).
Summary
Groovy's indexing and navigation expressions score zero ABC conditions in
a boolean slot where a bare identifier scores one. C# and Kotlin both count
their analogues, so this is a per-language asymmetry rather than a policy
question.
Found by the whole-branch review of PR #1459.
Measured
Against
fix/batch-2026-09-13, release binary rebuilt from HEAD:abc.conditionscyclomaticdef ctl(a) { if (a) { return 1 }; return 0 }def sub(l) { if (l[0]) { … } }def ssub(l) { if (l?[0]) { … } }def safenav(a) { if (a?.b) { … } }def dfa(a) { if (a.@b) { … } }Siblings classify the analogue:
if (l[0])→ 1, viaCsharp::ElementAccessExpressionincsharp_bool_terminal_kinds!()if (l[0])→ 1 viaIndexExpression;if (o?.b)→ 1 viaNavigationExpression, which covers.and?.in one kinda?.bis the worst case: Groovy cyclomatic counts?.as a decision(
src/metrics/cyclomatic/groovy.rs:32), so ABC sits two below its owndecision count on an entirely idiomatic predicate.
Where
groovy_bool_terminal_kinds!(big-code-analysis-ast/src/macros/kind_sets.rs).field_accessis listed; these five are not, and all are alternatives of_expressionin dekobon-tree-sitter-groovy 0.2.2:subscript_expression(248)safe_subscript_expressionsafe_navigation_expression(235)safe_chain_dot_expressiondirect_field_access_expressionWhy it survived the #1449 sweep
That sweep's stated completeness evidence was a
grammar.jsonsweep — butscoped to operator tokens, which structurally cannot see a missing
node kind. And the macro's own comment asserted the analogue did not
exist ("the dekobon Groovy grammar has no
awaitorarray_accessanalogues"), which is false and is the sort of claim that stops the next
reader looking. That sentence is corrected on the PR #1459 branch.
The right audit table is the
_expressionalternative list: 38 alternativesagainst the set's 10.
Corpus
No Groovy corpus (
.gradlefiles sit outside every test glob), so zerosnapshot movement.
Tests
Per §11 a fixture per spelling, each against an identifier control, in both
walker paths — the
&&chain and theifpredicate. Theown_production_bool_constructsmodule added in #1449 is the natural home.Related
===and!==score no condition #1449 — the sweep that should have caught it.method could not see the gap it was looking for.
Resolution
Fixed on
fix/issue-1466(commit41e107fc).All five kinds join
groovy_bool_terminal_kinds!(). Re-measured againsta release binary rebuilt from the branch tip, not the issue's numbers:
abc.conditionsbeforecyclomaticif (a)(control)if (l[0])if (l?[0])if (a?.b)if (a??.b)if (a.@b)Each also measured in the
&&chain slot, where every row goes 1 → 2against the control's 2.
a??.bwas not in the issue's table and hasthe same defect.
Verification
subscript_expression(248),safe_subscript_expression(249),safe_navigation_expression(235),safe_chain_dot_expression(236) anddirect_field_access_expression(239) each own exactly one variant in
language_groovy.rs; nonumeric-suffix sibling maps to any of those names.
_, and all five wereobserved in a
bca dumpof real source rather than read offnode-types.json.groovy_count_token_conditionlists nonavigation operator.
?./??.are cyclomatic decisions only, and?[is its own token (QMARKLBRACK, id 84 — dumped, not assumed),so the ternary-gated
QMARKarm cannot see it. The post-fixmeasurements above are 1, not 2.
Second defect fixed in the same pass
groovy_count_conditionrouted everyunary_expressiontogroovy_inspect_container, which peeled only the!spelling — so thearm claimed
~a/-a/+aand the peel dropped them (§7). This isthe identical divergence #1459 fixed in Kotlin. The slot now asks
groovy_wrapper_operand(condition).is_some(), so the two cannot drift.The peel also reads the unary operand by grammar field (
operand)rather than
child(1), which fixes an observable case:if (! /*c*/ a)scored 0 and now scores 1.
parenthesized_expressionnames nothing innode-types.json, so it keeps the positional read andif ( /*c*/ a)still scores 0 — measured, pinned by a test with a delete-me-when-fixed
message, and the same gap Kotlin records.
The
_expressionaudit (39 alternatives, not 38)Walked in full. Beyond the five fixed, nothing else is a live gap:
identity_expression,regex_find_expression,regex_match_expression,elvis_expression,binary_expression,ternary_expression.switch_expression.parenthesized_expression,unary_expression.identifier,number_literal,boolean_literal,field_access,method_invocation,cast_expression,parenthesized_type_cast,instanceof_expression,membership_expression.Remaining, all judged out:
list_literal,map_literal,closure,object_creation_expression,range_expression,power_expression,update_expression,unary_update_expression,method_pointer_expression,method_reference_expression,string_literal,null_literal,spaceship_expression,assignment_expression,spread_dot_expression. Each is either aconstant-truth value in a predicate slot (
closure,new Foo(),a.&b,a::bare always truthy;nullalways falsy) or degeneratethere, and none has a sibling-language precedent.
Further gaps recorded, not fixed:
spread_dot_expression(a*.b) is the closest call of the fifteen —it is a navigation form like the five fixed, but it yields a list and
its Groovy-truth value is emptiness, not the navigation itself. No
sibling language counts a spread. Recorded here rather than added
blind.
string_literal— Groovy truth makes a non-empty string truthy,the same argument that put
number_literalin the set in fix(abc): numeric operands score no condition in PHP, Groovy and the C family #1410.if ("x")is valid Groovy. Not added because unlikea && 1thereis no measured asymmetry driving it; worth its own issue if anyone
wants consistency with fix(abc): numeric operands score no condition in PHP, Groovy and the C family #1410's reasoning.
?[is not a Groovycyclomatic decision while
?.and??.are.l?[0]short-circuitsexactly as
a?.bdoes —impl_cyclomatic_java_like!listsQMARKDOTandQMARKQMARKDOTbut notQMARKLBRACK. That is acyclomatic under-count on its own terms.
Tests
In
src/metrics/abc.rs, moduleown_production_bool_constructs:l[0],l?[0],a.@b;expected_constructs7 → 10.These run through both slots and both existing tests.
groovy_safe_navigation_closes_the_two_below_gap—a?.b/a??.bcannot ride that row, whose contract is the control's cyclomatic, so
they get their own test asserting control conditions against control
cyclomatic + 1. Asserting the offset makes it a §5 double-count
guard too.
groovy_negation_operand_survives_an_interposed_commentgroovy_arithmetic_unary_is_not_a_conditionEvery one verified by perturbation. Removing each of the five kinds
fails the test naming that construct; reverting the field read to
child(1)fails the comment test; widening the peel to accept~/-/
+fails the arithmetic test. The slot-restates-the-list perturbationis not observable — confirmed by running it — which is recorded in
that test's doc comment rather than claimed otherwise.
Corpus
Confirmed, not assumed: zero
.snap.newundertests/repositories/big-code-analysis-output/after the full suite, andthe submodule is clean. No corpus carries a Groovy file.
make pre-commit:BCA_GATE: pass (gate=pre-commit).