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
Switch the root and override Groovy dependency versions from the staged snapshot coordinate to the released 5.0.7 artifact. Assisted-by: opencode:openai/gpt-5.5 oracle
Use Groovy's dynamic-resolution marker to recognize taglib namespace receivers after Groovy 5 resolves getProperty(String) before the unresolved callbacks run. Assisted-by: opencode:openai/gpt-5.5 oracle
Mark the GraphQL closure helper as anchored so sub-traits can call it directly under Groovy 5.0.7, and remove the duplicated inline delegate logic. Assisted-by: opencode:openai/gpt-5.5 oracle
|
Latest update pushed in this commit set:
The PR description was also refreshed to keep only the current required Grails-side workarounds/adaptations and to move fully resolved Groovy issues into the resolved-upstream section. |
Groovy 5 marks inherited getProperty(String) lookups as dynamic before the unresolved-variable extension hook can report unknown GSP identifiers. Scan dynamic-resolution variable expressions in the GSP type-checking extension and report non-taglib names as undeclared so compile-static GSPs keep Grails' stricter model contract. Assisted-by: hephaestus:openai/gpt-5.5 oracle
|
Pushed This adds the Grails-side GSP validation pass for Groovy 5 dynamic-resolution variables, so compile-static GSPs still fail unknown bare variables the way Grails expects while preserving dynamic taglib namespace dispatch. I also updated the PR description to remove the separate list of items fully addressed on the Groovy side and keep the top-level list focused on the remaining Grails-side workarounds/adaptations. Latest local verification:
|
Pin the SiteMesh Spring artifacts in the GSP Spring Boot dependency-management example from the checked-out dependency map. This prevents a refreshed stale snapshot BOM from downgrading spring-webmvc-sitemesh to an unpublished version in CI. Assisted-by: Hephaestus:openai/gpt-5.5 oracle
|
Pushed the latest CI fix to Latest head: What changed:
Local verification:
Review gate: GREEN via Oracle session |
Merge the latest 8.0.x changes into the Groovy 5 PR branch, including the run-app PID file support and the Grails BOM test-example dependency-management updates. Assisted-by: Hephaestus:openai/gpt-5.5
|
Merged the latest What changed in this update:
Local verification run:
Review gate: GREEN from Oracle session Note: the full required aggregate command |
Assisted-by: Hephaestus:openai/gpt-5.5
…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.
✅ All tests passed ✅🏷️ Commit: 7140f77 Learn more about TestLens at testlens.app. |
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