[ 
https://issues.apache.org/jira/browse/GROOVY-10355?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18106475#comment-18106475
 ] 

Paul King edited comment on GROOVY-10355 at 8/21/26 4:35 AM:
-------------------------------------------------------------

Thanks for the careful review [~sunlan] - you were right on essentially 
everything, and the {{in}} grouping catch was an important one. All points are 
now addressed on the branch. Point by point:

*{{in}} grouping.* Confirmed, and worse than a missing feature: on master those 
forms don't compile, so the sketch was turning a compile error into a silently 
inverted answer ({{\(x) in list && true}} evaluated to {{false}}, and the 
{{if}} form took the wrong branch; a chained {{\(x) in a in b}} inverted the 
same way). Your {{return}} observation is also right - {{return}} parses a 
plain expression, so it grouped correctly all along. Fixed along the lines you 
suggested, generalized: the repair now splices the binary reading in at 
relational precedence, descending the argument's left spine through any 
operator binding no tighter than relational (equality, regex find/match, 
bitwise, logical, ternary/elvis, a further relational or coercion) and 
repairing at the leaf:

{code:groovy}
def x = 1, list = [1]
assert ((x) in list && true)             // (x in list) && true
assert ((x) in list == true)             // (x in list) == true
assert (x) in [1] in [true]              // left-associative, like the 
unparenthesized form
assert ((x) in list ? 'y' : 'n') == 'y'
assert ((x) in [] ?: 'none') == 'none'
{code}

Tighter-binding right-hand sides such as {{\(x) in 1..5}} correctly stay whole. 
To answer your scope question directly: the intent is the full relational 
meaning, not just a simple name on the right - it is now implemented and tested 
that way.

*{{as}}.* All three confirmed and fixed. {{List<String>}} arrives as a 
{{ClassExpression}} as you said, so the repair now uses {{right.getType()}} 
(generics preserved). {{String[]}} turns out to arrive as an empty subscript on 
the element type, so that shape is read as an array type (recursively, so 
{{String[][]}} works too). {{\(x) as Long ?: 0}} falls out of the same splice 
as {{(\(x) as Long) ?: 0}}. Anything that still can't be read as a type bails 
out to a compile error rather than guessing - and those bail-outs now carry an 
explanatory hint as well (see below).

*Source positions.* Confirmed and fixed - every node {{combineRebalancing}} 
(and the new splice) creates or rotates now gets {{configureAST}} spanning 
left-operand start to right-operand end, and an AST-walking test asserts no 
{{-1}} positions remain. While in there I also tightened the unary/cast wrapper 
spans ({{-(a)}} in {{-(a) - 3}} previously spanned the whole expression).

*{{CAST_RESOLVE_HINT}}.* Now annotated {{@Internal}}.

