[
https://issues.apache.org/jira/browse/GROOVY-12441?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18130788#comment-18130788
]
Paul King commented on GROOVY-12441:
------------------------------------
AI read:
Thanks for the report. The cause turns out to be slightly different from the
conflicting-type theory, so here is what we found and what we'd recommend.
h3. Cause
{{VariableExpression.THIS_EXPRESSION}} is a shared instance intended for
compiler-internal use (see the comment on the field). The type checker
deliberately records no inferred type for it (it skips the shared instance in
{{StaticTypeCheckingVisitor.storeTypeForThis}}). Under static compilation, the
bytecode generator then falls back to the type of the class currently being
generated. That is correct in a method body, but inside a closure or lambda,
that class is the generated closure/lambda class rather than the enclosing
class. So a {{this}} receiver built from the shared instance gets a spurious
cast to the closure class.
Sharing the node between the closure and the method body is not what triggers
it: a single use inside a closure is enough.
h3. Where the shared instance works
Dynamic and {{@TypeChecked}} code: everywhere we tried (method, constructor,
closure, nested closure, lambda, inner and anonymous classes, static methods,
scripts).
{{@CompileStatic}}:
||Use of the shared instance||Method/constructor, inner/anonymous class, static
method||Inside a closure or lambda||
|method-call receiver {{this.foo()}}|works|{{ClassCastException}} (cast to the
closure/lambda class)|
|property receiver {{this.p}}|works|{{ClassCastException}}|
|method reference receiver {{this::foo}}|works|compile error: _Failed to find
method 'foo()' for the type: A$_m_closure1_|
|method pointer {{this.&foo}}|works|works|
|field access {{this.@f}}|works|works|
|argument {{bar(this)}}, or assigned to a local|works|works|
The same applies to {{MethodPointerExpression}}/{{MethodReferenceExpression}}
built with a {{null}} receiver, since they substitute the shared instance.
h3. Recommendation
For AST transforms, create a fresh node each time, e.g.
{{GeneralUtils.varX("this")}} (which is what your workaround does). A transform
that injects {{this}} into user code generally can't know whether that code
ends up inside a closure or lambda, or is statically compiled, so this is the
only choice that is safe everywhere. A fresh node also:
* can carry its own source position. The shared instance has none, so any error
reported against it has no line/column information.
* keeps its own node metadata. The shared instance is a mutable static, so
anything a transform, type-checking extension or compiler phase stores on it
(metadata, type, source position) leaks to every other use, across compilations
in the same JVM and across threads.
h3. Possible changes on our side
* Document {{THIS_EXPRESSION}}/{{SUPER_EXPRESSION}} more clearly as internal,
pointing transform authors to {{varX("this")}}.
* Make static compilation resolve an untyped {{this}} inside a closure or
lambda to the enclosing class rather than the generated class. This would make
the failing cells above work. It wouldn't address the position/metadata
concerns, so the recommendation above stands either way.
While investigating, we also found that a normal {{this::foo}} in a static
method failed under {{@CompileStatic}}. That is unrelated to the shared
instance and is tracked as GROOVY-12481.
> STC gets confused when THIS_EXPRESSION is used
> ----------------------------------------------
>
> Key: GROOVY-12441
> URL: https://issues.apache.org/jira/browse/GROOVY-12441
> Project: Groovy
> Issue Type: Bug
> Affects Versions: 2.5.23, 3.0.25, 4.0.33, 5.1.3, 6.0.0
> Reporter: Björn Kautler
> Priority: Major
>
> If you use {{VariableExpression.THIS_EXPRESSION}} the STC and thus
> static-compile gets confused.
> I assume this is a bug, if it is expected, sorry for the noise.
> We use this at some places in the Spock transform.
> While trying to make Spock specs compatible with STC and SC, it was observed
> that STC stores type information at the current node and using
> {{VariableExpression.THIS_EXPRESSION}} then causes conflicting casts when
> used inside and outside a closure.
> As a work-around we can instead use {{{}new VariableExpression("this"){}}},
> but I guess this actually is a bug, thus this report.
> As an example.
> {code:java}
> class ASpec extends Specification {
> def "mock init closure"() {
> given:
> List<String> list = Mock(List) {
> size() >> 3
> }
> when:
> int size = list.size()
> then:
> size == 3
> }
> }
> {code}
> compiles in semantic analysis to
> {code:java}
> import spock.lang.*
> @org.spockframework.runtime.model.SpecMetadata(filename = 'script.groovy',
> line = 1)
> public class ASpec extends spock.lang.Specification {
> @org.spockframework.runtime.model.FeatureMetadata(name = 'mock init
> closure', ordinal = 0, line = 2, blocks =
> [@org.spockframework.runtime.model.BlockMetadata(kind =
> org.spockframework.runtime.model.BlockKind.SETUP, texts = []),
> @org.spockframework.runtime.model.BlockMetadata(kind =
> org.spockframework.runtime.model.BlockKind.WHEN, texts = []),
> @org.spockframework.runtime.model.BlockMetadata(kind =
> org.spockframework.runtime.model.BlockKind.THEN, texts = [])], parameterNames
> = [])
> public void $spock_feature_0_0() {
> org.spockframework.runtime.ValueRecorder $spock_valueRecorder = new
> org.spockframework.runtime.ValueRecorder()
> org.spockframework.runtime.ErrorCollector $spock_errorCollector =
> org.spockframework.runtime.ErrorRethrower.INSTANCE
> org.spockframework.runtime.SpockRuntime.callBlockEntered(this, 0)
> java.util.List<String> list =
> org.spockframework.runtime.SpecInternals.MockImpl(this, 'list',
> java.util.List, java.util.List, { ->
>
> this.getSpecificationContext().getMockController().addInteraction(new
> org.spockframework.mock.runtime.InteractionBuilder(5, 7, 'size() >>
> 3').addEqualTarget(it).addEqualMethodName('size').setArgListKind(true,
> false).addConstantResponse(3).build())
> })
> org.spockframework.runtime.SpockRuntime.callBlockExited(this, 0)
> org.spockframework.runtime.SpockRuntime.callBlockEntered(this, 1)
> java.lang.Integer size = list.size()
> org.spockframework.runtime.SpockRuntime.callBlockExited(this, 1)
> org.spockframework.runtime.SpockRuntime.callBlockEntered(this, 2)
> try {
>
> org.spockframework.runtime.SpockRuntime.verifyCondition($spock_errorCollector,
> $spock_valueRecorder.reset(), 'size == 3', 12, 5, null,
> $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(2),
> $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0),
> size) ==
> $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), 3)))
> }
> catch (java.lang.Throwable $spock_condition_throwable) {
>
> org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector,
> $spock_valueRecorder, 'size == 3', 12, 5, null, $spock_condition_throwable)}
> finally {
> }
> org.spockframework.runtime.SpockRuntime.callBlockExited(this, 2)
> this.getSpecificationContext().getMockController().leaveScope()
> }
> }
> {code}
> The important part here is the two {{getSpecificationContext()}} calls, one
> inside the mock init closure, one as last line in the feature method, both
> added by the Spock transform.
> If those use {{VariableExpression.THIS_EXPRESSION}} then at runtime you get
> {{{}java.lang.ClassCastException: apackage.ASpec cannot be cast to
> apackage.ASpec$__spock_feature_0_0_closure1{}}}.
> If you use {{new VariableExpression("this")}} instead, it works as intended.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)