Skip to content

Groovy 5.0.x support for Grails 8 + Spring Boot 4#15557

Open
jamesfredley wants to merge 201 commits into
8.0.xfrom
grails8-groovy5-sb4
Open

Groovy 5.0.x support for Grails 8 + Spring Boot 4#15557
jamesfredley wants to merge 201 commits into
8.0.xfrom
grails8-groovy5-sb4

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Adds Apache Groovy 5 support on top of 8.0.x, tracking the pre-release Apache Groovy 5.0.7-SNAPSHOT development build.

Note on the version pin: This PR previously pinned the released 5.0.7 coordinate. Groovy 5.0.7 has not actually shipped yet (the GROOVY_5_0_X branch is still 5.0.7-SNAPSHOT), and the experimental @Anchored trait-static annotation this PR briefly relied on was voted down by the Groovy PMC and removed upstream (GROOVY-12093, "out with @Anchored in with @Virtual"). We therefore track 5.0.7-SNAPSHOT.

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

Component Version
Apache Groovy 5.0.7-SNAPSHOT
Spock 2.4-groovy-5.0
Spring Boot 4.x on the 8.0.x line
Spring Framework 7.0.x
Jakarta EE 10
JDK 21+

End-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 } plus io.spring.dependency-management, or Spring Boot applications consuming Grails GSP modules outside the normal Grails platform path, must import org.apache.grails:grails-bom and align groovy.version / spock.version with it.

Application code compiled with @CompileStatic / @GrailsCompileStatic should also account for the Groovy 5 behavior changes already documented in the upgrade guide updates in this PR:

  • Static field assignment inside static closures such as constraints, mapping, or namedQueries should qualify the field with the class name.
  • Raw ConfigObject probes that expect missing keys to be null should use containsKey(key) ? config.get(key) : null, since config[key] now creates an empty nested ConfigObject for missing keys.

Grails-side workarounds and adaptations

Site Why Grails still has code here Current handling
ConstrainedProperty.DEFAULT_MESSAGES GROOVY-12063 remains open. Uses a Groovy map literal, then wraps it immutable, so sibling default-message constants resolve against the enclosing interface scope rather than as dynamic reads against an empty HashMap initializer receiver.
GroovyPageTypeCheckingExtension and ControllerTagLibTypeCheckingExtension Groovy 5 intentionally pre-resolves getProperty(String) receivers as dynamic before unresolvedVariable / unresolvedProperty callbacks can record Grails taglib namespace dispatch. Uses StaticTypesMarker.DYNAMIC_RESOLUTION to recognize taglib namespace receivers, while still requiring GSP receiver names to be configured taglib namespaces.
GroovyPageTypeCheckingExtension GSP undeclared-variable validation Grails wants compile-static GSP model expressions to keep failing for unknown bare variables, even though Groovy 5 can dynamically resolve them through the inherited getProperty(String) path. Adds a Grails-side pass over dynamic-resolution variable expressions and reports non-taglib bare variables as undeclared. The negative GspCompileStaticSpec checks are enabled again.
grails-test-examples/gsp-spring-boot Spring Dependency Management example CI can refresh a stale remote 8.0.0-SNAPSHOT BOM before the updated snapshot is published, letting Spring DM downgrade SiteMesh Spring artifacts to an unpublished version. Explicitly manages the SiteMesh Spring artifacts from the checked-out dependencies.gradle values while still importing grails-bom, keeping this regression example deterministic in CI.

Local verification for the latest update

  • ./gradlew :grails-data-graphql-core:compileGroovy --refresh-dependencies (against the published 5.0.7-SNAPSHOT, build 20) - succeeds; the previously failing Arguable/ComplexTyped#withDelegate(Closure, Object) STC error is gone.
  • ./gradlew :grails-data-graphql-core:test
  • ./gradlew :grails-data-graphql-core:codeStyle
  • git diff --check

