[
https://issues.apache.org/jira/browse/GROOVY-10932?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18133869#comment-18133869
]
Paul King commented on GROOVY-10932:
------------------------------------
>From a slack conversation with [~jdaugherty].
Ok, looking into trait issues, after my fix to gradle, I found this:
Subject: A trait's implementer compiles to different bytecode depending on
whether the trait is precompiled or in the same compilation unit
Tested with Groovy 4.0.33 and 5.1.3, JDK 25.
Reproducer. Three files, no dependencies:
// Flag.groovy
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Flag { boolean value() default true; String note() default '' }
// Named.groovy
trait Named {
@Flag
String describe() { "named" }
}
// Thing.groovy
class Thing implements Named {}Compile Thing twice: once together with the
trait, once against the precompiled trait.
groovyc -d together Flag.groovy Named.groovy Thing.groovy
groovyc -d trait-first Flag.groovy Named.groovy
groovyc -d impl-second -cp trait-first Thing.groovy
together/Thing.class and impl-second/Thing.class differ. javap -v shows two
differences and nothing else:
- Flag (trait in the same unit)
+ Flag(note="", value=true) (trait precompiled)
-InnerClasses: (trait in the same unit
only)
- public static abstract Helper=class Named$Trait$Helper of class Named
Cause. When the trait is in the compilation unit, TraitComposer copies the
trait's methods from its AST, so annotations keep the members written in the
source, and the helper is an InnerClassNode of the trait, which puts an
InnerClasses entry into the implementer. When the trait is precompiled,
Traits.findHelpers loads the helper through the class loader and wraps it with
ClassHelper.make(Class), so its methods come from Java8.configureClassNode,
which builds AnnotationNodes from reflected java.lang.annotation.Annotation
instances. Reflection reports every member, defaults included, so each
annotation is written out in full, and the helper is a plain ClassNode, so no
InnerClasses entry is recorded.
Why it matters. The two class files are semantically equivalent, but the output
of compiling an unchanged Thing.groovy now depends on what else was in the
compilation unit. Incremental build tools recompile a subset of sources against
the previous output, so a trait is in the unit on a full build and precompiled
on an incremental one, and classes whose source never changed come out
different. Gradle, for instance, compares classpath classes by content and by
ABI, and annotation values are part of the ABI, so every downstream module that
uses those classes recompiles. In a Grails application, where most domain
classes implement project traits, a body-only change in one utility class ends
up recompiling almost everything downstream. Groovy's own @EqualsAndHashCode or
@ToString would not show this since they have source retention, but any runtime
or class-retained annotation on a trait method does, and @DelegatesTo on a
trait method parameter shows it the same way.
Suggested fix. Resolve precompiled trait helpers the way every other classpath
class is resolved, from bytecode through the compilation unit's
ClassNodeResolver, instead of loading them into the JVM and reflecting. The
decompiled ClassNode carries annotations exactly as the class file has them, so
the two paths would agree, and the InnerClasses entry could then be made
consistent in either direction. This would also remove the only place where the
compiler needs to load a class of the previous output, which is what breaks
incremental compilation with NoClassDefFoundError when a class the helper
refers to has been removed for recompilation, the subject of GROOVY-10932. A
smaller alternative is to drop members equal to their default in
Java8.setAnnotationMetaData, using Method.getDefaultValue(), which fixes the
annotation half only.
I can provide the class files from the reproducer, or a Gradle project showing
the downstream recompilation, if useful. (edited)
> Groovy incremental compilation failure with traits
> --------------------------------------------------
>
> Key: GROOVY-10932
> URL: https://issues.apache.org/jira/browse/GROOVY-10932
> Project: Groovy
> Issue Type: Bug
> Components: Compiler
> Affects Versions: 3.0.15
> Reporter: Lóránt Pintér
> Priority: Major
>
> See reproducer here:
> [https://github.com/asodja/groovy-traits-compilation-reproducer]
> This reproduces issue with Groovy traits and Gradle incremental compilation.
> Steps to reproduce:
> 1. git clone [email protected]:asodja/groovy-traits-compilation-reproducer.git
> 2. run `./gradlew :lib:compileGroovy --no-build-cache`
> 3. Change `println("a")` in MyTest.groovy to `println("b")`
> 4. run `./gradlew :lib:compileGroovy --no-build-cache` again
> 5. See error
> To resolve the issue run `./gradlew :lib:compileGroovy --no-build-cache
> --rerun-tasks`
> Exception thrown:
> {code:java}
> java.lang.RuntimeException: java.lang.NoClassDefFoundError: Unable to load
> class test.groovy.traits.WithPluginValidation$AllPluginsValidation due to
> missing dependency Ltest/groovy/traits/MyTestBase;
> at
> org.codehaus.groovy.control.CompilationUnit$IPrimaryClassNodeOperation.doPhaseOperation(CompilationUnit.java:977)
> at
> org.codehaus.groovy.control.CompilationUnit.processPhaseOperations(CompilationUnit.java:672)
> at
> org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:636)
> at
> org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:611)
> at
> org.gradle.api.internal.tasks.compile.ApiGroovyCompiler.execute(ApiGroovyCompiler.java:271)
> ...
> Caused by: java.lang.NoClassDefFoundError: Unable to load class
> test.groovy.traits.WithPluginValidation$AllPluginsValidation due to missing
> dependency Ltest/groovy/traits/MyTestBase;
> at org.codehaus.groovy.vmplugin.v8.Java8.configureClassNode(Java8.java:447)
> at org.codehaus.groovy.ast.ClassNode.lazyClassInit(ClassNode.java:273)
> at
> org.codehaus.groovy.ast.ClassNode.getUnresolvedSuperClass(ClassNode.java:1014)
> at
> org.codehaus.groovy.ast.ClassNode.getUnresolvedSuperClass(ClassNode.java:1006)
> at org.codehaus.groovy.ast.ClassNode.getSuperClass(ClassNode.java:1000)
> at org.codehaus.groovy.ast.ClassNode.isDerivedFrom(ClassNode.java:911)
> at
> org.codehaus.groovy.classgen.asm.InvocationWriter.castToNonPrimitiveIfNecessary(InvocationWriter.java:1144)
> at
> org.codehaus.groovy.classgen.asm.OperandStack.doConvertAndCast(OperandStack.java:363)
> at
> org.codehaus.groovy.classgen.asm.OperandStack.doGroovyCast(OperandStack.java:300)
> at
> org.codehaus.groovy.classgen.AsmClassGenerator.storeThisInstanceField(AsmClassGenerator.java:1259)
> at
> org.codehaus.groovy.classgen.AsmClassGenerator.visitFieldExpression(AsmClassGenerator.java:1196)
> at org.codehaus.groovy.ast.expr.FieldExpression.visit(FieldExpression.java:41)
> at
> org.codehaus.groovy.classgen.AsmClassGenerator.visitPropertyExpression(AsmClassGenerator.java:1124)
> at
> org.codehaus.groovy.ast.expr.PropertyExpression.visit(PropertyExpression.java:63)
> at
> org.codehaus.groovy.classgen.AsmClassGenerator.visitVariableExpression(AsmClassGenerator.java:1339)
> at
> org.codehaus.groovy.ast.expr.VariableExpression.visit(VariableExpression.java:71)
> ...
> at
> org.codehaus.groovy.ast.ClassCodeVisitorSupport.visitClass(ClassCodeVisitorSupport.java:52)
> at
> org.codehaus.groovy.classgen.AsmClassGenerator.visitClass(AsmClassGenerator.java:273)
> at
> org.codehaus.groovy.control.CompilationUnit$3.call(CompilationUnit.java:798)
> at
> org.codehaus.groovy.control.CompilationUnit$IPrimaryClassNodeOperation.doPhaseOperation(CompilationUnit.java:943)
> ... 34 more
> {code}
> Classes hierarchy:
> {code:java}
> class MyTest extends MyTestBase implements WithPluginValidation { ... }
> class MyTestBase { ... } // has a dependency to MyTest inside a
> method@SelfType(MyTestBase)
> trait WithPluginValidation {
> final AllPluginsValidation allPlugins = new AllPluginsValidation(this)
> static class AllPluginsValidation {
> final MyTestBase base
> AllPluginsValidation(MyTestBase base) {
> this.base = base
> }
> }
> }{code}
>
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)