diff --git a/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java b/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java index d0d9d21cde2..5595abd2f20 100644 --- a/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java +++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java @@ -220,7 +220,7 @@ private Expression transformBinaryExpression(final BinaryExpression exp, final C private Expression transformFieldReference(final Expression exp, final FieldNode fn, final boolean isStatic) { Expression receiver = createFieldHelperReceiver(); if (isStatic) { - receiver = asClass(receiver); + receiver = asClass(receiver, fn); // GROOVY-11907 } MethodCallExpression mce = callX(receiver, Traits.helperGetterName(fn)); @@ -352,7 +352,17 @@ private Expression createFieldHelperReceiver() { } private Expression asClass(final Expression e) { + return asClass(e, null); + } + + private Expression asClass(final Expression e, final FieldNode fn) { ClassNode rawClass = ClassHelper.CLASS_Type.getPlainNodeReference(); - return ternaryX(isInstanceOfX(e, rawClass), e, callX(e, "getClass")); + MethodCallExpression getClassCall = callX(e, "getClass"); + if (fn != null && fn.isStatic()) { + // GROOVY-11907: mark sub-expression for dynamic dispatch so that + // GROOVY-11817's per-expression DYNAMIC_RESOLUTION check handles it + getClassCall.putNodeMetaData(org.codehaus.groovy.transform.stc.StaticTypesMarker.DYNAMIC_RESOLUTION, rawClass); + } + return ternaryX(isInstanceOfX(e, rawClass), e, getClassCall); } } diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy index 259db64938a..d364c28c853 100644 --- a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitASTTransformationTest.groovy @@ -4053,4 +4053,45 @@ final class TraitASTTransformationTest { new C().test() ''' } + + // GROOVY-11907 + @Test + void testTraitStaticFieldHelperGetClassMarkedDynamic() { + // When a global AST transform alters class processing order (e.g. Spock's SpockTransform), + // TraitTypeCheckingExtension.handleMissingMethod is invoked for the static field helper. + // It calls makeDynamic() on the outer MCE, but the inner getClass() sub-expression was + // not marked, producing incompatible stack frame types. Verify the fix: asClass(e, fn) + // must mark the getClass() call with DYNAMIC_RESOLUTION for static fields. + def cu = new org.codehaus.groovy.control.CompilationUnit() + cu.addSource('MyTrait.groovy', ''' + @groovy.transform.CompileStatic + trait MyTrait { + static String myField + } + ''') + cu.compile(org.codehaus.groovy.control.Phases.CANONICALIZATION) + + // Find the helper class that accesses the static field + def allClasses = cu.AST.modules.collectMany { it.classes } + def traitHelper = allClasses.find { it.name.contains('Helper') } + assert traitHelper != null, "Trait helper class not found in: ${allClasses*.name}" + + // Walk the AST to find getClass() calls in the helper methods + def getClassCalls = [] + def visitor = new org.codehaus.groovy.ast.CodeVisitorSupport() { + void visitMethodCallExpression(org.codehaus.groovy.ast.expr.MethodCallExpression mce) { + if (mce.methodAsString == 'getClass') { + getClassCalls << mce + } + super.visitMethodCallExpression(mce) + } + } + traitHelper.methods.each { it.code?.visit(visitor) } + + assert !getClassCalls.isEmpty(), 'Expected getClass() call in trait helper for static field' + getClassCalls.each { mce -> + assert mce.getNodeMetaData(org.codehaus.groovy.transform.stc.StaticTypesMarker.DYNAMIC_RESOLUTION) != null, + 'getClass() sub-expression must be marked with DYNAMIC_RESOLUTION for static trait fields' + } + } }