Skip to content

fix(abc/groovy): indexing and safe navigation score no condition #1466

Description

@dekobon

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:

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:

  • subscript_expression (248)
  • safe_subscript_expression
  • safe_navigation_expression (235)
  • safe_chain_dot_expression
  • direct_field_access_expression

Why it survived the #1449 sweep

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.

Related

Resolution

Fixed on fix/issue-1466 (commit 41e107fc).

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 every unary_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.
  • Peeled: parenthesized_expression, unary_expression.
  • Already listed: 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 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:

  1. 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.
  2. string_literal — Groovy truth makes a non-empty string truthy,
    the same argument that put number_literal in 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 unlike a && 1 there
    is 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.
  3. 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_gapa?.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.
  • groovy_negation_operand_survives_an_interposed_comment
  • groovy_arithmetic_unary_is_not_a_condition

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions