[ 
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)

Reply via email to