Skip to content

GROOVY-10355: restore binary reading of (name) +/- x and (name) in/as… - #2817

Merged
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy10355
Aug 21, 2026
Merged

GROOVY-10355: restore binary reading of (name) +/- x and (name) in/as…#2817
paulk-asert merged 1 commit into
apache:masterfrom
paulk-asert:groovy10355

Conversation

@paulk-asert

Copy link
Copy Markdown
Contributor

… x (design sketch)

A parenthesized bare name whose final segment starts lowercase is by convention a value, not a class, so a cast mis-parse of the ambiguous shapes is rebuilt in AstBuilder as the binary expression the syntax visually suggests, preserving textual left-to-right grouping across precedence levels. The binary-only keywords in/as captured as cast operand identifiers are restored to their relational reading for any capitalization. Unresolvable bare-name cast types now carry a hint explaining the ambiguity and the ((name)) workaround.

The grammar is unchanged: a predicate-gated castExprAlt is not viable because adaptive prediction under the me.sunlan antlr4 fork only consults semantic predicates when a decision conflict is registered, which this decision never produces, so the predicate would only fire as a parse-time FailedPredicateException.

@paulk-asert
paulk-asert force-pushed the groovy10355 branch 3 times, most recently from 5451ff0 to af2b7b6 Compare August 18, 2026 23:50
@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.64706% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.2571%. Comparing base (55dcfb9) to head (515d6f7).
⚠️ Report is 32 commits behind head on master.

Files with missing lines Patch % Lines
...va/org/apache/groovy/parser/antlr4/AstBuilder.java 87.5000% 9 Missing and 12 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2817        +/-   ##
==================================================
+ Coverage     70.1846%   70.2571%   +0.0726%     
- Complexity      35846      36248       +402     
==================================================
  Files            1562       1569         +7     
  Lines          132542     133706      +1164     
  Branches        24379      24632       +253     
==================================================
+ Hits            93024      93938       +914     
- Misses          31109      31255       +146     
- Partials         8409       8513       +104     
Files with missing lines Coverage Δ
...va/org/codehaus/groovy/control/ResolveVisitor.java 90.9425% <100.0000%> (+0.6596%) ⬆️
...va/org/apache/groovy/parser/antlr4/AstBuilder.java 86.4078% <87.5000%> (-1.1136%) ⬇️

... and 65 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java Outdated
@paulk-asert
paulk-asert force-pushed the groovy10355 branch 2 times, most recently from 96b0192 to cd6fa5e Compare August 19, 2026 07:16
@daniellansun

Copy link
Copy Markdown
Contributor

A couple of things I wasn’t sure about — I may well have missed the intended scope.

in grouping. repairBinaryKeywordCast uses the whole command argument as the RHS. With x = 1, list = [1]:

  • x in list && true is (x in list) && truetrue
  • def r = (x) in list && true becomes x in (list && true)false
  • if ((x) in list && true) takes the same path

On master the parenthesized command forms don’t compile. After the rewrite they compile, but with the inverted meaning. return (x) in list && true is already grouped the other way — return parses a normal expression, not a command.

Would it be reasonable to peel && / == / a second in off the RHS, the way combineRebalancing does for +/-? If the sketch is only meant to cover a simple name on the right, it might help to say so — the commit currently reads as the full relational meaning.

as. (x) as Long works; (x) as List<String>, (x) as String[], and (x) as Long ?: 0 still fail to resolve x. For List<String> the argument is already a ClassExpressionright.getType() might be enough. The other two look harder (expression vs coercionType).

Source positions. Inner nodes from combineRebalancing show up as line=-1 (e.g. the inner + in "A" + (b) + "C"). A configureAST on each new BinaryExpression would probably sort that out.

CAST_RESOLVE_HINT. It’s a new public constant; the parser only needs the string. @Internal (or a shared internal key) might keep it off the public surface.

Comments / tests. The text says “lowercase-initial”; the check is !isUpperCase (_foo is treated as a value). And the in grouping above isn’t in the suite yet — something like assert ((x) in list && true) would have caught it. ((x) as Long) already works on master because of the extra parens; (x) as List<String> as a statement would cover the as gap. (p)++ isn’t a cast, so it passes without the rewrite.

… x (design sketch)

A parenthesized bare name whose final segment starts lowercase is by
convention a value, not a class, so a cast mis-parse of the ambiguous
shapes is rebuilt in AstBuilder as the binary expression the syntax
visually suggests, preserving textual left-to-right grouping across
precedence levels. The binary-only keywords in/as captured as cast
operand identifiers are restored to their relational reading for any
capitalization. Unresolvable bare-name cast types now carry a hint
explaining the ambiguity and the ((name)) workaround.

The grammar is unchanged: a predicate-gated castExprAlt is not viable
because adaptive prediction under the me.sunlan antlr4 fork only
consults semantic predicates when a decision conflict is registered,
which this decision never produces, so the predicate would only fire
as a parse-time FailedPredicateException.
@testlens-app

testlens-app Bot commented Aug 21, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 515d6f7
▶️ Tests: 111642 executed
⚪️ Checks: 31/31 completed


Learn more about TestLens at testlens.app/docs.

@paulk-asert

Copy link
Copy Markdown
Contributor Author

@daniellansun Response to your questions in the Jira ticket.

* The value is the complete hint text; this visitor attaches no meaning to it.
*/
@Internal
public static final String CAST_RESOLVE_HINT = "_CAST_RESOLVE_HINT";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks better that way, but I am still not happy about this being such a specialized mechanism for just one thing, but spanning so much across packages and compilation phases. If we made this a more general mechanism and for example call it "_RESOLVE_HINT", then it would look better, but is it better? If it stays a single use case, then probably not. So I guess in combination with @internal this is ok. We can still change it later if we really want to.

@paulk-asert
paulk-asert merged commit f6bc5a5 into apache:master Aug 21, 2026
32 checks passed
@paulk-asert
paulk-asert deleted the groovy10355 branch August 21, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants