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

Paul King edited comment on GROOVY-12313 at 10/21/26 9:17 PM:
--------------------------------------------------------------

h3. AI assessment

Classification: confirmed bug, triggered by non-hygienic AST from a third-party 
transform. Groovy has no rule against sharing expression nodes, but per-node 
metadata makes it fragile everywhere, not just here. Best immediate remedy is 
in Spock: create a fresh VariableExpression for each use. That's a few lines, 
fixes all released Groovy versions, and I'd suggest the reporter raise it with 
Spock.

h3. More details

The trigger (Spock side)

Spock's AstUtil.createGetAtWithMapSupportMethodCall (spock-core 2.5-groovy-6.0) 
passes the same expression object to all three positions:

{code}
new TernaryExpression(
    new BooleanExpression(new BinaryExpression(expression, instanceof, 
classX(MAP_TYPE))),
    createGetAtMethodCall(expression, key),
    createGetAtMethodCall(expression, index));
{code}

WhereBlockRewriter.rewriteMultiParameterization goes further and reuses that 
one rightBase node for every data variable (a, b, …).

The mechanism (Groovy side)

1. In the true branch, StaticTypeCheckingVisitor.java:821–823 finds the 
temporary instanceof type and writes INFERRED_TYPE = Map onto the receiver node.
2. In the else branch, narrowing gives Object. That store is skipped when the 
type is Object, and nothing clears the old value.
3. getType() (StaticTypeCheckingVisitor.java:6594) reads INFERRED_TYPE metadata 
before anything else. So the else-branch receiver comes out as Map: STC picks 
DGM.getAt(Map, Object) and SC emits castToType(p, Map). That cast is the 
GroovyCastException in the report.

h3. Suggestion for this ticket

It goes on to suggest some hardening on the Groovy side which I will look at.


was (Author: paulk):
AI assessment:

bq. Classification: confirmed bug, triggered by non-hygienic AST from a 
third-party transform. Groovy has no rule against sharing expression nodes, but 
per-node metadata makes it fragile everywhere, not just here. Best immediate 
remedy is in Spock: create a fresh VariableExpression for each use. That's a 
few lines, fixes all released Groovy versions, and I'd suggest the reporter 
raise it with Spock.

More details:

The trigger (Spock side)

Spock's AstUtil.createGetAtWithMapSupportMethodCall (spock-core 2.5-groovy-6.0) 
passes the same expression object to all three positions:

{code}
new TernaryExpression(
    new BooleanExpression(new BinaryExpression(expression, instanceof, 
classX(MAP_TYPE))),
    createGetAtMethodCall(expression, key),
    createGetAtMethodCall(expression, index));
{code}

WhereBlockRewriter.rewriteMultiParameterization goes further and reuses that 
one rightBase node for every data variable (a, b, …).

It goes on to suggest some hardening on the Groovy side which I will look at.

> static-compilation leaks instanceof inferred type into else-branch
> ------------------------------------------------------------------
>
>                 Key: GROOVY-12313
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12313
>             Project: Groovy
>          Issue Type: Bug
>    Affects Versions: 5.0.3, 4.0.31
>            Reporter: Björn Kautler
>            Priority: Major
>
> With this in groovyconsole.dev
> {code:java}
> @groovy.transform.CompileStatic
> class Foo extends Specification {
>    def foo() {
>       expect:
>          true
>       where:
>          [a, b] << [['a', 'b']]
>    }
> }
> {code}
> you get
> {code:java}
> Cannot cast object '[a, b]' with class 'java.util.ArrayList' to class 
> 'java.util.Map' due to: groovy.lang.GroovyRuntimeException: Could not find 
> matching constructor for: java.util.Map(String, String){code}
> The AST transformed code after class generation phase for this is
> {code:java}
>     @org.spockframework.runtime.model.DataProcessorMetadata(dataVariables = 
> ['a', 'b'])
>     public java.lang.Object $spock_feature_0_0proc(java.lang.Object 
> $spock_p0) {
>         java.lang.Object a = ((java.lang.Object) $spock_p0 instanceof 
> java.util.Map ? $spock_p0.getAt('a') : $spock_p0.getAt(0))
>         java.lang.Object b = ((java.lang.Object) $spock_p0 instanceof 
> java.util.Map ? $spock_p0.getAt('b') : $spock_p0.getAt(1))
>         return new java.lang.Object[]{ a , b }
>     } {code}
> where {{$spock_p0}} is given as {{{}['a', 'b']{}}}.
> Without static compilation this works fine and runs the expected iterations.
> With static compilation and indy switched off, the generated and then 
> decompiled bytecode is this:
> {code:java}
> @DataProcessorMetadata(
>     dataVariables = {"a", "b"}
> )
> public Object $spock_feature_0_0proc(Object $spock_p0) {
>     Object a = $spock_p0 instanceof Map ? 
> DefaultGroovyMethods.getAt((Map)ScriptBytecodeAdapter.castToType($spock_p0, 
> Map.class), "a") : 
> DefaultGroovyMethods.getAt((Map)ScriptBytecodeAdapter.castToType($spock_p0, 
> Map.class), 0);
>     Object b = $spock_p0 instanceof Map ? 
> DefaultGroovyMethods.getAt((Map)ScriptBytecodeAdapter.castToType($spock_p0, 
> Map.class), "b") : 
> DefaultGroovyMethods.getAt((Map)ScriptBytecodeAdapter.castToType($spock_p0, 
> Map.class), 1);
>     return new Object[]{a, b};
> } {code}
> So with static compilation the inferred {{Map}} type somehow leaks into the 
> else-branch of the ternary expression and causes this class-cast exception.



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

Reply via email to