Skip to content

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

Open
jamesfredley wants to merge 191 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 191 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 again and adapt the affected Grails-side workaround to the new trait-static model. See the comment at the bottom of this PR for details.

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.
ExecutesClosures.withDelegate in grails-data-graphql Groovy 5's experimental @Anchored trait-static annotation (which let sub-traits call a parent trait's static helper under @CompileStatic) was voted down and removed (GROOVY-12093). Its replacement, @groovy.transform.Virtual, only restores per-implementer override dispatch for same-trait/implementer calls; it does not make a child trait resolve an inherited parent-trait static under STC (verified empirically against 5.0.7-SNAPSHOT). Keeps the plain static ExecutesClosures.withDelegate as the trait's public contract, but re-inlines the null-safe DELEGATE_ONLY closure logic in the Arguable / ComplexTyped sub-traits (the workaround that predated @Anchored), since no @Virtual call form compiles for the cross-trait case.
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 :grails-data-graphql-core:compileTestGroovy --rerun-tasks --refresh-dependencies (against a freshly re-downloaded 5.0.7-SNAPSHOT)
  • ./gradlew :grails-data-graphql-core:test
  • ./gradlew :grails-data-graphql-core:codeStyle
  • git diff --check

Latest commit set

This commit set switches dependencies.gradle back from the unreleased 5.0.7 coordinate to 5.0.7-SNAPSHOT, and updates the GraphQL closure-delegate workaround for the upstream removal of @Anchored: it drops the @Anchored annotation/import from ExecutesClosures (leaving a plain static helper as the public trait contract) and re-inlines the null-safe DELEGATE_ONLY closure logic into the Arguable and ComplexTyped sub-traits, because the replacement @Virtual annotation does not let a child @CompileStatic trait resolve an inherited parent-trait static method.

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

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

I think we could move forward with this if we open subsequent tickets on the identified issues. I would really like to hear from @paulk-asert on the static compilation workaround in gsp & on the ? vs object assignment issue

@jamesfredley I believe we can also remove the inlines now?

uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: ${{ matrix.language }}
# The autobuild action runs `./gradlew testClasses`, which compiles Groovy sample apps that are covered by regular CI.

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.

Don't we need to compile it? Isn't setting this to non not building for it?

