Groovy 5.0.x support for Grails 8 + Spring Boot 4 - #15557
Conversation
This reverts commit 457d6cd.
# Conflicts: # build.gradle # dependencies.gradle # grails-forge/build.gradle # grails-gradle/build.gradle
# Conflicts: # buildSrc/build.gradle # dependencies.gradle # grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy # grails-gradle/buildSrc/build.gradle
# Conflicts: # dependencies.gradle # gradle/test-config.gradle # grails-forge/settings.gradle # settings.gradle
# Conflicts: # gradle.properties # grails-core/src/test/groovy/org/grails/plugins/BinaryPluginSpec.groovy
… + latest Jackson)
Cherry-picked comprehensive Groovy 5 compat from 9574fe8. Conflict resolutions: - dependencies.gradle: Groovy 5.0.5 GA (not SNAPSHOT) + Jackson 2.21.2 - LoggingTransformer: Keep manual log field injection (avoids Groovy 5 VariableScopeVisitor NPE entirely) - TransactionalTransformSpec: Remove direct Spock feature method invocation (Groovy 5/Spock 2.x incompatible) - grails-test-core/build.gradle: Remove spock-core transitive=false, keep junit-platform-suite - grails-test-suite-uber/build.gradle: Remove spock-core transitive=false and explicit byte-buddy
…lper Groovy's experimental @Anchored trait-static annotation was voted down by the Groovy PMC and removed upstream (GROOVY-12093, replaced by @virtual), and Groovy 5.0.7 has not shipped yet, so revert the dependency coordinate from the released 5.0.7 back to 5.0.7-SNAPSHOT. @virtual only restores per-implementer override dispatch for same-trait or implementing-class calls; it does not let a child @CompileStatic trait resolve an inherited parent-trait static method (verified against 5.0.7-SNAPSHOT). Drop the @Anchored annotation/import from ExecutesClosures (keeping the plain static withDelegate as the public trait contract) and re-inline the null-safe DELEGATE_ONLY closure logic into the Arguable and ComplexTyped sub-traits, the workaround that predated @Anchored. Assisted-by: opencode:anthropic/claude-opus-4-8 oracle librarian
Update: tracking
|
| Attempt | Result |
|---|---|
plain static + ExecutesClosures.withDelegate(...) (qualified) |
STC error: Cannot find matching method java.lang.Class#withDelegate(...) |
@Virtual + ExecutesClosures.withDelegate(...) (qualified) |
same java.lang.Class#withDelegate error |
@Virtual + withDelegate(...) (unqualified, inherited) |
STC error: Cannot find matching method ...Arguable#withDelegate(...) |
This matches Groovy's own TraitStaticDispatchMatrix (rows 7/8: trait-qualified static access throws / is unsupported) and VirtualAnnotationTest (every @Virtual case is a same-trait or implementing-class call, never a child-trait→parent-trait inherited static).
The workaround. Since no @Virtual call form compiles for the cross-trait case, the GraphQL helper goes back to the approach that predated @Anchored:
ExecutesClosures.withDelegatestays a plainstaticmethod (still the trait's public contract for implementing classes - just without@Anchored/@Virtual).ArguableandComplexTypedre-inline the null-safeDELEGATE_ONLYclosure logic instead of calling the parent-trait static.
Verification (against fresh 5.0.7-SNAPSHOT, Groovy snapshot caches flushed before re-resolve):
./gradlew :grails-data-graphql-core:compileGroovy :grails-data-graphql-core:compileTestGroovy --rerun-tasks --refresh-dependencies- BUILD SUCCESSFUL./gradlew :grails-data-graphql-core:test- all specs PASSED./gradlew :grails-data-graphql-core:codeStyle- PASSED (Checkstyle + CodeNarc)git diff --check- clean
…c dispatch Groovy 5's finalized trait-static model (after @Anchored was removed in GROOVY-12093) makes plain trait statics declarer-bound: a this.staticMethod() call in a trait body invokes the trait's own copy, not an implementing class's override. Validateable's trait body calls this.defaultNullable(), so a subclass that overrides defaultNullable() to return true was no longer seen, regressing two ValidateableTraitSpec tests (constraints nullable-by-default when overridden, and overridden-defaultNullable properties not accessed during validation). Annotate defaultNullable() with @groovy.transform.Virtual to opt into per-implementer dispatch so the override is visible to trait-body calls. External callers are unaffected: BeanPropertyAccessorFactory invokes it reflectively on the concrete class and DefaultASTValidateableHelper consumes a compile-time boolean. Assisted-by: opencode:anthropic/claude-opus-4-8 oracle librarian
The Arguable/ComplexTyped sub-traits inline ExecutesClosures.withDelegate because Groovy 5 STC cannot resolve an inherited parent-trait static from a sub-trait body when an argument is a subtype of the parameter type. Cite the tracking issue GROOVY-12106 (with a standalone reproducer) so the workaround can be removed once the STC bug is fixed. Assisted-by: opencode:anthropic/claude-opus-4-8 oracle librarian
Deep-dive: the
|
GROOVY-12106 (STC cannot resolve an inherited parent-trait static from a sub-trait body) is now Resolved/Fixed upstream for Groovy 5.0.7, which puts the GraphQL Arguable/ComplexTyped inline-duplication workaround on a removal path. It cannot be removed yet: the latest published 5.0.7-SNAPSHOT artifact (build 18, 5.0.7-20260628.011302-18) was cut before the fix merged, and :grails-data-graphql-core:compileGroovy --refresh-dependencies against it still fails STC with "Cannot find matching method ...#withDelegate(Closure, Object)". Refresh the two in-code comments to record the precise status so the inline is restored to the plain withDelegate(closure, (Object)...) 8.0.x form as soon as a snapshot containing the fix is consumed. Assisted-by: claude-code:claude-4.8-opus
…ent) The earlier commit claimed the GraphQL Arguable/ComplexTyped inline workaround stays because the published 5.0.7-SNAPSHOT predated the GROOVY-12106 fix. That was wrong: the fix IS present in the consumed snapshot (build 18, 5.0.7-20260628.011302-18, produced by the CI run for fix commit b46ebb25e7). The real reason the workaround stays is that the GROOVY-12106 fix does not cover this case - it is order-dependent. The fix lives in TraitTypeCheckingExtension (the STC layer) and only engages after TraitReceiverTransformer has rewritten the unqualified inherited static call into super-trait helper-static form. When a sub-trait is transformed before the super-trait's $Trait$Helper is generated - exactly the layout here, since Arguable/ComplexTyped sort alphabetically before ExecutesClosures - that rewrite is skipped (findConcreteMethod returns null) and the fixed STC branch is never reached. Confirmed with a minimal standalone reproducer compiled against the fix build: sub-trait-before-super-trait fails STC, super-trait-first compiles. This is a distinct upstream transform-order defect, earlier than the 12106 STC fix (cf. GROOVY-11743). No clean Grails-side call-form or instance-method variant compiles, so the inline is retained; correct only the in-code comments. Assisted-by: claude-code:claude-4.8-opus
|
**
Proof - minimal self-contained reproducer compiled against build 18 (and against a local Groovy build from import groovy.transform.CompileStatic
final class FieldA {}
@CompileStatic
trait ArguableA<T> extends ExecutesClosuresA { // sub-trait declared FIRST
String describe(FieldA f) { withDelegate({ -> }, (Object) f); 'ok' }
}
@CompileStatic
trait ExecutesClosuresA {
static void withDelegate(@DelegatesTo(strategy = Closure.DELEGATE_ONLY) Closure c, Object d) { if (c != null) c.call() }
}
class CA implements ArguableA<String> {}
This is essentially Grails-side escape hatches tried and rejected: plain The in-code comments and the PR description have been corrected to this root cause in Assisted-by: claude-code:claude-4.8-opus |
# Conflicts: # grails-doc/src/en/guide/upgrading/upgrading80x.adoc
GROOVY-12117: tested the dedicated transform-order fix locally - GraphQL inline must stayThe What I did:
Result:
So the GROOVY-12117 fix, as it currently stands on Updated the workarounds table at the top of the PR and the in-code Assisted-by: claude-code:claude-4.8-opus |
…tream) GROOVY-12106 (STC cannot resolve an inherited parent-trait static from a sub-trait body) is now fixed in the published Groovy 5.0.7-SNAPSHOT. Paul King's port to GROOVY_5_0_X (apache/groovy commit 67198d4) makes the TraitReceiverTransformer resolve the original static on the trait node even when findHelper returns null - exactly the sub-trait-transformed-first ordering the Grails workaround targeted (Arguable/ComplexTyped sort before ExecutesClosures). The fix is present in the latest published snapshot (build 20, 5.0.7-20260630.144435-20), so no local Groovy build is required. Verified by compiling against the refreshed snapshot: :grails-data-graphql-core:compileGroovy --refresh-dependencies runs the task fresh and succeeds; the previously failing "Cannot find matching method ...#withDelegate(Closure, Object)" STC error is gone. Drop the inlined ExecutesClosures.withDelegate bodies from Arguable and ComplexTyped and restore the plain inherited-static withDelegate(closure, (Object) ...) call (the 8.0.x form). The call is behaviorally identical: withDelegate keeps the null-safe DELEGATE_ONLY logic and the finally-block delegate cleanup. Assisted-by: claude-code:claude-4.8-opus oracle
GROOVY-12106 fixed upstream - GraphQL trait-static workaround removedPaul King pushed the GROOVY-12106 port to Why this commit fixes our case. Earlier the STC fix was order-dependent: it only engaged after The good part: it is in the published snapshot, so no local Groovy build is needed. The fix is present in the latest published Verified locally against the published snapshot: ran the task fresh (not from cache) and Change. Dropped the inlined |
jdaugherty
left a comment
There was a problem hiding this comment.
Once Groovy 5.0.7 releases, I agree this is ready for Grails 8.
Resolve conflicts in the tag-library type-checking extension where 8.0.x renamed ControllerTagLibTypeCheckingExtension to TagLibraryInvokerTypeCheckingExtension and generalized it to tag dispatchers (controllers + taglibs) with a canMakeDynamic() guard, while this branch added Groovy 5 GROOVY-12041 namespace-dispatch handling. The merged extension keeps both: the tag-dispatcher generalization and the isDynamicNamespaceReceiver() Groovy 5 handling. GrailsASTUtils keeps both the GRAILS_COMPILE_STATIC_EXTENSIONS list and the ZERO_PARAMETERS deprecation, and the spec retains all taglib tests plus the Groovy 5 namespace-dispatch test (retargeted to the renamed extension). Assisted-by: claude-code:claude-4.8-opus
Groovy 5's groovy-groovysh uses JLine 3, so the second, legacy JLine 2 (jline:jline:2.14.6) that was pinned only to satisfy Groovy 4's groovysh is no longer needed. All source uses the org.jline.* (JLine 3) API, and the full JLine 3 stack is pulled in transitively via groovy-groovysh 5.x. Drops the jline2 managed dependency from grails-bom (dependencies.gradle) and every explicit jline:jline consumer: the grails-console runtime and the groovydoc `documentation` configurations. Verified :grails-console:build (including groovydoc) and :grails-shell-cli:test pass with only JLine 3 on the classpath. Assisted-by: claude-code:claude-4.8-opus
|
Merged latest Merge conflict resolution (3 files)
Verified: Removed the second JLine (JLine 2)The Verified: |
Removing the JLine 2 pin exposed an even older jline:jline:2.11 that Spring Boot CLI drags in transitively, which the SBOM license gate rejects (BSD-4-Clause) and broke CI in :grails-shell-cli:cyclonedxDirectBom and the Forge :grails-cli SBOM. No source uses the old jline.* API, so exclude jline:jline from every spring-boot-cli declaration; only the JLine 3 (org.jline) stack remains on the classpath. Verified :grails-shell-cli:cyclonedxDirectBom and Forge :grails-cli:cyclonedxDirectBom generate clean SBOMs, and :grails-shell-cli:test / :grails-console:test pass. Assisted-by: claude-code:claude-4.8-opus
|
CI fix pushed ( The previous JLine 2 removal turned CI red: dropping the Since no source uses the old Verified locally:
|
✅ All tests passed ✅🏷️ Commit: 2966b3b Learn more about TestLens at testlens.app. |
Pulled apache/groovy master to commit 40499016 (HEAD as of 2026-05-03 18:03 UTC) and the 6.0.0-SNAPSHOT publication at build #571 (5.0.6-20260503.181740-571 on the snapshot timeline). Two more workarounds become removable: 1. grails-data-hibernate5/.../HibernateConnectionSourceSettings.groovy The explicit clone() override on the inner @AutoClone HibernateSettings class was the workaround for the Java stub generator regression that emitted 'clone() throws CloneNotSupportedException' on a class extending LinkedHashMap (whose JDK clone() does not declare the exception). Tracked as GROOVY-11980 (https://issues.apache.org/jira/browse/GROOVY-11980), committed to apache/groovy master 2026-05-02 21:29 UTC as ced726ce ('GROOVY-11980: @AutoClone clone() override adds CloneNotSupportedException not declared by superclass'). Build #571 contains the fix. Removed the explicit clone() body and the 16-line workaround comment. @AutoClone now generates the override with the correct (no-throws) signature, javac accepts it as a valid override of LinkedHashMap.clone(), and the deep- clone semantics for tenant connection-source settings are preserved by @AutoClone(style = CLONE) which is the default style. 2. grails-geb/.../testFixtures/grails/plugin/geb/ContainerGebConfiguration.groovy IContainerGebConfiguration converted from trait back to interface with default methods. The interface->trait workaround was for an indy=false IncompatibleClassChangeError ('Method '...\()' must be InterfaceMethodref constant') that fired when downstream classes compiled with -PgrailsIndy=false consumed the interface. Tracked as GROOVY-11982 (https://issues.apache.org/jira/browse/GROOVY-11982), committed to apache/groovy master 2026-05-02 23:16 UTC as 88ca738c ('GROOVY-11982: Default methods in interface throw IncompatibleClassChangeError under indy=false'). Build #571 contains the fix. Standalone reproducer in https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/blob/main/quick-checks/src/main/groovy/InterfaceDefaultsCheck.groovy was the basis for both the original workaround and this restoration; it now passes against build #571. Compilation re-verified locally on Groovy 6.0.0-SNAPSHOT build #571: ./gradlew :grails-data-hibernate5-core:compileGroovy --refresh-dependencies ./gradlew :grails-geb:compileTestFixturesGroovy --refresh-dependencies Both BUILD SUCCESSFUL. Runtime validation of the indy=false ContainerGebSpec class init path is deferred to the canary CI matrix - the affected specs (InheritedConfigSpec, ChildPreferenceInheritedConfigSpec) extend ContainerGebSpec implements IContainerGebConfiguration and exercise the exact \() InterfaceMethodref dispatch the upstream fix addresses. (Pre-existing :grails-fields:compileGroovy failure on this canary - unrelated to either of these workarounds; reproduces on the unmodified merged tree.) Net effect: two more rows leave the 'Real Groovy 6 regressions, no upstream PR yet' table in the PR description. Combined with the three inherited-from-#15557 workarounds dropped on the parent branch (GROOVY-11983 unlocking PersistentEntityCodec + DefaultHalViewHelper), five workarounds dropped against this round of upstream fixes. Assisted-by: claude-code:claude-opus-4.6
Adds Apache Groovy 5 support on top of
8.0.x, tracking the pre-release Apache Groovy5.0.7-SNAPSHOTdevelopment build.The PR is now narrowed to the Grails-side workarounds and adaptations still needed after the fixes that landed in Groovy
5.0.7-SNAPSHOT.Target stack
8.0.xlineEnd-application impact
Default Grails 8 applications that use the Grails Gradle plugin and the default
platform(grails-bom)dependency management should not need build-script changes for this Groovy 5 update.Applications that intentionally opt out of the default Grails BOM path still need to keep Groovy and Spock aligned with the Grails BOM. In particular, apps using
grails { bom = null }plusio.spring.dependency-management, or Spring Boot applications consuming Grails GSP modules outside the normal Grails platform path, must importorg.apache.grails:grails-bomand aligngroovy.version/spock.versionwith it.Application code compiled with
@CompileStatic/@GrailsCompileStaticshould also account for the Groovy 5 behavior changes already documented in the upgrade guide updates in this PR:constraints,mapping, ornamedQueriesshould qualify the field with the class name.ConfigObjectprobes that expect missing keys to be null should usecontainsKey(key) ? config.get(key) : null, sinceconfig[key]now creates an empty nestedConfigObjectfor missing keys.Grails-side workarounds and adaptations
ConstrainedProperty.DEFAULT_MESSAGESHashMapinitializer receiver.GroovyPageTypeCheckingExtensionandControllerTagLibTypeCheckingExtensiongetProperty(String)receivers as dynamic beforeunresolvedVariable/unresolvedPropertycallbacks can record Grails taglib namespace dispatch.StaticTypesMarker.DYNAMIC_RESOLUTIONto recognize taglib namespace receivers, while still requiring GSP receiver names to be configured taglib namespaces.GroovyPageTypeCheckingExtensionGSP undeclared-variable validationgetProperty(String)path.GspCompileStaticSpecchecks are enabled again.grails-test-examples/gsp-spring-bootSpring Dependency Management example8.0.0-SNAPSHOTBOM before the updated snapshot is published, letting Spring DM downgrade SiteMesh Spring artifacts to an unpublished version.dependencies.gradlevalues while still importinggrails-bom, keeping this regression example deterministic in CI.Local verification for the latest update
./gradlew :grails-data-graphql-core:compileGroovy --refresh-dependencies(against the published5.0.7-SNAPSHOT, build 20) - succeeds; the previously failingArguable/ComplexTyped#withDelegate(Closure, Object)STC error is gone../gradlew :grails-data-graphql-core:test./gradlew :grails-data-graphql-core:codeStylegit diff --check