*Comments/wording.* You read the check correctly - the behaviour is the 
intended one (the convention encoded is "class names start with an uppercase 
letter", so {{_foo}} and {{$foo}} read as values), but the comments said 
otherwise. I renamed the method to {{hasValueConventionFinalSegment}}, fixed 
the comments, and added {{(_foo) + 1}} / {{($bar) - 1}} tests.

*Tests.* Your exact {{assert (\(x) in list && true)}} is in the suite now (I 
verified it failed before the fix), along with the grouping-equivalence family 
and the three {{as}} shapes as statements. One correction though: {{(\(x) as 
Long)}} does not work on master. I checked released 5.0.6, 5.1.0, 6.0.0-alpha-1 
and 6.0.0-beta-2, in both dynamic and {{@CompileStatic}} mode, and all fail 
with {{unable to resolve class x}}. The outer parentheses don't rescue it 
because {{expressionInPar}} accepts a command expression, so the inner {{\(x) 
as Long}} takes the same mis-parse path. That assertion is therefore genuine 
repair coverage and I've kept it. Agreed on {{(p)++}} - it passes on master 
unchanged and stays purely as a regression guard.

While verifying I found two further families that failed with a bare {{unable 
to resolve class x}} and no hint: shapes where the repair bails out (e.g. 
{{\(x) as 3}}), which now get a shape-specific hint explaining how the 
expression was read, and parenthesized right-hand sides like {{\(x) in 
(list)}}, which parse as a method call of the keyword identifier on the cast - 
now covered by adding {{MethodCallExpression}} to the ambiguous-operand set, 
and for those the {{(\(x))}} workaround the hint suggests genuinely works. 
({{\(x) in [1, 2] && true}} is fine, incidentally - the cast only captures the 
unary-level {{in [1, 2]}}, so the trailing operator lands on the repaired 
binary.)

The suite is now 18 compile-and-run tests asserting evaluated results, and the 
full antlr4 parser test group is green, including {{SyntaxErrorTest}}, which 
asserts error-message content and so exercises the hint changes.



was (Author: paulk):
Thanks for the careful review [~sunlan] - you were right on essentially 
everything, and the {{in}} grouping catch was an important one. All points are 
now addressed on the branch. Point by point:

*{{in}} grouping.* Confirmed, and worse than a missing feature: on master those 
forms don't compile, so the sketch was turning a compile error into a silently 
inverted answer ({{(x) in list && true}} evaluated to {{false}}, and the {{if}} 
form took the wrong branch; a chained {{(x) in a in b}} inverted the same way). 
Your {{return}} observation is also right - {{return}} parses a plain 
expression, so it grouped correctly all along. Fixed along the lines you 
suggested, generalized: the repair now splices the binary reading in at 
relational precedence, descending the argument's left spine through any 
operator binding no tighter than relational (equality, regex find/match, 
bitwise, logical, ternary/elvis, a further relational or coercion) and 
repairing at the leaf:

{code:groovy}
def x = 1, list = [1]
assert ((x) in list && true)             // (x in list) && true
assert ((x) in list == true)             // (x in list) == true
assert (x) in [1] in [true]              // left-associative, like the 
unparenthesized form
assert ((x) in list ? 'y' : 'n') == 'y'
assert ((x) in [] ?: 'none') == 'none'
{code}

Tighter-binding right-hand sides such as {{(x) in 1..5}} correctly stay whole. 
To answer your scope question directly: the intent is the full relational 
meaning, not just a simple name on the right - it is now implemented and tested 
that way.

*{{as}}.* All three confirmed and fixed. {{List<String>}} arrives as a 
{{ClassExpression}} as you said, so the repair now uses {{right.getType()}} 
(generics preserved). {{String[]}} turns out to arrive as an empty subscript on 
the element type, so that shape is read as an array type (recursively, so 
{{String[][]}} works too). {{(x) as Long ?: 0}} falls out of the same splice as 
{{((x) as Long) ?: 0}}. Anything that still can't be read as a type bails out 
to a compile error rather than guessing - and those bail-outs now carry an 
explanatory hint as well (see below).

*Source positions.* Confirmed and fixed - every node {{combineRebalancing}} 
(and the new splice) creates or rotates now gets {{configureAST}} spanning 
left-operand start to right-operand end, and an AST-walking test asserts no 
{{-1}} positions remain. While in there I also tightened the unary/cast wrapper 
spans ({{-(a)}} in {{-(a) - 3}} previously spanned the whole expression).

*{{CAST_RESOLVE_HINT}}.* Now annotated {{@Internal}}.

*Comments/wording.* You read the check correctly - the behaviour is the 
intended one (the convention encoded is "class names start with an uppercase 
letter", so {{_foo}} and {{$foo}} read as values), but the comments said 
otherwise. I renamed the method to {{hasValueConventionFinalSegment}}, fixed 
the comments, and added {{(_foo) + 1}} / {{($bar) - 1}} tests.

*Tests.* Your exact {{assert ((x) in list && true)}} is in the suite now (I 
verified it failed before the fix), along with the grouping-equivalence family 
and the three {{as}} shapes as statements. One correction though: {{((x) as 
Long)}} does not work on master. I checked released 5.0.6, 5.1.0, 6.0.0-alpha-1 
and 6.0.0-beta-2, in both dynamic and {{@CompileStatic}} mode, and all fail 
with {{unable to resolve class x}}. The outer parentheses don't rescue it 
because {{expressionInPar}} accepts a command expression, so the inner {{(x) as 
Long}} takes the same mis-parse path. That assertion is therefore genuine 
repair coverage and I've kept it. Agreed on {{(p)++}} - it passes on master 
unchanged and stays purely as a regression guard.

While verifying I found two further families that failed with a bare {{unable 
to resolve class x}} and no hint: shapes where the repair bails out (e.g. {{(x) 
as 3}}), which now get a shape-specific hint explaining how the expression was 
read, and parenthesized right-hand sides like {{(x) in (list)}}, which parse as 
a method call of the keyword identifier on the cast - now covered by adding 
{{MethodCallExpression}} to the ambiguous-operand set, and for those the 
{{((x))}} workaround the hint suggests genuinely works. ({{(x) in [1, 2] && 
true}} is fine, incidentally - the cast only captures the unary-level {{in [1, 
2]}}, so the trailing operator lands on the repaired binary.)

The suite is now 18 compile-and-run tests asserting evaluated results, and the 
full antlr4 parser test group is green, including {{SyntaxErrorTest}}, which 
asserts error-message content and so exercises the hint changes.


> Compiler interpret variable name  as class name when in parentheses.
> --------------------------------------------------------------------
>
>                 Key: GROOVY-10355
>                 URL: https://issues.apache.org/jira/browse/GROOVY-10355
>             Project: Groovy
>          Issue Type: Bug
>          Components: parser-antlr4
>    Affects Versions: 3.0.0, 4.0.0, 5.0.0
>         Environment: JDK 11.0.12
>            Reporter: Olof Asbrink
>            Priority: Major
>         Attachments: GROOVY-10355-Assessment.pdf, screenshot-1.png, 
> screenshot-2.png
>
>
> This behavior seems unexpected:
> {code:java}
> String b = "B"
> System.out.println("A" + (b) + "C")
> {code}
> Throws this exception:
> {code:java}
> org.codehaus.groovy.control.MultipleCompilationErrorsException: startup 
> failed:
> /tmp/repo1.gm: 2: unable to resolve class b
>  @ line 2, column 26.
>    System.out.println("A" + (b) + "C")
>                             ^{code}
> However these examples work:
> {code:java}
> String b = "B"
> System.out.println("A" + b + "C")
> {code}
> and
> {code:java}
> String b = "B"
> System.out.println("A" + (b))
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to