private void handleArgumentClosure(CustomArgument argument, @DelegatesTo(strategy = Closure.DELEGATE_ONLY)Closure closure) {
withDelegate(closure, (Object)argument)
// Inlined ExecutesClosures.withDelegate: Groovy 5 STC can't resolve a parent trait's static method.
if (closure != null) {

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.

Isn't this fixed now so the inline is unnecessary?

field.nullable(defaultNull)
withDelegate(closure, (Object)field)
// Inlined ExecutesClosures.withDelegate: Groovy 5 STC can't resolve a parent trait's static method.
if (closure != null) {

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.

I thought this is fixed and the inline is unnecessary?


trait TestTrait<F extends Serializable> {
F from
trait TestTrait<T> {

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.

This code is still here but I thought @matrei opened a ticket and its been fixed

Comment thread grails-gsp/core/src/test/groovy/org/grails/gsp/GspCompileStaticSpec.groovy Outdated
Comment thread grails-gsp/core/src/test/groovy/org/grails/gsp/GspCompileStaticSpec.groovy Outdated
…2041

Per review feedback, switch the two GspCompileStaticSpec negative
(undeclared-variable) specs from @IgnoreIf to @PendingFeatureIf so they fail -
prompting removal of both the guards and the GroovyPageTypeCheckingExtension
methodNotFound name-matching workaround - the moment GROOVY-12041 (open) is
fixed and the undeclared-variable check is reported again. On Groovy 5 the two
specs report as pending (skipped); on Groovy 4 they pass unchanged.

Assisted-by: claude-code:claude-4.8-opus
GROOVY-12091 (bounded generic trait property setter remains abstract in
the implementing class) is Resolved/Fixed for Groovy 5.0.7 and verified
on the consumed 5.0.7-SNAPSHOT, so ClassPropertyFetcherTests.TestTrait
restores its original `<F extends Serializable>` bound and drops the
Groovy 5 workaround. Verified by
ClassPropertyFetcherTests.testClassPropertyFetcherWithTraitProperty.

Assisted-by: Sisyphus:anthropic/claude-opus-4-8
@jamesfredley

Copy link
Copy Markdown
Contributor Author

Burned down the Groovy 5 workarounds against the current 5.0.7-SNAPSHOT and merged the latest 8.0.x.

Removed (now fixed upstream):

  • GROOVY-12091 (ClassPropertyFetcherTests.TestTrait) is Resolved/Fixed for Groovy 5.0.7. Restored the original <F extends Serializable> bound - the bounded generic trait-property setter is no longer left abstract in the implementer. Verified by ClassPropertyFetcherTests.testClassPropertyFetcherWithTraitProperty on the refreshed snapshot. This resolves @jdaugherty's "why are you removing serializable?" comment.

Checked, still required (kept):

  • GROOVY-12063 (ConstrainedProperty.DEFAULT_MESSAGES map literal) - still Open upstream.
  • GROOVY-12041 (GroovyPageTypeCheckingExtension name-matching + GspCompileStaticSpec @PendingFeatureIf) - still Open upstream.
  • Arguable / ComplexTyped inline withDelegate - no upstream ticket. Re-tested on 5.0.7-SNAPSHOT: reverting either trait to the parent-trait withDelegate(closure, ...) call still fails STC (Cannot find matching method ...#withDelegate(Closure, Object)). Groovy 5 STC does not resolve a parent trait's static method from a sub-trait body; this is distinct from the now-fixed GROOVY-11985 (static-override-via-this). I'll file a dedicated Groovy ticket so the inline can be dropped later.

The PR description at the top has been updated to reflect the current state.

…Groovy 5

The compileStaticTagLibs support merged from 8.0.x (ControllerTagLibTypeCheckingExtension)
relied on the unresolvedVariable / unresolvedProperty callbacks firing for the namespace
dispatcher receiver (e.g. the `cst` in `cst.greet(...)`). On Groovy 5 those callbacks no
longer fire when the controller inherits getProperty(String) (GROOVY-12041), so the
dispatcher resolves to Object and the subsequent tag call failed static type checking with
"Cannot find matching method java.lang.Object#<tag>(...)".

methodNotFound now recognises that case directly: when the receiver is Object-typed and the
object expression is a bare-variable or this-property dispatcher, the call is made dynamic -
matching this extension's existing intent to silence namespace dispatch in controllers. Calls
on declared fields/locals keep their concrete receiver type and remain fully type-checked, so
the type-safety guarantees are preserved.

This fixes the Groovy 5 compile failures in ControllerCompileStaticTagLibSpec (grails-gsp) and
the demo33 CompileStaticController, which were cascading into the full build, functional,
mongodb, hibernate and SiteMesh CI lanes.

Assisted-by: Sisyphus:anthropic/claude-opus-4-8
@jdaugherty

Copy link
Copy Markdown
Contributor

@jamesfredley your last update said you were going to file a dedicated ticket - has this been done?

@jamesfredley

Copy link
Copy Markdown
Contributor Author

@jdaugherty I will try to get it filed today. Have had a small amount of time. And should have a commit to fix all the breakage in 1-2 hours after h7.4 was merged in.

@jdaugherty

Copy link
Copy Markdown
Contributor

@jamesfredley did you see the GSP workaround that Paul posted as well on the groovy ticket? That would resolve the gsp issue.

Assisted-by: hephaestus:gpt-5.5
@jamesfredley

Copy link
Copy Markdown
Contributor Author

@jdaugherty https://issues.apache.org/jira/browse/GROOVY-12106 and I will attempt to pull that in.

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.

@testlens-app

This comment has been minimized.

…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

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