matrei and others added 30 commits May 15, 2025 10:51
# 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
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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Latest update pushed in this commit set:

  • 25c424b8f5 switches dependencies.gradle from 5.0.7-SNAPSHOT to the released Groovy 5.0.7.
  • 62ec5e253c tightens the GROOVY-12041 taglib namespace workaround around StaticTypesMarker.DYNAMIC_RESOLUTION for controller and GSP static type checking.
  • 9774d03b77 uses Groovy 5.0.7 @Anchored for GraphQL's trait-owned ExecutesClosures.withDelegate helper and removes the duplicated inline closure-delegate workaround.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Pushed 996a779100 to grails8-groovy5-sb4.

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:

  • ./gradlew :grails-gsp-core:test --tests "org.grails.gsp.GspCompileStaticSpec" --rerun-tasks
  • ./gradlew :grails-gsp-core:compileGroovy :grails-gsp-core:compileTestGroovy
  • ./gradlew :grails-gsp-core:codeStyle
  • ./gradlew :grails-data-graphql-core:compileGroovy
  • git diff --check

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Pushed the latest CI fix to grails8-groovy5-sb4.

Latest head: 235ced26d58e2b8b20110fd88984badb29331915

What changed:

  • Added explicit Spring Dependency Management entries for the SiteMesh Spring artifacts in the GSP Spring Boot example.
  • This keeps the example using the checked-out dependencies.gradle SiteMesh 3 versions even when CI refreshes a stale remote 8.0.0-SNAPSHOT BOM.

Local verification:

  • CI=true SITEMESH2_TESTING_ENABLED=true ./gradlew :grails-test-examples-gsp-spring-boot:dependencyInsight --dependency org.sitemesh:spring-webmvc-sitemesh --configuration runtimeClasspath --refresh-dependencies
  • CI=true SITEMESH2_TESTING_ENABLED=true ./gradlew :grails-test-examples-gsp-spring-boot:bootWar --refresh-dependencies
  • ./gradlew :grails-gsp-spring-boot:compileGroovy :grails-sitemesh3:compileGroovy

Review gate: GREEN via Oracle session ses_0ffdbc8caffeMkqOKxTGPs1fi6.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Merged the latest 8.0.x changes into grails8-groovy5-sb4 and pushed the branch.

What changed in this update:

  • Resolved the merge conflicts in the GSP Spring Boot and Spring dependency-management test examples by keeping the new Grails BOM platform(project(':grails-bom')) setup from 8.0.x.
  • Brought in the run-app / stop-app PID-file support and related tests/docs from 8.0.x.
  • Confirmed the previous missing SiteMesh artifact path now resolves org.sitemesh:spring-webmvc-sitemesh to 3.3.0-M1.
  • Re-ran the previously failing SiteMesh 2 scaffolding User list integration spec with CI-style flags and it passed.

Local verification run:

  • git diff --check
  • :grails-test-examples-scaffolding:integrationTest --tests "com.example.UserControllerSpec.User list" -PgebAtCheckWaiting -PgrailsIndy=false
  • :grails-test-examples-gsp-spring-boot:dependencyInsight --dependency org.sitemesh:spring-webmvc-sitemesh --configuration runtimeClasspath --refresh-dependencies
  • :grails-test-examples-gsp-spring-boot:bootWar --refresh-dependencies
  • :grails-test-examples-spring-dependency-management:bootJar --refresh-dependencies
  • Focused tests for GrailsAppPidFileSpec, DefaultGrailsPluginManagerSpec, BootRunExitCodeVerifierSpec, GrailsGradlePluginToolchainSpec, and RunningApplicationProcessSpec

Review gate: GREEN from Oracle session ses_0fe86dcfbffejVkLi5FDHkDmSl.

Note: the full required aggregate command ./gradlew clean aggregateViolations :grails-test-report:check --continue was started, produced no generated Checkstyle/CodeNarc/PMD/SpotBugs violations in the reports inspected, but exceeded the local 10-minute tool timeout before completion.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Update: tracking 5.0.7-SNAPSHOT again + new GraphQL trait-static workaround

Pushed 0035ca1b58.

Why the version moved back to a snapshot. This PR had pinned the released Groovy 5.0.7 coordinate. Two problems:

  1. Groovy 5.0.7 has not actually shipped - the GROOVY_5_0_X branch is still 5.0.7-SNAPSHOT.
  2. The experimental @Anchored trait-static annotation this PR briefly relied on was voted down by the Groovy PMC and removed upstream in GROOVY-12093 (commit e83dd19b, "out with @Anchored in with @Virtual"). groovy.transform.Anchored no longer exists; it is replaced by groovy.transform.Virtual.

So dependencies.gradle now tracks 5.0.7-SNAPSHOT (the build that contains the replacement model), and the affected Grails-side workaround is adapted to it.

Why @Virtual is not a drop-in replacement here. @Anchored let a child trait call a parent trait's static helper (ExecutesClosures.withDelegate(...)) under @CompileStatic. The replacement @Virtual does something different: it only restores per-implementer override dispatch for calls made from the same trait or an implementing class (the Validateable.defaultNullable()-style hook). It does not make a child @CompileStatic trait resolve an inherited parent-trait static. I verified this empirically with full Gradle compiles against a freshly re-downloaded 5.0.7-SNAPSHOT:

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.withDelegate stays a plain static method (still the trait's public contract for implementing classes - just without @Anchored/@Virtual).
  • Arguable and ComplexTyped re-inline the null-safe DELEGATE_ONLY closure 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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Deep-dive: the withDelegate trait-static problem is GROOVY-12106, and it is still broken on 5.0.7-SNAPSHOT

I dug into why the GraphQL sub-traits cannot call the inherited ExecutesClosures.withDelegate static, since it is the one thing keeping the inline-duplication workaround in this PR.

Standalone reproducer: https://github.com/jamesfredley/groovy-12106-repro (pure Groovy + Gradle, ./gradlew compileGroovy fails on 5.0.7-SNAPSHOT).

Root cause

Under @CompileStatic, when a child trait extends a parent trait and calls an inherited static helper, STC misroutes the call to the child trait's helper with a synthetic $self:

Cannot find matching method Arguable$Trait$Helper#withDelegate(java.lang.Class, groovy.lang.Closure, SimpleArgument)

The decisive trigger (found by bisection): an argument whose static type is a proper subtype of the declared parameter type. With withDelegate(Closure, Object), calling it with a CustomArgument/Field (subtypes of Object) fails; calling it with an exact Object resolves fine. @DelegatesTo, a stateful trait field, and the presence of an implementer were all ruled out as necessary - only the subtype argument is required to reproduce the minimal case.

This is exactly why GROOVY-12106 (which I filed) was closed Resolved / Cannot Reproduce: a naive minimal repro passes exact-typed arguments and compiles. The subtype case still fails on the current GROOVY_5_0_X tip - the repo above demonstrates it and I think the issue should be reopened with it.

What does NOT fix it

  • @groovy.transform.Virtual (the replacement for the removed @Anchored): only restores per-implementer override dispatch for same-trait / implementing-class calls; it does not make a child trait resolve an inherited parent-trait static. Adding it does not change the failure.
  • Casting the argument to the exact parameter type (withDelegate(closure, (Object) argument)) fixes the minimal case, but in the full grails-data-graphql-core module it is still not sufficient (the real traits then fail with Arguable#withDelegate(Closure, Object) not found). So a cast is not a usable workaround for this PR.

Conclusion for this PR

The inline-duplication workaround already on this branch stays as the only reliable option until the Groovy STC fix lands. Once GROOVY-12106 is genuinely fixed in a 5.0.7-SNAPSHOT build, the inlined blocks in Arguable / ComplexTyped can be deleted and replaced with a plain withDelegate(closure, argument) call. I left // GROOVY-12106 markers on the inlined blocks so they are easy to find and remove later.

Also fixed: the related Validateable.defaultNullable() CI failure

The same finalized Groovy 5 trait-static model broke the other direction. After the 5.0.7-SNAPSHOT switch, ValidateableTraitSpec had 2 failures (constraints nullable-by-default when overridden; overridden-defaultNullable properties not accessed during validation). Root cause: Validateable's trait body calls this.defaultNullable(), and under the new declarer-bound default a plain trait static returns the trait's own false instead of an implementing class's override. Fix: annotate Validateable.defaultNullable() with @groovy.transform.Virtual (the matrix-documented "Grails Validateable.defaultNullable" use case) so the override is seen by trait-body calls. :grails-validation:test + :grails-validation:codeStyle now pass. External callers are unaffected (reflective static invocation / compile-time boolean).

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
@jamesfredley

jamesfredley commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

**Arguable / ComplexTyped GROOVY-12106 fix does not cover the Grails shape, because it is order-dependent.

  • The fix (b7796c3f9b, PR GRAILS-5598: GSP expressions in HTML attributes cause exception if separated by a whitespace #2635) is entirely in TraitTypeCheckingExtension - the STC layer. It does subtype-aware lookup over the super-trait helper's declared statics, but only after the call has already been rewritten into …$Trait$Helper.withDelegate($static$self, closure, arg) form.
  • That rewrite happens earlier, in TraitReceiverTransformer.findConcreteMethod. If the super-trait's $Trait$Helper has not been generated yet when the sub-trait body is transformed, findConcreteMethod returns null and the call is left as a plain Arguable#withDelegate(Closure, Object) - so the fixed STC branch (which keys off isClassType(argumentTypes[0]), i.e. the synthetic $static$self Class arg) is never entered.
  • In this module the files sort Arguable / ComplexTyped before ExecutesClosures alphabetically, so the sub-traits are transformed first and hit exactly this gap. Groovy's own passing test Groovy12106.testGrailsHelperShapeWithDelegatesTo declares the parent trait first in a single script, so it never triggers the ordering.

Proof - minimal self-contained reproducer compiled against build 18 (and against a local Groovy build from b46ebb25e7):

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> {}
  • Sub-trait-first (above) → fails: Cannot find matching method ArguableA#withDelegate(groovy.lang.Closure, java.lang.Object) - the exact Grails error.
  • Reverse the two trait declarations (super-trait first) → compiles.

This is essentially Groovy12106.testGrailsHelperShapeWithDelegatesTo with only the declaration order reversed. It looks like a distinct upstream transform-order defect, earlier than the 12106 STC fix - reminiscent of the older GROOVY-11743 "super trait may not be transformed when creating helper". Worth a follow-up ticket for Paul with the reproducer above (the real fix would be in TraitReceiverTransformer.findConcreteMethod: fall back to the super-trait's declared statics when its helper is not yet generated, or order helper generation before sub-trait body rewriting).

Grails-side escape hatches tried and rejected: plain withDelegate(closure, argument), (Object)-cast, this.withDelegate(...), and making withDelegate an instance method all fail - the instance + @DelegatesTo form fails to bind (Not enough arguments found for a @DelegatesTo method call), and withDelegate is used across ~8 GraphQL files, so a signature change is invasive. So the inline stays.

The in-code comments and the PR description have been corrected to this root cause in 12292b860b. The inline can drop to the byte-identical 8.0.x withDelegate(closure, (Object)…) call once the upstream transform-order gap is fixed.

Assisted-by: claude-code:claude-4.8-opus

@jamesfredley

Copy link
Copy Markdown
Contributor Author

GROOVY-12117: tested the dedicated transform-order fix locally - GraphQL inline must stay

The withDelegate trait-static gap now has its own ticket, GROOVY-12117 ("resolution of inherited static trait method from sub-trait body is transform-order dependent"), and the fix landed on GROOVY_5_0_X as commit fcab925d21. The Apache snapshot publish for it failed in CI (run 28402000859), so it never reached repository.apache.org - the published 5.0.7-SNAPSHOT (build 19, 5.0.7-20260629.205749-19) does not contain it. I therefore tested the fix from a local build.

What I did:

  • Checked out GROOVY_5_0_X tip at fcab925d21 (the GROOVY-12117 fix) and ./gradlew publishToMavenLocal (5.0.7-SNAPSHOT).
  • Purged every org.apache.groovy:*:5.0.7-SNAPSHOT artifact from the Gradle cache so nothing remote could shadow it.
  • Removed the Arguable / ComplexTyped inline and restored the plain withDelegate(closure, (Object)…) call.
  • Compiled with GRAILS_INCLUDE_MAVEN_LOCAL=1, which puts mavenLocal() first in dependency resolution. Confirmed via dependencyInsight + an empty remote-snapshot cache that groovy resolved from mavenLocal (the fix build).

Result:

Configuration (groovy = local fcab925d21 build) :grails-data-graphql-core:compileGroovy
Workaround removed (plain withDelegate(closure, (Object)…)) FAILS - Cannot find matching method Arguable#withDelegate(Closure, Object) / ComplexTyped#withDelegate(Closure, Object)
Workaround kept (inline DELEGATE_ONLY) compiles green

So the GROOVY-12117 fix, as it currently stands on GROOVY_5_0_X, still does not cover the Grails sub-trait-before-super-trait module layout (Arguable/ComplexTyped sort alphabetically before ExecutesClosures, so they are transformed before the super-trait's $Trait$Helper exists and TraitReceiverTransformer skips the rewrite the fixed STC branch keys off). The inline workaround stays for now.

Updated the workarounds table at the top of the PR and the in-code // GROOVY-12117 markers to cite this ticket and the local-build verification. The inline can drop to the byte-identical 8.0.x withDelegate(closure, (Object)…) call once GROOVY-12117 is fixed for the transform-order case and a fixed snapshot publishes.

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
@jamesfredley

Copy link
Copy Markdown
Contributor Author

GROOVY-12106 fixed upstream - GraphQL trait-static workaround removed

Paul King pushed the GROOVY-12106 port to GROOVY_5_0_X (apache/groovy@67198d4), and this time it lands the case Grails actually hits.

Why this commit fixes our case. Earlier the STC fix was order-dependent: it only engaged after TraitReceiverTransformer rewrote the unqualified inherited static call into super-trait helper-static form, which is skipped when a sub-trait is transformed before the super-trait's $Trait$Helper exists - exactly our layout, since Arguable/ComplexTyped sort alphabetically before ExecutesClosures. The new commit makes TraitReceiverTransformer.findConcreteMethod resolve the original static on the trait node even when findHelper returns null (which is what happens on 5.0.x for the not-yet-lowered super trait), so the rewrite is no longer skipped in the sub-trait-first ordering.

The good part: it is in the published snapshot, so no local Groovy build is needed. The fix is present in the latest published 5.0.7-SNAPSHOT (build 20, 5.0.7-20260630.144435-20), which is the artifact this PR resolves and the same one CI will resolve - so CI should compile green too, no mavenLocal / GRAILS_INCLUDE_MAVEN_LOCAL dance required anymore.

Verified locally against the published snapshot:

./gradlew :grails-data-graphql-core:compileGroovy --refresh-dependencies

ran the task fresh (not from cache) and BUILD SUCCESSFUL. The previously failing Cannot find matching method ...#withDelegate(Closure, Object) STC error is gone with the workaround removed.

Change. Dropped the inlined ExecutesClosures.withDelegate bodies from Arguable and ComplexTyped and restored the plain inherited-static withDelegate(closure, (Object) ...) call (the 8.0.x form). Behaviorally identical - withDelegate keeps the null-safe DELEGATE_ONLY logic and the finally-block delegate cleanup. The GROOVY-12106 row has been removed from the workarounds table in the description above.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once Groovy 5.0.7 releases, I agree this is ready for Grails 8.

@testlens-app

testlens-app Bot commented Jun 30, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 7140f77
▶️ Tests: 29107 executed
⚪️ Checks: 46/46 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants