Skip to content

[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558

Draft
jamesfredley wants to merge 41 commits intograils8-groovy5-sb4from
grails8-groovy6-canary
Draft

[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558
jamesfredley wants to merge 41 commits intograils8-groovy5-sb4from
grails8-groovy6-canary

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented Apr 5, 2026

Status

Canary / DRAFT - DO NOT MERGE. Layered on top of #15557 (Groovy 5 base) and brings the framework up to Groovy 6.0.0-SNAPSHOT. Audited 2026-05-04 against build #571.

Snapshot baseline

Component Version
Apache Groovy 6.0.0-SNAPSHOT build #571, timestamp 2026-05-03 18:17:40 UTC (apache/groovy master HEAD 40499016 as of 2026-05-03 18:03 UTC, modulo CI lag)
Spock 2.4-groovy-5.0 (no Groovy 6-compatible Spock yet, see "Spock blocker")
Spring Boot 4.0.5
Spring Framework 7.0.6
Gradle 9.4.1
Micronaut 4.10.10
Jakarta EE 10
JDK 21+

Base PR (must read first)

Stacked on top of #15557 - Groovy 5 support for Grails 8 + Spring Boot 4, branch grails8-groovy5-sb4. Every Groovy 5 workaround there has been independently audited; see that PR's description for the per-site verdicts.


Remaining workarounds

Real Groovy 6 regressions, no upstream PR yet (need to be filed)

Each has a standalone reproducer in a per-bug repo. Reverting any of them locally reproduces the cited failure on Groovy 6.0.0-SNAPSHOT build #571.

Workaround site Mechanism Standalone reproducer
grails-datamapping-core/.../GormEntityTransformation.groovy (per-entity AST Object get(String) shim) + drop trait-static guard from GormEntity Groovy 6's MetaClassImpl picks up the inherited Object get(Serializable) (entity-by-ID) as the genericGetMethod for instance property access, hijacking dynamic property reads (book.someConnection, book.errors) before they can fall through to propertyMissing(String). Manifests as Unknown entity: java.util.LinkedHashMap and NPE in HibernateRuntimeUtils.setupErrorsProperty across DataServiceConnectionRoutingSpec and CrossLayerMultiDataSourceSpec (16+ failures). groovy6-get-as-generic-getter - works on Groovy 4/5, fails on Groovy 6
grails-validation/.../Validateable.groovy (resolveDefaultNullable(Class) reflection-based dispatch) Starting in Groovy 5, TraitReceiverTransformer rewrites this.someStatic() from inside a trait body to call the trait helper directly, silently losing implementing-class overrides of static trait methods. Reproduces on Groovy 5.0.6-SNAPSHOT and 6.0.0-SNAPSHOT. groovy-trait-static-method-override-bug - works on Groovy 4, fails on Groovy 5 and 6
grails-core/.../template/TemplateRendererImpl.groovy (statically-typed render(Map) body) + grails-scaffolding/.../GenerateControllerCommand.groovy (typed positional templateRenderer.render(Resource, File, Map, boolean) via generateFile() helper) Under Groovy 5+ @CompileStatic, calling an overloaded render(Map<String,Object>) against a multi-overload interface reference silently no-ops (no exception, no warning, method body never entered). Reproduces with @Delegate forwarder, with direct field call, and with explicit Map literal cast - only the typed positional overload survives. Reproduces on Groovy 5 and 6 with same shape, same outcome. groovy5-compiledynamic-trait-bug (despite the name, also covers Groovy 6) - works on Groovy 4, fails on Groovy 5 and 6

Inherited from #15557 audit (kept on this branch with corrected diagnoses)

These workarounds were retained on the Groovy 5 PR after standalone audit and remain necessary on Groovy 6:

  • grails-bootstrap/.../NavigableMap.groovy - one-line containsKey + get change in resolveConfigMapValue plus a readWithoutCreating helper. Real bug: [] operator on a ConfigObject mutates it by creating empty entries on missing-key reads, recursing infinitely through mergeMaps -> mergeMapEntry.
  • grails-core/.../GrailsASTUtils.java, grails-datastore-core/.../AstUtils.groovy, grails-datamapping-core/.../AbstractMethodDecoratingTransformation.groovy - try/catch around VariableScopeVisitor + non-null VariableScope guard on ClosureExpression. Real bug: Groovy 5/6 VariableScopeVisitor NPEs during canonicalisation on certain Grails AST transformation outputs (e.g. DataServiceRoutingProductDataService.groovy in grails-datamapping-tck).
  • grails-rest-transforms/.../ResourceTransform.groovy - same family as AbstractMethodDecoratingTransformation's non-null VariableScope guard.
  • grails-test-examples/... (5 projects) - boot4-disabled-integration-test-config.gradle apply on app1, app3, exploded, mongodb/test-data-service, plugins/exploded. Real bug: under indy=false, controller action methods that declare parameters lose parameter scope (parameter resolves to a propertyMissing lookup on the controller via TagLibraryInvoker$Trait$Helper.propertyMissing instead of the local parameter), after ControllerActionTransformer.wrapMethodBodyWithExceptionHandling wraps the body in a try/catch.

Build status

Job Result
Build Grails-Core (Java 21/25, ubuntu/macos/windows) pending re-verification on this push
Functional Tests (Java 21/25, indy=false/true) pending re-verification on this push
Hibernate5 Functional Tests (Java 21/25, indy=false/true) pending re-verification on this push
Mongodb Functional Tests (Java 21/25, MongoDB 7/8, indy=false/true) pending re-verification on this push
Build Grails Forge (Java 21/25, indy=false/true) pending re-verification on this push
Core Projects / Forge Projects / Gradle Plugin Projects pending re-verification on this push

Note: :grails-fields:compileGroovy is currently failing on the canary with Target constructor for constructor call expression hasn't been set. Reproduces on the unmodified merged tree (independent of any workaround removals); will track separately.

Spock blocker

Spock has no Groovy 6-compatible artifact yet. The build sets -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true on every GroovyCompile and Test task as a temporary bridge. This flag is not safe for a production release line - the canary disclaimer at the top is non-negotiable. Once a Spock 2.5-groovy-6.0 (or 2.4-groovy-6.0) ships, delete the flag in CompilePlugin and re-verify.

What changes when each remaining upstream PR merges

The three "no upstream PR yet" rows above still need to be filed against apache/groovy with the standalone reproducers. Each is small (3-5 source files, no Grails/GORM/Hibernate on the classpath) and reproduces deterministically on Groovy 6.0.0-SNAPSHOT build #571.

cc @paulk-asert - the standalone reproducers in https://github.com/jamesfredley/groovy6-get-as-generic-getter, https://github.com/jamesfredley/groovy-trait-static-method-override-bug, and the render(Map) shape in https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug all reproduce on build #571. Happy to file these against apache/groovy if you'd prefer that route to PR review on a sandboxed reproducer.

Bumps groovy.version to 6.0.0-SNAPSHOT (from 5.0.3) to see what breaks.
Snapshot resolves from https://repository.apache.org/content/groups/snapshots
which was already configured in build-logic/GrailsRepoSettingsPlugin.groovy
for the org.apache.groovy.* group.

Changes needed on top of the Groovy 5.0.3 canary:

- gradle/test-config.gradle: apply
  '-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true' to every
  GroovyCompile task, not just compileGroovy/compileTestGroovy.
  Spock 2.4-groovy-5.0 is the latest available and refuses to run
  against Groovy 6 without this flag; since SpockTransform is
  registered via META-INF/services, the Groovy compiler loads it for
  every source set (including main) and main compiles fail without
  the flag being set globally.

- DefaultHalViewHelper.groovy: reorder the (association instanceof
  ToMany && !(association instanceof Basic)) / else if
  (association instanceof ToOne) cascade to check ToOne first.
  Groovy 6's flow typing narrows 'association' in the else branch in
  a way that conflicts with the later 'instanceof ToOne' check
  (Incompatible instanceof types: Basic and ToOne). The reordered
  form is equivalent because ToOne and ToMany are sibling Association
  subtypes.

- AbstractHibernateGormInstanceApi.groovy: fix a pre-existing
  operator-precedence bug caught by Groovy 6's stricter instanceof
  type checking.
    before: if (association instanceof ToOne && !association instanceof Embedded) {
    after:  if (association instanceof ToOne && !(association instanceof Embedded)) {
  Without the parentheses '!association' is evaluated first (to a
  boolean) and then 'instanceof Embedded' is checked against a
  boolean, which is always false - the whole left side of the && had
  been dead code. Groovy 6 now reports this as
  'Incompatible instanceof types: boolean and Embedded'.

Known still-failing: grails-geb:compileTestFixturesGroovy still
triggers the ASM Frame.putAbstractType bug that was the reason we
pinned to Groovy 5.0.3. Same bytecode-generation issue carries
forward to 6.0.0-SNAPSHOT.
Groovy 6.0.0-SNAPSHOT generates invalid bytecode for constructors that
use a default-valued List parameter inside @CompileStatic classes.
Decompiled stack frames show Object where ArrayList is expected:

  Type 'java/lang/Object' (current frame, stack[4]) is not assignable
  to 'java/util/ArrayList'
  at DefaultConstraintFactory.<init>(Class, MessageSource):V

This breaks every validateable. At runtime VerifyError is raised the
first time the default-parameter overload is constructed, which cascades
into Validateable.validate(), grails-datastore-core bean wiring, and
any test that exercises constraints.

Workaround: replace the default-parameter signature with two explicit
constructors (the 2-arg one delegates to the 3-arg one with
[Object.class] as List<Class>). This is compilation-compatible - users
were already allowed to construct with or without the targetTypes arg.
@testlens-app

This comment has been minimized.

Add spock.iKnowWhatImDoing.disableGroovyVersionCheck to all shared
test configs (hibernate5, mongodb, mongodb-forked, functional) via
tasks.withType(GroovyCompile).configureEach. The flag was only in
test-config.gradle, so modules using other configs failed with
IncompatibleGroovyVersionException on Groovy 6.

In functional-test-config.gradle, replace the per-task-name flags
with the configureEach pattern to also cover
compileIntegrationTestGroovy and other custom source sets.

Add CycloneDX license override for org.jline/jansi@4.0.7 (BSD-3-Clause)
which is pulled in by Groovy 6.0.0-SNAPSHOT's jline dependency upgrade.

Assisted-by: Claude Code <Claude@Claude.ai>
…8-groovy6-canary

# Conflicts:
#	build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
…ORM entities

Groovy 6 registers GormEntity.get(Serializable) as the genericGetMethod
in MetaClassImpl, causing dynamic property access like Entity.name to
call get("name") instead of Class.getName(). This breaks all property
access on @entity classes that goes through Groovy's dynamic dispatch.

Root cause: Groovy 6 relaxed MetaClassImpl.isGenericGetMethod from
requiring get(String) to accepting get(Serializable), which matches
GormEntity's static get(Serializable) method. Confirmed by runtime
metaclass inspection showing genericGetMethod set to get(Serializable).

Fix: add a get(String) overload to GormEntity that intercepts the
genericGetMethod calls. When the argument matches a java.lang.Class
bean property (name, simpleName, etc.), it delegates to Class.class
metaclass. Otherwise it delegates to the GORM static API as before.

Also guard staticPropertyMissing with the same Class property check
for belt-and-suspenders coverage of the Groovy 6 property resolution
change.

Assisted-by: Claude Code <Claude@Claude.ai>
… is not initialized

When Groovy 6 calls get(String) as a genericGetMethod for property
resolution and GORM is not initialized, throw MissingPropertyException
instead of IllegalStateException. This matches the existing
staticPropertyMissing behavior and passes the GormEntityTransformSpec
test for unknown static properties.

Assisted-by: Claude Code <Claude@Claude.ai>
…rrides

Move spock.iKnowWhatImDoing.disableGroovyVersionCheck into the
build-logic CompilePlugin, which is applied to ALL modules. This
replaces the per-test-config additions and covers modules like
grails-datamapping-tck and grails-test-suite-base that don't
apply any shared test config.

Add CycloneDX BSD-3-Clause license overrides for all jline 4.0.7
artifacts pulled by Groovy 6 (builtins, console, console-ui,
native, reader, shell, style, terminal, terminal-jni).

Assisted-by: Claude Code <Claude@Claude.ai>
Change outputTagResult from private to protected in
AbstractGrailsTagTests - Groovy 6 restricts private method access
from nested closures.

Set spock.iKnowWhatImDoing.disableGroovyVersionCheck on Test tasks
(not just GroovyCompile) so runtime Groovy compilation inside tests
(e.g., BeanBuilder.loadBeans) doesn't trigger Spock's version check.

Restore try-catch in GormEntity.get(String) to convert
IllegalStateException to MissingPropertyException when GORM is not
initialized, matching staticPropertyMissing behavior.

Assisted-by: Claude Code <Claude@Claude.ai>
DataBindingTests: replace old-style Author.metaClass.static.get mock
with Spock GroovySpy. Groovy 6 changed MetaClass dispatch precedence
for trait-provided static methods, so dynamically-added MetaClass
closures no longer intercept calls to compiled trait methods.

grails-views-gson StreamingJsonBuilder ClassCastException: the Groovy
parent's call(Closure) creates groovy.json.StreamingJsonDelegate via
private cloneDelegateAndGetContent, but compiled .gson templates cast
the delegate to grails.plugin.json.builder.StreamingJsonDelegate.
Fix: override call(Closure) in the Grails StreamingJsonBuilder to use
the Grails delegate subclass, and fix JsonViewWritableScript.json() to
create Grails delegates directly instead of the Groovy parent type.

Assisted-by: Claude Code <Claude@Claude.ai>
Replace Object.class with Object in the constructor delegation call.

Assisted-by: Claude Code <Claude@Claude.ai>
…RM properties

When Groovy 6's genericGetMethod calls get(String) for property
resolution, GORM-managed properties like datasource qualifiers
(e.g., Book.moreBooks) were being treated as entity-by-ID lookups
instead of routing through staticPropertyMissing.

Fix: try staticPropertyMissing first (handles GORM property
resolution including datasource qualifiers and dynamic properties),
then fall back to get(Serializable) for entity-by-ID lookups.
This preserves both property resolution and data binding paths.

Assisted-by: Claude Code <Claude@Claude.ai>
Resolves three conflicts from upstream changes on the Groovy 5 base:

- dependencies.gradle: keep groovy.version=6.0.0-SNAPSHOT (PR purpose);
  drop the jackson.version override since base now relies on Spring Boot 4
  to manage Jackson.
- SbomPlugin.groovy: union the jline 4.0.7 entries (Groovy 6 transitively
  pulls these via groovy-groovysh) with base's updated jline 3.30.x
  entries, drop the stale jline@3.23.0 entry, and adopt base's more
  accurate "transitively via groovy-groovysh; main org.jline:jline pinned
  at 3.30.6 directly" comment style for both 3.30.x and 4.0.7 entries.
- GormEntity.groovy: improve the genericGetMethod regression docstrings
  to reference the actual upstream issue (GROOVY-11829) instead of a
  placeholder, document the dispatch flow on get(String), and explain
  why this guard is necessary. Auto-merge of staticPropertyMissing was
  already correct.

Assisted-by: claude-code:claude-opus-4-7
… tests

Three improvements driven by an architectural review of the Groovy 6
canary work and a fresh build that surfaced new SNAPSHOT-related issues.

1) SbomPlugin: introduce LICENSE_GROUP_MAPPING fallback (build fix)

The Groovy 6.0.0-SNAPSHOT just bumped its transitive jline pull from
4.0.7 to 4.0.12, which broke `cyclonedxBom` for grails-shell-cli,
grails-console, and grails-dependencies-starter-web with:

  Unpermitted License found for bom dependency:
  pkg:maven/org.jline/jansi@4.0.12?type=jar : BSD-4-Clause

The previous fix added per-version entries for 4.0.7 only. Per-version
entries for an entire dependency group that drifts on every SNAPSHOT
bump is unmaintainable.

Replace the per-version `pkg:maven/org.jline/*` entries with a single
group-level mapping that forces BSD-3-Clause for the whole group. The
fallback kicks in only after the exact-match LICENSE_MAPPING fails, so
existing per-version overrides keep their fast path. Verified locally:

  Forcing license for pkg:maven/org.jline/jansi@4.0.12?type=jar
  to BSD-3-Clause via group rule pkg:maven/org.jline/
  ...
  BUILD SUCCESSFUL in 42s

The criteria for adding a group rule are documented inline (stable
license + cyclonedx-core-java#205 misreport + SNAPSHOT version drift),
so future maintainers know when to extend it and when to stick with
per-version entries.

2) MappingContextAwareConstraintFactory: defensive sibling fix

Architectural review flagged this class as carrying the same
default-valued `List<Class>` constructor parameter that triggered the
Groovy 6 VerifyError in DefaultConstraintFactory. The class itself is
not @CompileStatic, so the bug does not currently fire here, but the
parent constructor it delegates to is, and it is cheaper to apply the
same explicit two-constructor pattern now than to reproduce the same
debugging session if a future Groovy 6 alpha tightens bytecode rules.

3) GormEntityTransformSpec: regression tests for the GROOVY-11829 shim

The original PR added a `get(String)` overload to GormEntity to work
around Groovy 6's relaxed `MetaClassImpl.isGenericGetMethod`, but did
not add focused tests. Architectural review correctly pointed out that
the shim has user-visible behavioral consequences for String-id
entities (e.g. `Book.get("simpleName")` no longer means "load the
entity whose id is the string 'simpleName'") and those need test
coverage so the regression surface is documented and any future change
is caught.

Add three feature methods to GormEntityTransformSpec:

- "test Groovy 6 genericGetMethod regression workaround (GROOVY-11829)"
  asserts the new `get(String)` exists and is @generated alongside the
  original `get(Serializable)`, and that Class bean property access
  (`Book.simpleName`, `Book.name`) still resolves through the
  workaround.
- "test get(String) throws MissingPropertyException when GORM not
  initialized and string is not a Class property" pins the contract
  that genuinely-missing names raise MissingPropertyException, not the
  IllegalStateException that an uninitialised GORM static API would
  otherwise leak.
- "test get(String) returns Class bean property when name matches
  Class property and GORM not initialized" pins the user-visible
  behavior change vs Groovy 5: `Book.get("simpleName")` returns the
  Class.simpleName, not an entity-by-id lookup. The test docstring
  references GormEntity.get(String) and GROOVY-11829 so the trade-off
  is discoverable from the test rather than buried in commit history.

All three new tests pass against Groovy 6.0.0-SNAPSHOT locally:
  ./gradlew :grails-datamapping-core:test \
      --tests "org.grails.compiler.gorm.GormEntityTransformSpec"
  -> 12 tests, 0 failures, BUILD SUCCESSFUL in 36s

Assisted-by: claude-code:claude-opus-4-7
A fresh Groovy 6.0.0-SNAPSHOT pull broke grails-rest-transforms compile:

  Execution failed for task ':grails-rest-transforms:compileGroovy'.
  > Unrecoverable compilation error: startup failed:
    General error during semantic analysis: No signature of method:
    doCall for class: ControllerActionTransformer$1 is applicable for
    argument types: (org.codehaus.groovy.ast.MethodNode) values:
    [org.codehaus.groovy.ast.MethodNode@... index(java.lang.Integer)
    from grails.rest.RestfulController]

The transformer used `DefaultGroovyMethods.count(Iterable, Closure)` with
an inline anonymous Closure subclass that overrode `call(Object)`. Under
Groovy 5 that dispatched via Closure.call(Object) directly. Under Groovy 6
the count helper now goes through MOP `doCall` lookup first, and a Java
inner class overriding `call(Object)` does not advertise a matching
`doCall(MethodNode)`, so dispatch fails at compile time when the AST
transform itself runs against any controller subclass that has typed
overload methods on the supertype (e.g. RestfulController.index(Integer)).

The Closure roundtrip is unnecessary here. Replace it with a plain Java
counting loop. This is shorter, allocates no Closure, removes the
implicit MOP dependency entirely, and works on every Groovy version. The
DefaultGroovyMethods import is no longer used in this file, so remove it
too.

Verified locally:
  ./gradlew :grails-rest-transforms:compileGroovy -PskipCodeStyle
  -> BUILD SUCCESSFUL in 29s

Other `new Closure(this)` sites in the codebase use either no-arg call()
or call(Object...) varargs and were not affected by the new MOP path; if
that changes those should get the same treatment.

Assisted-by: claude-code:claude-opus-4-7
Comment thread build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy Outdated
@bito-code-review

This comment was marked as outdated.

CI surfaced a regression in every Hibernate5 / Functional / Mongodb test
suite that exercised connection-aware entities, all failing with:

  java.lang.IllegalArgumentException: Unknown entity: java.util.LinkedHashMap
    at org.hibernate.internal.SessionImpl.fireDelete(...)
    at AbstractHibernateGormInstanceApi.delete(...)
    at GormStaticApi.delete(GormStaticApi.groovy:536)
    at DataServiceConnectionRoutingSpec.deleteAllFromConnection (line 280)

That stack maps onto the cleanup helper

  DataServiceRoutingProduct."secondary".list().each {
      it."secondary".delete(flush: true)
  }

The class-level `DataServiceRoutingProduct.secondary` was being routed
through the existing GROOVY-11829 workaround on the GormEntity trait
(`static Object get(String nameOrId)`) and correctly returned a
connection-scoped `GormStaticApi`. The instance-level `it.secondary`
however - which should resolve through the entity's
`propertyMissing(String)` to a `DelegatingGormEntityApi` - was finding
the SAME static method as its instance generic-getter under Groovy 6.
Verified directly:

  metaClass.respondsTo(entity, 'get', String) ->
    [public static java.lang.Object DataServiceRoutingProduct.get(java.lang.String)]

So `it.secondary` returned a `GormStaticApi` instead of a
`DelegatingGormEntityApi`. The subsequent `.delete(flush: true)` then
matched `GormStaticApi.delete(D instance)` with the `[flush: true]`
LinkedHashMap cast as `D`, which Hibernate finally rejected at
`session.delete(LinkedHashMap)`.

The same misrouting also explained the secondary failure pattern seen
across CrossLayerMultiDataSourceSpec:

  java.lang.NullPointerException: Cannot invoke
    "org.springframework.validation.Errors.getFieldErrors()"
    because "originalErrors" is null
    at HibernateRuntimeUtils.setupErrorsProperty(...:79)

`it.errors` was being similarly hijacked by the static `get(String)`
on a multi-datasource entity, leaving the `getErrors()` accessor used
by `setupErrorsProperty` returning `null` instead of a real `Errors`.

Fix
---
Drop the trait-level `static Object get(String nameOrId)` and instead
have `GormEntityTransformation` add an INSTANCE `Object get(String name)`
method directly to every `@Entity` class. Its body is a one-line
delegate to the existing `propertyMissing(String)`:

  // generated on every @entity class
  public Object get(String name) { propertyMissing(name) }

Why this works:

  1. Trait-merge no longer rejects the trait. We could not declare BOTH
     `static get(String)` and instance `get(String)` on the trait
     itself - Groovy reports "static and instance methods having the
     same signature". Adding the instance overload via AST keeps it on
     the entity class, where static + instance with the same name and
     params is legal.

  2. Instance dispatch picks the more specific candidate. Because the
     instance method now lives directly on the entity class (not just
     on the trait), Groovy's instance MOP finds it before falling back
     to any trait-static `get(...)` method, so `it.secondary` routes
     through the existing `propertyMissing` and yields the correct
     `DelegatingGormEntityApi`.

  3. Class-level dynamic property access still works. `Class` bean
     properties (`simpleName`, `name`, `canonicalName`, ...) are
     resolved by Groovy's normal Class metaclass before any
     genericGetMethod is consulted, and connection-name lookups like
     `Book.secondary` continue to land on the existing
     `staticPropertyMissing` in GormEntity.

The trait keeps its original `static D get(Serializable id)` (the
public entity-by-id API) untouched.

Tests
-----
Updated `GormEntityTransformSpec` to assert the new shape:
  - the AST-added instance `get(String)` exists and is `@Generated`,
  - it is NOT static,
  - the original `get(Serializable)` is still present.
The earlier tests that documented the old static-overload behaviour
(`Book.get('simpleName') == 'Book'`, etc.) were specific to the
removed shim and have been deleted alongside it.

Verified locally on Groovy 6.0.0-SNAPSHOT:
  ./gradlew :grails-datamapping-core:test \
            :grails-data-hibernate5-core:test \
      --tests 'org.grails.compiler.gorm.GormEntityTransformSpec' \
      --tests 'org.apache.grails.data.testing.tck.tests.Domain*' \
      --tests 'org.apache.grails.data.testing.tck.tests.CrossLayer*' \
      --tests 'org.apache.grails.data.testing.tck.tests.DataService*'
  -> 42 tests, 0 failures, BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Every CI job that compiled GSPs against Groovy 6.0.0-SNAPSHOT failed
with a Groovy compiler stack like:

  General error during instruction selection: Index 3 out of bounds for length 3
  java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at org.codehaus.groovy.util.ListHashMap.toMap(ListHashMap.java:207)
    at org.codehaus.groovy.util.ListHashMap.put(ListHashMap.java:146)
    at java.base/java.util.Map.computeIfAbsent(Map.java:1067)
    at org.codehaus.groovy.ast.NodeMetaDataHandler.getNodeMetaData(NodeMetaDataHandler.java:65)
    at org.codehaus.groovy.ast.AnnotationNode.isTargetAllowed(AnnotationNode.java:168)
    at org.codehaus.groovy.classgen.ExtendedVerifier.visitAnnotations(ExtendedVerifier.java:354)
    at org.codehaus.groovy.classgen.ExtendedVerifier.visitConstructor(ExtendedVerifier.java:216)
  ...
  at org.grails.web.pages.GroovyPageForkedCompiler.main(GroovyPageForkedCompiler.groovy:106)

`AnnotationNode.isTargetAllowed` was added in Groovy 6 (GROOVY-11838) to
honour the new default annotation targets and uses
`NodeMetaDataHandler.getNodeMetaData` (a `Map.computeIfAbsent` over an
internal `ListHashMap`) on shared `Annotation*` AST nodes. That cache
is touched concurrently by the Grails `GroovyPageCompiler` thread pool
(`Executors.newFixedThreadPool(availableProcessors() * 2)`) once shared
annotations like `@Inject`, `@CompileStatic`, etc. are seen by more
than one GSP compile at the same time, which is exactly the case for
test apps that pull in Spring/Grails compiled output. `ListHashMap` is
not designed for concurrent mutation, so the resize fails with an
`ArrayIndexOutOfBoundsException` and the entire GSP compile aborts.

Replace the unconditional `availableProcessors() * 2` thread pool with
a small `computeGspCompilerParallelism()` helper that:

* defaults to 1 worker on Groovy 6 (eliminates the race),
* defaults to `availableProcessors() * 2` on Groovy 5 and earlier
  (preserves prior behaviour),
* honours `-Dgrails.gsp.compiler.parallelism=N` so callers can opt back
  into parallel GSP compilation once Groovy 6 fixes the race (or
  experimentally tune it down on Groovy 5).

Trade-off: a small wall-clock increase on Groovy 6 GSP compilation in
exchange for deterministic behaviour. The control knob is a single
system property, so this is easy to revert once the upstream Groovy
fix is available.

Verified locally:
  ./gradlew :grails-gsp-core:compileGroovy --rerun-tasks -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Removing the obsolete static get(String) Groovy 6 workaround in 8e9cdbc
left a doubled blank line above the read(Serializable) method, which the
Core Projects CI job flagged via the CodeNarc ConsecutiveBlankLines rule:

  GormEntity.groovy:608 - File GormEntity.groovy has consecutive blank lines

Tighten back to a single blank separator. No semantic change.

Verified locally:
  ./gradlew :grails-datamapping-core:codenarcMain :grails-gsp-core:codenarcMain
  -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Per @jdaugherty review on
#15558 (comment):

> This defeats the entire purpose of this plugin. We should not wholesale
> map these. every version has to be checked because at any time a license
> can change. We need to review these individually
>
> FYI: if these are really wrong, we should be pushing upstream on cyclone
> or the jline project itself to fix their licensing.

Both points are correct. The SBOM plugin's value is exactly that each
artifact-version is auditable, and a wholesale group rule erases that
guarantee the moment a transitive bumps onto a new major. Drop the
LICENSE_GROUP_MAPPING map and the matching group-fallback branch in
pickLicense, and go back to per-version entries with explicit
provenance.

Per-version replacements added (each carries the upstream-versioned
LICENSE.txt URL inline so future maintainers can re-verify on the next
SNAPSHOT bump):

  pkg:maven/org.jline/jansi@4.0.12             BSD-3-Clause
  pkg:maven/org.jline/jline@3.30.6             BSD-3-Clause   (direct)
  pkg:maven/org.jline/jline-builtins@4.0.12    BSD-3-Clause
  pkg:maven/org.jline/jline-console@4.0.12     BSD-3-Clause
  pkg:maven/org.jline/jline-console-ui@4.0.12  BSD-3-Clause
  pkg:maven/org.jline/jline-native@4.0.12      BSD-3-Clause
  pkg:maven/org.jline/jline-reader@4.0.12      BSD-3-Clause
  pkg:maven/org.jline/jline-shell@4.0.12       BSD-3-Clause
  pkg:maven/org.jline/jline-style@4.0.12       BSD-3-Clause
  pkg:maven/org.jline/jline-terminal@4.0.12    BSD-3-Clause
  pkg:maven/org.jline/jline-terminal-jni@4.0.12 BSD-3-Clause

Each was verified against
https://github.com/jline/jline3/blob/jline-parent-<version>/LICENSE.txt
which carries the BSD-3-Clause text. The cyclonedx-core-java#205
misclassification (BSD-4-Clause) is the same root issue we have for the
2.14.6 / antlr4 entries.

The 3.30.9 and 4.0.7 entries from the merge with grails8-groovy5-sb4
are dropped because Groovy 6.0.0-SNAPSHOT now resolves the entire
org.jline:* group to 4.0.12 transitively via groovy-groovysh; verified
with `:grails-shell-cli:dependencies --configuration runtimeClasspath`
plus the `Forcing license for ...` log lines on cyclonedxBom. If a
future SNAPSHOT bumps onto a new major (5.x), we add fresh per-version
entries with re-verified provenance, exactly as the SBOM plugin
intends.

Verified locally:
  ./gradlew :grails-shell-cli:cyclonedxBom :grails-console:cyclonedxBom \
            :grails-dependencies-starter-web:cyclonedxBom \
            -PskipCodeStyle --rerun-tasks
  -> BUILD SUCCESSFUL in 1m 56s

Assisted-by: claude-code:claude-opus-4-7
… build

The Build Grails Forge CI jobs have been failing on this PR with:

  CreateControllerCommandSpec > test app with controller FAILED
    Condition not satisfied after 240.00 seconds and 240 attempts
    output.toString().contains(value)
    | false BUILD SUCCESSFUL
    | ...
    | > Task :compileTestGroovy FAILED
    | gradle/actions: Writing build results to ...

We can see compileTestGroovy fails in the generated app, but the actual
compiler error message is not visible anywhere in the CI log. The
PollingConditions assertion only inspects what is captured in `output`,
and `executeCommand` here only consumes the forked Gradle process's
*stdout* (process.consumeProcessOutputStream(output)). Compile-error
diagnostics from groovyc / Spock are written to *stderr* and are
therefore silently dropped on every failed run.

Switch to consumeProcessOutput(stdout, stderr) with the same
StringBuilder for both streams so the next CI run surfaces the actual
compiler error in the assertion failure (and in any future debugging).
This is a test-only change to test infrastructure; production code is
unaffected.

Once the underlying compile failure is identified and fixed, this can
stay (it is the more useful default) or be reverted at the maintainer's
discretion.

Assisted-by: claude-code:claude-opus-4-7
…gradle

The Build Grails Forge CI jobs were failing because the gradle build of
each forge-generated test app aborted at compileTestGroovy with:

  Could not instantiate global transform class
  org.spockframework.compiler.SpockTransform specified at
  jar:.../spock-core-2.4-groovy-5.0.jar!/META-INF/services/...
  because of exception
  org.spockframework.util.IncompatibleGroovyVersionException:
  The Spock compiler plugin cannot execute because Spock 2.4.0-groovy-5.0
  is not compatible with Groovy 6.0.0-SNAPSHOT.

(Captured by the CommandSpec stderr fix in 48598b4 which was
otherwise dropping this diagnostic on the floor.)

The Grails 8 + Groovy 6 canary BOM still pins Spock to 2.4-groovy-5.0
because no Groovy 6-compatible Spock artifact is published yet. Spock's
own version check is purely a guard - the compile itself completes when
the bypass is enabled. The Grails core build does this in
build-logic/.../CompilePlugin and the shared gradle/test-config.gradle.
The generated apps did not have an equivalent, so they failed every
time on this canary.

Add the Spock bypass to the buildGradle.rocker.raw template under the
existing `if (features.contains("spock"))` block, on both:

  - `tasks.withType(GroovyCompile)` via `options.forkOptions.jvmArgs`
    (the AST transform classpath where SpockTransform actually loads),
  - `tasks.withType(Test)` via `systemProperty`
    (the Test JVM where BeanBuilder.loadBeans() and similar compile
    Groovy scripts at runtime).

The flag is a no-op when Spock and Groovy major versions match, so it
is safe to set unconditionally. The inline comment in the template
documents the symptom, the trade-off, and the removal trigger
(grails-bom pinning a Spock artifact whose Groovy major matches
groovy.version).

Existing SpockSpec test still passes (it asserts on
useJUnitPlatform() and the spock-core dependency, both preserved).
Verified the rocker template compiles via:
  ./gradlew :grails-forge-core:generateRockerTemplateSource :grails-forge-core:compileGroovy
  -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
Mongodb Functional Tests (Java 21, MongoDB 7.0, indy=true) failed in
the latest run with the same Groovy 6 ListHashMap thread-safety
regression that the GSP-side fix in ddc7ea2 already addressed,
but now triggered through the views (.gson) compiler:

  > Task :grails-test-examples-hibernate5-grails-data-service:compileGsonViews FAILED
  Exception in thread "main" java.util.concurrent.ExecutionException:
    org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
    General error during instruction selection: Index 3 out of bounds for length 3
    java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
      at org.codehaus.groovy.util.ListHashMap.toMap(ListHashMap.java:207)
      at org.codehaus.groovy.util.ListHashMap.put(ListHashMap.java:146)
      at java.base/java.util.Map.computeIfAbsent(Map.java:1067)
      at org.codehaus.groovy.ast.NodeMetaDataHandler.getNodeMetaData(...)
      at org.codehaus.groovy.ast.AnnotationNode.isTargetAllowed(...)

`AbstractGroovyTemplateCompiler.compile(List<File>)` was using
`Executors.newFixedThreadPool(availableProcessors() * 2)`, the same
historical default as `GroovyPageCompiler`, and the same fix applies:
default parallelism to 1 on Groovy 6 to dodge the race; preserve
`availableProcessors() * 2` on Groovy 5 and earlier; allow opt-back-in
or override via `-Dgrails.views.compiler.parallelism=N`.

Mirrors the GSP-side `computeGspCompilerParallelism()` helper from
ddc7ea2 (`grails.gsp.compiler.parallelism` system property). The
inline comment at the call site documents the symptom, the Groovy
classes involved, the trade-off, and the toggle property.

Verified locally:
  ./gradlew :grails-views-core:compileGroovy --rerun-tasks
  -> BUILD SUCCESSFUL

Assisted-by: claude-code:claude-opus-4-7
… comments

The GROOVY-11829 cross-reference in three places turned out to be the wrong
JIRA: https://issues.apache.org/jira/browse/GROOVY-11829 is "Properties
located from a set(key, value) always use the same method even when the
value type is better matched by another" - resolved 2026-01-01, fix
version 6.0.0-alpha-1, and entirely about set(...) overload selection,
not the get(...) dispatch behaviour we work around in GormEntity.

Re-checked the actual mechanism on apache/groovy master HEAD `f5ab762500`
(committed 2026-04-25 15:06 UTC, 11 minutes before the snapshot we test
with):

  private static boolean isGenericGetMethod(MetaMethod method) {
      if (method.getName().equals("get")) {
          CachedClass[] parameterTypes = method.getParameterTypes();
          return parameterTypes.length == 1
              && parameterTypes[0].getTheClass() == String.class;
      }
      return false;
  }

So the genericGetMethod selection still requires String.class. The
regression we hit was a different one entirely: a trait-static
get(String) is picked up by the *implementing class's* MOP as a
candidate for instance-property generic-getter dispatch, returning a
GormStaticApi where propertyMissing should produce a
DelegatingGormEntityApi. There is no upstream Apache Groovy JIRA we
could find for this dispatch behaviour at the time of writing.

Update the three citations to:

* GormEntity.get(Serializable) docstring: drop the relaxed-isGenericGetMethod
  story (it never happened), describe the actual symptom
  (instance-MOP picking up the trait-static get on @entity classes,
  Hibernate "Unknown entity: java.util.LinkedHashMap"), point at the
  GormEntityTransformation AST shim as the home of the fix, and note
  that no upstream JIRA is filed.

* GormEntityTransformation: same symptom narrative, drop the
  GROOVY-11829 reference, add an explicit "remove this once an upstream
  JIRA is filed and fixed (or once Spock 2.x ships a Groovy 6-compatible
  artifact and we re-validate)" pointer.

* GormEntityTransformSpec: rename the feature method to
  "test Groovy 6 generic-getter instance-dispatch guard" (no JIRA in
  the title) and rewrite the docstring to match.

Verified locally:
  ./gradlew :grails-datamapping-core:test \
            --tests 'org.grails.compiler.gorm.GormEntityTransformSpec'
  -> 9 tests, 0 failures, BUILD SUCCESSFUL
  ./gradlew :grails-datamapping-core:codenarcMain \
            :grails-datamapping-core:codenarcTest
  -> BUILD SUCCESSFUL

No production-code behaviour changed; this is purely the comment /
docstring / spec-method-name cleanup pass.

Assisted-by: claude-code:claude-opus-4-7
@jamesfredley

This comment was marked as outdated.

@jamesfredley

This comment was marked as outdated.

@jamesfredley
Copy link
Copy Markdown
Contributor Author

Final wrap-up: Groovy 5 / Groovy 6 workaround audit complete

This is the final state of the workaround audit after end-to-end integration testing against Apache Groovy 6.0.0-SNAPSHOT master HEAD (build 508-516; verified that builds 509-516 add only Javadoc + 1 build-infra refactor on top of build 508 - so no functional delta vs. master HEAD).

Bottom line

5 Groovy 5 workarounds removed in this audit. 7 confirmed required. 2 will be removable when 2 OPEN upstream PRs merge. 1 is Spring 7 specific. 3 untestable locally (Forge canary red for unrelated reasons).

Methodology recap

  1. Read every // Groovy 5 and // GROOVY- comment in the working tree
  2. For each, attempted removal and ran the affected module's test suite on Groovy 6.0.0-SNAPSHOT build 508
  3. Verified MongoDB integration tests with live mongo:7.0 TestContainer (478 tests passed with BsonPersistentEntityCodec.resolvePropertyType() walker removed)
  4. Verified Geb indy=false integration tests with selenium/standalone-chrome:latest TestContainer (reproduced VerifyError: get long/double overflows locals on ContainerSupport$Trait$Helper.createFileInputSource @0: dload_3 when @CompileDynamic reverted - the workaround stays)
  5. Cloned apache/groovy master, fetched OPEN PRs GRAILS-10803: Can't see chinese in log console in 2.3.x #2492 and GRAILS-6219: Under certain circumstances the DefaultGrailsDomainClass's metaClass is a MetaClassImpl, not an ExpandoMetaClass #2493, built each locally with ./gradlew publishToMavenLocal, replaced cached SNAPSHOT JARs, reverted the corresponding Grails workaround, and re-ran the affected tests to confirm both upstream PRs unblock our workarounds

Verified upstream PRs that unblock our remaining workarounds (NOT yet merged to master)

Upstream Local verification Grails workaround it unblocks
OPEN PR apache/groovy#2492 (GROOVY-11966, Paul King) - synchronises NodeMetaDataHandler.getNodeMetaData map access Built locally, replaced cache JAR (NodeMetaDataHandler monitorenter count went from 0 to 7), reverted both parallelism guards, ran 8 parallel :grails-test-examples-*:compileGroovyPages - BUILD SUCCESSFUL, 0 ListHashMap errors. Race only reliably reproduces in CI (timing-dependent) so we couldn't reliably reproduce the negative case locally, but the synchronisation is a textbook fix for the documented ArrayIndexOutOfBoundsException race grails-gsp/core/.../GroovyPageCompiler.groovy parallelism guard + grails-views-core/.../AbstractGroovyTemplateCompiler.groovy parallelism guard
OPEN PR apache/groovy#2493 (GROOVY-11967, Paul King) - adds CHECKCAST to indy-mode ListExpressionTransformer to fix the VerifyError on the synthesised lower-arity bridge constructor Built locally, replaced cache JAR (verified InvokeDynamicWriter reference now in ListExpressionTransformer$NewListExpression.class), reverted DefaultConstraintFactory + MappingContextAwareConstraintFactory back to single-constructor form with targetTypes = [Object] as List<Class> default value, ran :grails-validation:test :grails-datamapping-validation:test under both indy=false and indy=true - BUILD SUCCESSFUL grails-datamapping-validation/.../DefaultConstraintFactory.groovy two-ctor split + grails-datamapping-core/.../MappingContextAwareConstraintFactory.groovy two-ctor split

Verified upstream JIRAs already in build 508+ master HEAD

JIRA Status Fix commit Already-removed Grails workaround
GROOVY-11907 "trait static field helper generates invalid bytecode" Resolved (5.0.6) 19f38997a (2026-04-08) HibernateEntity static SQL methods (commit 8af5d1dc4c), JspTagImpl @CompileDynamic (commit 2bb0930a5d), ClassPropertyFetcherTests generic trait (commit a71c8b5ebb), GormEntityTransformation AST shim path (now unconditional, commit 8e9cdbc50f), MongoCodecSession increment, scaffolding GROOVY-11907 trait statics (commit a290b37156). Note: indy=false static-setter helper path is NOT covered - reproduced today on ContainerSupport, needs an upstream follow-up filed
GROOVY-11911 "Restore Groovy 5's MOP-aware call dispatch for Java Closure subclasses overriding call(Object) without doCall" Resolved (master) ac71deb (2026-04-26 07:19 UTC, in build 508) ControllerActionTransformer Closure dispatch workaround (REMOVED in this audit) - reverted Java for-loop back to DefaultGroovyMethods.count(Iterable, Closure) form, all 133 :grails-controllers:test :grails-rest-transforms:test tasks green
GROOVY-11512 "Inconsistent isAttribute & getAttribute behavior in Groovy 4 with traits" Resolved (4.0.28 / 5.0.0-beta-2 / 6.0.0-alpha) 88c63360 (2024-11-01) TraitPropertyAccessStrategy (inherited from base PR #15557, not retested in this canary)
GROOVY-11829 "Properties located from a set(key, value) always use the same method even when the value type is better matched by another" Resolved (6.0.0-alpha-1) 7bc29825bc (2026-01-01) This addresses set(...) not get(...) - it does NOT address our MetaClassImpl.isGenericGetMethod instance-dispatch hijack, which is why our GormEntityTransformation instance get(String) shim is still required
GROOVY-11522 "Possible Null Pointer Dereference in VariableScopeVisitor" Resolved (4.0.28 / 5.0.0-beta-2 / 3.0.26) f5666584e1 (2026-02-24) This addresses a different findClassMember NPE - it does NOT address our 4 visitConstructorOrMethod NPE catches, which still reproduce on master HEAD when removed. Our NPE class needs a separate upstream filing

Issues that NEED to be filed upstream (no matching JIRA / PR found)

Issue Caused workaround
MetaClassImpl static-trait get(String) hijacking instance dispatch (distinct from GROOVY-11829 which addresses set(key, value)) GormEntityTransformation instance get(String) shim
Our specific VariableScopeVisitor.visitConstructorOrMethod NPE class (4 catch sites + 2 null-VariableScope ClosureWriter sites; one umbrella bug family per Oracle review) GrailsASTUtils, AstUtils, AbstractMethodDecoratingTransformation, ResourceTransform, LoggingTransformer
@Delegate field on trait silently returns null on Groovy 5/6 lowering (separate from GROOVY-11512) GrailsApplicationCommand trait → abstract class
TraitReceiverTransformer static override loss when calling this.method() from a trait static method Validateable.resolveDefaultNullable reflection lookup
ConfigObject infinite recursion under Map iteration (Groovy 4 → 5 behavior change carried into 6, no JIRA filed since 2014) NavigableMap.convertConfigObjectToMap() shallow + lazy conversion
Interface $getCallSiteArray() IncompatibleClassChangeError under indy=false IContainerGebConfiguration interface → trait
GROOVY-11907 follow-up: indy=false static-setter trait helper bytecode (verified today: dload_3 overflow on 2-local frame) ContainerSupport @CompileDynamic

Forge integration tests (3 workarounds remaining there)

GrailsApplicationCommand (trait → abstract class), TemplateRendererImpl (explicit type checks), and GenerateControllerCommand (explicit 4-arg render calls) all compile cleanly when reverted. Their failure mode is silent runtime @Delegate returning null, only catchable by Forge ScaffoldingSpec.test generate-controller command integration test. Build Grails Forge is currently red on this branch's CI for an unrelated compileTestGroovy failure (was already red on the previous canary CI run before any of these fixes), so we cannot get a clean signal. These workarounds stay until Forge canary goes green.

Net delta

  • Working tree: 14 files changed, 5 source workarounds removed (-128 / +51 lines), 9 inline comment updates from "Groovy 5" → "Groovy 5/6" with reproducer details on the workarounds confirmed still required.
  • All 530 modules compile clean: ./gradlew classes -> BUILD SUCCESSFUL in 2m 10s.
  • No commit yet - waiting for review.

Assisted-by: claude-code:claude-opus-4-7

@jamesfredley
Copy link
Copy Markdown
Contributor Author

Final wrap-up: workaround burndown audit complete

After end-to-end testing against apache/groovy master HEAD (build 508+; verified == master HEAD modulo Javadoc commits), here's the bottom line.

Burndown count

  • 6 Groovy 5 workarounds REMOVED in this audit (verified by full module tests)
  • 4 outstanding workarounds gated on 2 OPEN upstream PRs (apache/groovy#2492 and #2493 by Paul King) - locally verified that those PRs unblock the corresponding Grails workarounds
  • 13 outstanding workarounds need to be filed upstream - each has a reproducer test that fails on master HEAD when reverted
  • 1 inherited from base PR removed (TraitPropertyAccessStrategy is now restored to pre-Groovy-5 form since GROOVY-11512 is fixed in 6.0.0-alpha)

Verified upstream PRs unblock our remaining workarounds

Upstream Verification
GROOVY-11966 / OPEN PR apache/groovy#2492 synchronises NodeMetaDataHandler.getNodeMetaData map access Cloned apache/groovy, fetched PR branch, ran ./gradlew publishToMavenLocal -x test -x check -x javadoc -x groovydoc -x asciidoctor, replaced cached 6.0.0-SNAPSHOT JARs (verified monitorenter count in NodeMetaDataHandler went from 0 to 7), reverted both parallelism guards, ran 8-project parallel :grails-test-examples-*:compileGroovyPages - clean. Original race only reliably reproduces in CI (timing-dependent).
GROOVY-11967 / OPEN PR apache/groovy#2493 adds CHECKCAST to indy-mode ListExpressionTransformer Built PR #2493 locally, replaced cached snapshot (verified InvokeDynamicWriter reference present in ListExpressionTransformer$NewListExpression), reverted DefaultConstraintFactory + MappingContextAwareConstraintFactory back to single-constructor form, ran :grails-validation:test :grails-datamapping-validation:test under both indy=true and indy=false - clean.

Verified upstream JIRAs already in master and removed our workaround

  • GROOVY-11512 (trait boolean property generates isser and getter) - resolved in 6.0.0-alpha. Removed TraitPropertyAccessStrategy is-prefix fallback, verified :grails-data-hibernate5-core:test --rerun-tasks 79/79 green.
  • GROOVY-11829 (set(key, value) method selection) - resolved in 6.0.0-alpha. NOT our get(String) hijack issue (those are different code paths) so our GormEntityTransformation AST shim is still required.
  • GROOVY-11907 (trait static field bytecode) - resolved in 5.0.6. Most workarounds removed in earlier commits. ContainerSupport indy=false static-setter helper path is NOT covered by this fix - verified today with reproducible VerifyError: get long/double overflows locals at ContainerSupport$Trait$Helper.createFileInputSource @0: dload_3 under :grails-test-examples-geb:integrationTest -PgrailsIndy=false. Needs a follow-up filed.
  • GROOVY-11911 (count(Iterable, Closure) MOP doCall) - resolved in master, in build 508+. Removed ControllerActionTransformer Closure dispatch workaround, restored to original DefaultGroovyMethods.count(...) form, verified 133/133 tasks green.
  • GROOVY-11522 (VariableScopeVisitor NPE in findClassMember) - resolved in 4.0.28 / 5.0.0-beta-2. NOT our visitConstructorOrMethod NPE class (different code path, same file).

Issues to file upstream (no matching JIRA / PR found in apache/groovy master commit log or JIRA)

The 7 upstream issues that still need to be filed:

  1. MetaClassImpl static-trait get(String) hijacking instance dispatch (caused GormEntityTransformation AST shim - distinct from GROOVY-11829's set(key, value) fix)
  2. VariableScopeVisitor.visitConstructorOrMethod NPE class (4 catch sites + 2 null-VariableScope ClosureWriter sites - one umbrella bug family)
  3. @Delegate field on trait silently returns null on Groovy 5/6 lowering (caused GrailsApplicationCommand trait → abstract class)
  4. @Delegate named-arg bridge silently corrupts template name in render(Map) (verified today: Template [Controller.groovy]] not found - caused TemplateRendererImpl + GenerateControllerCommand workarounds)
  5. TraitReceiverTransformer static override loss when calling this.method() from a trait static method (caused Validateable.resolveDefaultNullable reflection)
  6. ConfigObject infinite recursion under Map iteration (caused NavigableMap.convertConfigObjectToMap shallow conversion - latest ConfigObject commits in master are 2012-2014)
  7. Interface $getCallSiteArray() IncompatibleClassChangeError under indy=false (caused IContainerGebConfiguration interface → trait)

Plus: GROOVY-11907 follow-up for the indy=false static-setter trait helper bytecode (caused ContainerSupport @CompileDynamic - reproduced today).

Net change

15 files modified, -135/+51 lines, 6 Groovy 5 workarounds removed. All 530 modules compile clean. Forge ScaffoldingSpec.test generate-controller command passes with the workarounds in place.

Assisted-by: claude-code:claude-opus-4-7

@jamesfredley jamesfredley force-pushed the grails8-groovy6-canary branch from 2bd7667 to 3f485f3 Compare April 27, 2026 16:20
Reproducer at https://github.com/jamesfredley/groovy6-get-as-generic-getter isolates the actual Groovy 6 MOP regression to four small files (no Grails, no GORM, no Hibernate). Updates the inline comment to point at the upstream bug (Groovy 6 picks the inherited Object get(Serializable) as the genericGetMethod for instance property access) rather than the previous 'no upstream JIRA identified' framing - the reproducer narrows it down to a specific apache/groovy MOP behaviour change between 5.0.6-SNAPSHOT and 6.0.0-SNAPSHOT.
…rmer regression

Reproducer at https://github.com/jamesfredley/groovy-trait-static-method-override-bug isolates the trait static override hijacking to three small files. Confirms the regression is identical on Groovy 5.0.6-SNAPSHOT and 6.0.0-SNAPSHOT (passes on Groovy 4.0.31). Updates the inline javadoc on resolveDefaultNullable accordingly and points future maintainers at the upstream reproducer.
…ender(Map) note

# Conflicts:
#	grails-validation/src/main/groovy/grails/validation/Validateable.groovy
…e reproducer

Verified on absolute-latest Groovy 6.0.0-SNAPSHOT build #518 (2026-04-27 14:33 UTC) that the @CompileStatic + trait-static-field + indy=false VerifyError still reproduces. Apache Groovy PR #2495 (GROOVY-11968) by @paulk-asert is the explicit follow-up to GROOVY-11907 that should fix it; opened 2026-04-27, currently OPEN. The ContainerSupport @CompileDynamic shim should be reverted once that PR merges and a fresh snapshot publishes.
@jamesfredley
Copy link
Copy Markdown
Contributor Author

Audit pass against Groovy 6.0.0-SNAPSHOT build #518 (2026-04-27)

Pulled the latest snapshot from Apache snapshots (build #518, timestamp 2026-04-27 14:33:02 UTC; tracks apache/groovy master HEAD at 2026-04-27 15:50 UTC modulo CI lag) and re-verified every Groovy 6 workaround on this branch. Inherited the Groovy 5 audit results from #15557 via merge.

Workarounds with confirmed upstream fix in flight

Workaround Upstream PR / JIRA
GSP compile parallelism guard (GroovyPageCompiler, AbstractGroovyTemplateCompiler) apache/groovy#2492 (GROOVY-11966)
DefaultConstraintFactory / MappingContextAwareConstraintFactory two-constructor split apache/groovy#2493 (GROOVY-11967)
ContainerSupport @CompileDynamic (trait static fields under indy=false) apache/groovy#2495 (GROOVY-11968) - newly opened by @paulk-asert today, explicit GROOVY-11907 follow-up

All three are OPEN as of build #518; bug confirmed still present. When each merges + a fresh snapshot publishes, the corresponding workaround can be reverted.

Standalone reproducers published for the four real Groovy 6 regressions still needing upstream filing

# Reproducer repo What it isolates
1 groovy6-get-as-generic-getter Groovy 6 MetaClassImpl picks up Object get(Serializable) as the genericGetMethod for instance property access, hijacking propertyMissing(String). Drives the GormEntityTransformation per-entity AST Object get(String) shim.
2 groovy-trait-static-method-override-bug Groovy 5+ TraitReceiverTransformer rewrites this.someStatic() from inside a trait body to call the trait helper directly, silently losing implementing-class overrides. Drives Validateable.resolveDefaultNullable(Class) reflection workaround.
3 groovy5-compiledynamic-trait-bug Groovy 5+ @CompileStatic render(Map<String,Object>) overload silently no-ops against multi-overload interface references. Drives the typed positional call shape in GenerateControllerCommand and TemplateRendererImpl. (Despite the repo name, also covers Groovy 6 with the same shape and outcome.)
4 groovy5-compiledynamic-trait-bug/quick-checks/InterfaceDefaultsCheck.groovy Interface with default methods compiled with $getCallSiteArray() -> IncompatibleClassChangeError under indy=false. Drives the IContainerGebConfiguration interface->trait conversion.

Each repo has a self-contained build, README pinned to Java 21 + Gradle 9.4.1, and toggles for Groovy 4/5/6 + indy=true/false. Reverting any of the corresponding Grails workarounds and re-running the related test on this branch reproduces the cited failure.

Inherited-from-#15557 workarounds re-verified on Groovy 6

  • PersistentEntityCodec smart-cast workaround (SmartCastCheck.groovy) - still needed
  • NavigableMap.resolveConfigMapValue containsKey + get fix - still needed
  • VariableScopeVisitor try/catch guards (4 sites) - still needed
  • ResourceTransform non-null VariableScope guard - still needed
  • @Slf4j LoggingTransformer was just a comment update - reverted

Removed since Groovy 5 (Groovy 6 fixed them)

  • AbstractConstraint.java getDefaultMessageFromBundle fallback
  • GroovyConfigPropertySourceLoader.toRegularMap
  • HibernateEntityTransformation instanceof InnerClassNode swap
  • ControllerActionTransformer count overload (GROOVY-11911 merged 2026-04-26)
  • BsonPersistentEntityCodec.resolvePropertyType hierarchy walker
  • TraitPropertyAccessStrategy is-prefix fallback (GROOVY-11512 in 6.0.0-alpha)

Net effect

Workaround surface area on this canary is now:

cc @paulk-asert - the four "no upstream PR yet" reproducers (#1-#4 above) are all small, deterministic, and don't pull in Grails or GORM. Each one would benefit from upstream eyes; happy to file the JIRAs and link the reproducers from there if that helps.

The PR description has the full per-site inventory.

James Fredley added 2 commits May 2, 2026 08:27
…8-groovy6-canary

# Conflicts:
#	dependencies.gradle
#	grails-geb/src/testFixtures/groovy/grails/plugin/geb/support/ContainerSupport.groovy
Re-audited every Groovy 6 workaround in this canary against the latest
Apache Groovy master. Three fixes have merged and are present in the
6.0.0-SNAPSHOT artifact (latest publication: 2026-05-02 11:47:43 UTC,
build #546), so the corresponding workarounds can be removed.

GROOVY-11968 (apache/groovy#2495), merged 2026-05-01 03:40 UTC,
SHA 84f2f37c4f93d6ea44ad8bc76570704c84499c6b
  - grails-geb/.../ContainerSupport.groovy: revert @CompileDynamic to
    @CompileStatic now that the trait-static-field VerifyError under
    indy=false no longer triggers.

GROOVY-11967 (apache/groovy#2493), merged 2026-05-01 09:37 UTC,
SHA 406feaf5082f1741c318f924b520c4c27bfa0754
  - DefaultConstraintFactory.groovy: collapse the two explicit
    constructors back to a single constructor with a default-valued
    List parameter; the @CompileStatic VerifyError on the synthesised
    bridge constructor no longer reproduces.
  - MappingContextAwareConstraintFactory.groovy: same collapse.

GROOVY-11966 (apache/groovy#2492), merged 2026-05-01 18:58 UTC,
SHA 8dde1c84134ef6fdeecf26b5cbb5183d5aab4dac
  - GroovyPageCompiler.groovy: drop the parallelism guard and the
    grails.gsp.compiler.parallelism system property; restore the
    original Executors.newFixedThreadPool(availableProcessors() * 2)
    sizing now that AnnotationNode.isTargetAllowed -> ListHashMap is
    thread-safe again.
  - AbstractGroovyTemplateCompiler.groovy: same restoration; drop the
    grails.views.compiler.parallelism system property.

Verified locally on Java 21 / Groovy 6.0.0-SNAPSHOT build #546:
  ./gradlew :grails-datamapping-validation:compileGroovy
  ./gradlew :grails-datamapping-core:compileGroovy
  ./gradlew :grails-gsp-core:compileGroovy
  ./gradlew :grails-views-core:compileGroovy
  ./gradlew :grails-geb:compileTestFixturesGroovy
  -> all BUILD SUCCESSFUL

The remaining workarounds (TraitReceiverTransformer static-method
override loss, MetaClassImpl genericGetMethod hijack on GORM entities,
@CompileStatic named-argument render(Map) silent no-op, smart-cast
in 'if (cond && !(x instanceof Y))', VariableScopeVisitor NPE, and
ConfigObject [] mutation) have no upstream fix yet and stay in place.
@jamesfredley
Copy link
Copy Markdown
Contributor Author

Re-audit 2026-05-02 against Groovy 6.0.0-SNAPSHOT build #546

Pulled latest grails8-groovy5-sb4 into the canary, resolved the two merge conflicts (dependencies.gradle keeping groovy.version: 6.0.0-SNAPSHOT while picking up base's new hibernate-groovy-proxy / jakarta-servlet-api / jakarta-validation / junit / selenium 4.38.0 entries; ContainerSupport.groovy re-evaluated below), then re-checked every workaround against apache/groovy master HEAD 4654b1134b5c3f5d7a8277f150f16ae430521ad9 (2026-05-02 12:26 UTC) and the freshly-published 6.0.0-SNAPSHOT build #546 (2026-05-02 11:47 UTC).

Headline finding

The three upstream PRs that the previous audit flagged as OPEN all merged on 2026-05-01, after the previous canary baseline (build #518, 2026-04-27) was cut. Build #546 contains all three fixes:

JIRA apache/groovy PR Merged Merge SHA
GROOVY-11968 #2495 2026-05-01 03:40 UTC 84f2f37c4f93d6ea44ad8bc76570704c84499c6b
GROOVY-11967 #2493 2026-05-01 09:37 UTC 406feaf5082f1741c318f924b520c4c27bfa0754
GROOVY-11966 #2492 2026-05-01 18:58 UTC 8dde1c84134ef6fdeecf26b5cbb5183d5aab4dac

Workarounds removed in this push (5 sites, 1 commit: 2a5e983555)

  • grails-geb/.../testFixtures/grails/plugin/geb/support/ContainerSupport.groovy - reverted from @CompileDynamic to @CompileStatic (GROOVY-11968 fix). The Trait$Helper static-setter VerifyError under grailsIndy=false no longer reproduces.
  • grails-datamapping-validation/.../DefaultConstraintFactory.groovy - collapsed the two-constructor split back to a single constructor with List<Class> targetTypes = [Object] (GROOVY-11967 fix).
  • grails-datamapping-core/.../MappingContextAwareConstraintFactory.groovy - same collapse.
  • grails-gsp/core/.../GroovyPageCompiler.groovy - dropped the parallelism guard, the grails.gsp.compiler.parallelism system property, the computeGspCompilerParallelism() helper and the isGroovy6OrLater() runtime probe; restored the original Executors.newFixedThreadPool(availableProcessors() * 2) and the matching collationLevel (GROOVY-11966 fix). 60 lines deleted.
  • grails-views-core/.../AbstractGroovyTemplateCompiler.groovy - same restoration; dropped the grails.views.compiler.parallelism system property. 59 lines deleted.

Net diff: 5 files changed, 7 insertions(+), 146 deletions(-).

Local verification

Compiled all five touched modules under Java 21 / Groovy 6.0.0-SNAPSHOT build #546:

./gradlew :grails-datamapping-validation:compileGroovy   -> BUILD SUCCESSFUL
./gradlew :grails-datamapping-core:compileGroovy         -> BUILD SUCCESSFUL
./gradlew :grails-gsp-core:compileGroovy                 -> BUILD SUCCESSFUL
./gradlew :grails-views-core:compileGroovy               -> BUILD SUCCESSFUL
./gradlew :grails-geb:compileTestFixturesGroovy          -> BUILD SUCCESSFUL

Runtime validation (parallel GSP / GSON template compilation surfacing the ListHashMap.toMap race; ContainerGebSpec class-init under grailsIndy=false exercising the trait-static-field VerifyError; validator constraint construction exercising the lower-arity bridge constructor VerifyError) is deferred to the canary CI matrix on this push.

Workarounds re-evaluated and kept (no new upstream fix yet)

Each was checked against the same build #546 / master HEAD; none has an upstream merge:

  1. grails-datamapping-core/.../GormEntityTransformation.groovy - per-entity AST Object get(String) shim. MetaClassImpl genericGetMethod hijack on GORM entities. Reproducer: https://github.com/jamesfredley/groovy6-get-as-generic-getter.
  2. grails-validation/.../Validateable.groovy - resolveDefaultNullable(Class) reflection dispatch. TraitReceiverTransformer static-method override loss. Reproducer: https://github.com/jamesfredley/groovy-trait-static-method-override-bug.
  3. grails-core/.../template/TemplateRendererImpl.groovy + grails-scaffolding/.../GenerateControllerCommand.groovy - typed positional render(Resource, File, Map, boolean) instead of named-argument render(Map). @CompileStatic overload resolution silent no-op on render(Map). Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug.
  4. grails-geb/.../testFixtures/grails/plugin/geb/ContainerGebConfiguration.groovy - IContainerGebConfiguration as trait rather than interface. Interface $getCallSiteArray() IncompatibleClassChangeError under grailsIndy=false. Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/blob/main/quick-checks/src/main/groovy/InterfaceDefaultsCheck.groovy.
  5. grails-data-mongodb/core/.../PersistentEntityCodec.groovy - two ManyToMany.isAssignableFrom(...) swaps. @CompileStatic smart-cast bug in the else branch of if (cond && !(x instanceof Y)). Reproducer: https://github.com/jamesfredley/groovy5-compiledynamic-trait-bug/blob/main/quick-checks/src/main/groovy/SmartCastCheck.groovy.
  6. grails-bootstrap/.../NavigableMap.groovy - containsKey + get instead of [] operator. ConfigObject [] operator mutates on missing-key reads.
  7. grails-core/.../GrailsASTUtils.java, grails-datastore-core/.../AstUtils.groovy, grails-datamapping-core/.../AbstractMethodDecoratingTransformation.groovy, grails-rest-transforms/.../ResourceTransform.groovy - try/catch around VariableScopeVisitor plus non-null VariableScope guard on ClosureExpression. VariableScopeVisitor NPE during canonicalisation on certain Grails AST transformation outputs.

The four "no upstream PR yet" rows still need to be filed against apache/groovy with the standalone reproducers above.

Heads-up: unrelated snapshot drift in build #546

While running local builds I hit one failure that is not caused by these removals and that I want to flag separately:

> Task :grails-data-hibernate5-core:compileGroovy
.../HibernateConnectionSourceSettings.java:89: error: clone() in HibernateSettings cannot override clone() in HashMap
@groovy.transform.Generated() public  ...HibernateSettings clone() throws java.lang.CloneNotSupportedException { return null; }
overridden method does not throw CloneNotSupportedException

The Groovy 6 stub generator now emits throws java.lang.CloneNotSupportedException on the @Generated clone() override of a class that extends LinkedHashMap<String, String>, but LinkedHashMap.clone() doesn't declare that exception, so javac rejects the stub. Confirmed by stashing my workaround removals: the failure reproduces against the merged state without my edits, so it's a separate Groovy 6 SNAPSHOT regression that arrived between build #518 and build #546, not a side effect of dropping these workarounds. Filing this as its own upstream issue is the right next step (likely a sibling of GROOVY-11823 / the @Generated-overrides-checked-exceptions family); for now grails-data-hibernate5-core will fail until the Groovy team revisits it or we add a narrowly-scoped workaround there.

Files

Apache Groovy 6.0.0-SNAPSHOT build #546 (and onward, until upstream
fixes it) regresses the Java stub generator: when @AutoClone is applied
to a class that extends a JDK type whose clone() override drops the
`throws CloneNotSupportedException` clause (LinkedHashMap.clone() is
the canonical example), the generated stub still emits

  @groovy.transform.Generated() public ... HibernateSettings clone()
      throws java.lang.CloneNotSupportedException { return null; }

and javac rejects it because the parent LinkedHashMap.clone() doesn't
declare that exception. CI was failing the entire 'Core Projects' job
on grails-data-hibernate5-core:compileGroovy with:

  HibernateConnectionSourceSettings.java:89: error:
    clone() in HibernateSettings cannot override clone() in HashMap
    overridden method does not throw CloneNotSupportedException

The fix is to define clone() explicitly. @AutoClone short-circuits its
own clone() generation when the user already provides one, so the stub
generator emits a stub matching this user-defined no-throws signature.
Tested @AutoClone(style = COPY_CONSTRUCTOR) first - same stub still
emitted, confirming the regression is in the stub generator and is
independent of the @AutoClone style.

The body mirrors what @AutoClone(style = CLONE) used to produce - a
shallow LinkedHashMap.clone() followed by deep-cloning of the Cloneable
typed fields (osiv, cache, flush, additionalProperties) - so multi-tenant
settings cloning in HibernateDatastore.createTenantConnectionSource
(line 597, getSettings().clone()) keeps the same isolation properties
it had on Groovy 5 and earlier Groovy 6 snapshots.

Verified locally on Java 21 / Groovy 6.0.0-SNAPSHOT build #546:
  ./gradlew :grails-data-hibernate5-core:compileGroovy --rerun-tasks
    -> BUILD SUCCESSFUL
  ./gradlew :grails-data-hibernate5-core:codeStyle
    -> BUILD SUCCESSFUL
  ./gradlew :grails-data-hibernate5-core:test --tests \
      'org.grails.orm.hibernate.connections.HibernateConnectionSourceSettingsSpec'
    -> 1 tests, 1 successes, 0 failures

This is a separate Groovy 6 regression, not caused by the workaround
removals in 2a5e983. Confirmed by stashing those removals and
reproducing the same failure on the unmodified merge state. Filing
upstream against apache/groovy is the next step; revert this commit
once the stub-generator fix lands and a fresh snapshot publishes.
@jamesfredley
Copy link
Copy Markdown
Contributor Author

CI fix - 2026-05-02 follow-up

The Core Projects job was red on the previous push (2a5e983555), but not because of the workaround removals: the failure reproduces against the unmodified merged tree (verified locally by stashing the audit edits). Root cause is a separate Groovy 6.0.0-SNAPSHOT regression that arrived between build #518 and build #546 - the same one I flagged in the previous comment.

Symptom

> Task :grails-data-hibernate5-core:compileGroovy FAILED
HibernateConnectionSourceSettings.java:89: error: clone() in HibernateSettings cannot override clone() in HashMap
@groovy.transform.Generated() public  ...HibernateSettings clone() throws java.lang.CloneNotSupportedException { return null; }
overridden method does not throw CloneNotSupportedException

Root cause

Groovy 6's Java stub generator now unconditionally emits throws java.lang.CloneNotSupportedException on the @Generated clone() stub for any @AutoClone target, even when the actual parent's clone() doesn't declare that exception. HibernateSettings extends LinkedHashMap<String, String> and LinkedHashMap.clone() drops the throws clause, so javac rejects the stub as not a valid override.

I confirmed by trying @AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR) first - same broken stub, same javac error - so this is in the stub generator, independent of the @AutoClone style.

Fix in 4a518983a2

Defined clone() explicitly on HibernateSettings. @AutoClone short-circuits its own clone() generation when a user-supplied clone() exists, so the stub generator emits a stub matching the user-defined no-throws signature. Body mirrors @AutoClone(style = CLONE) to preserve deep-clone of the typed nested settings (OsivSettings, CacheSettings, FlushSettings, additionalProperties) - this matters for the getSettings().clone() call at HibernateDatastore.java:597 in createTenantConnectionSource, which expects multi-tenant settings instances to be properly isolated rather than sharing nested mutable state.

Local verification on Java 21 / Groovy 6.0.0-SNAPSHOT build #546

./gradlew :grails-data-hibernate5-core:compileGroovy --rerun-tasks  -> BUILD SUCCESSFUL
./gradlew :grails-data-hibernate5-core:codeStyle                    -> BUILD SUCCESSFUL
./gradlew :grails-data-hibernate5-core:test --tests \
    'org.grails.orm.hibernate.connections.HibernateConnectionSourceSettingsSpec'
                                                                    -> 1 tests, 1 successes, 0 failures

Code style status

./gradlew codeStyle was already green on the 5 modules touched by 2a5e983555 (datamapping-validation, datamapping-core, gsp-core, views-core, geb). Re-ran on grails-data-hibernate5-core after this fix - also green. The CI failure was pure compile-error, not a code-style violation.

Next steps

This stub-generator regression should be filed upstream against apache/groovy with a minimal reproducer (Groovy class with @AutoClone + extends LinkedHashMap + a Java consumer in the same compilation unit). The workaround in HibernateConnectionSourceSettings.groovy is documented inline and explicitly marked removable once upstream lands the stub-generator fix.

PR description updated to add this entry to "Real Groovy 6 regressions, no upstream PR yet (need to be filed)".

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
@jamesfredley
Copy link
Copy Markdown
Contributor Author

2026-05-04 audit pass against Groovy 6.0.0-SNAPSHOT build #571

Pulled apache/groovy master to commit 40499016 (HEAD as of 2026-05-03 18:03 UTC) and the published 6.0.0-SNAPSHOT snapshot at build #571 (6.0.0-20260503.181740-571). Three new master commits unlock additional workaround removals (two on this canary, two more inherited via the merge from grails8-groovy5-sb4).

New Groovy 6 fixes since the previous audit (build #546#571)

Ticket Master commit (UTC) Workaround dropped on this canary
GROOVY-11980 ced726ce (2026-05-02 21:29) HibernateConnectionSourceSettings.HibernateSettings.clone() explicit override removed - @AutoClone(style = CLONE) on a LinkedHashMap-extending class now generates the override with the correct (no-throws) signature.
GROOVY-11982 88ca738c (2026-05-02 23:16) IContainerGebConfiguration reverted from trait back to interface with default methods. The downstream IncompatibleClassChangeError: Method '...$getCallSiteArray()' must be InterfaceMethodref constant under -PgrailsIndy=false is fixed at the bytecode level.
GROOVY-11983 af95d66d (2026-05-03 01:25) Inherited from grails8-groovy5-sb4 via the merge: PersistentEntityCodec two ManyToMany.isAssignableFrom swaps + DefaultHalViewHelper ToOne-first/ToMany-second cascade reorder, both reverted to natural instanceof form.

Local compilation against build #571 (Java 21):

./gradlew :grails-data-hibernate5-core:compileGroovy --refresh-dependencies
./gradlew :grails-geb:compileTestFixturesGroovy --refresh-dependencies

Both BUILD SUCCESSFUL.

Runtime validation deferred to CI

The GROOVY-11982 fix is at the bytecode Methodref vs InterfaceMethodref constant-pool emission layer. The compile path is happy on both old and new bytecode shapes - the failure is at class-loading time when a downstream class compiled with -PgrailsIndy=false consumes the interface. The affected specs (InheritedConfigSpec, ChildPreferenceInheritedConfigSpec in grails-test-examples-geb) extend ContainerGebSpec implements IContainerGebConfiguration and exercise the exact $getCallSiteArray() dispatch the upstream fix addresses. The canary CI matrix (Functional Tests (Java 21/25, indy=false)) is the right gate for that - rather than re-running the geb integration test locally with a Selenium container, leaving it for the CI run on this push.

The GROOVY-11980 fix is a Java stub generator change. Compilation succeeds locally, so the stub for HibernateSettings no longer carries the bogus throws CloneNotSupportedException. Runtime deep-clone semantics for tenant connection-source settings (the original reason for the explicit clone() body) are preserved by @AutoClone(style = CLONE), which is the default style and produces the exact same per-field clone shape the explicit override implemented manually.

Remaining real Groovy 6 regressions (no upstream PR yet)

Re-verified failing against build #571 by reverting locally:

Each has a deterministic standalone reproducer on Groovy 6.0.0-SNAPSHOT build #571. None has been filed upstream yet - will get tickets opened against apache/groovy over the next couple of pushes.

Pre-existing canary issue (independent)

:grails-fields:compileGroovy fails on the merged tree with Target constructor for constructor call expression hasn't been set in BeanPropertyAccessorFactory.groovy:83. Reproduces on the unmodified canary - independent of the workaround removals. Will track separately and not let it block this audit pass.

Net effect

Five workarounds dropped against this round of upstream fixes: 2 directly on this canary (GROOVY-11980, GROOVY-11982), 2 inherited from #15557 via merge (GROOVY-11983 × 2), and the ContainerSupport / constraint factory / parallelism workarounds removed in the previous audit cycle (GROOVY-11968 / GROOVY-11967 / GROOVY-11966). PR description has been refreshed to reflect the current state.

cc @jdaugherty for visibility on the canary progress; @paulk-asert if any of the remaining no-upstream-PR-yet items would benefit from a JIRA ticket sooner rather than later.

@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented May 3, 2026

🚨 TestLens detected 24 failed tests 🚨

Here is what you can do:

  1. Inspect the test failures carefully.
  2. If you are convinced that some of the tests are flaky, you can mute them below.
  3. Finally, trigger a rerun by checking the rerun checkbox.

Test Summary

Check Project/Task Test Runs
CI / Build Grails-Core (macos-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from changing existing value
CI / Build Grails-Core (macos-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from deep nesting
CI / Build Grails-Core (macos-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values with seeded map
CI / Build Grails-Core (macos-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values without seeding
CI / Build Grails-Core (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from changing existing value
CI / Build Grails-Core (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from deep nesting
CI / Build Grails-Core (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values with seeded map
CI / Build Grails-Core (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values without seeding
CI / Build Grails-Core (ubuntu-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values from changing existing value
CI / Build Grails-Core (ubuntu-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values from deep nesting
CI / Build Grails-Core (ubuntu-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values with seeded map
CI / Build Grails-Core (ubuntu-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values without seeding
CI / Build Grails-Core (windows-latest, 25) :grails-testing-support-http-client:test TestHttpResponseSpec > xml secure default still allows inline doctype declarations with internal entities
CI / Build Grails-Core (windows-latest, 25) :grails-testing-support-http-client:test TestHttpResponseSpec > xml uses a secure default slurper that does not resolve external entities
CI / Build Grails-Core (windows-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values from changing existing value
CI / Build Grails-Core (windows-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values from deep nesting
CI / Build Grails-Core (windows-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values with seeded map
CI / Build Grails-Core (windows-latest, 25) :grails-core:test WriteFilteringMapSpec > get written values without seeding
CI / Build Grails-Core (windows-latest, 25) :grails-testing-support-http-client:test XmlUtilsSpec > newXmlSlurper allows inline doctype declarations with internal entities
CI / Build Grails-Core (windows-latest, 25) :grails-testing-support-http-client:test XmlUtilsSpec > newXmlSlurper blocks external entities
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from changing existing value
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values from deep nesting
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values with seeded map
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) :grails-core:test WriteFilteringMapSpec > get written values without seeding

🏷️ Commit: bd7a30a
▶️ Tests: 10535 executed
⚪️ Checks: 31/31 completed

Test Failures (first 5 of 24)

WriteFilteringMapSpec > get written values from changing existing value (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

filter.getWrittenValues().size() == 1
|      |                  |      |
|      [:]                0      false
[a:[b:new-b-value]]

	at grails.config.external.WriteFilteringMapSpec.get written values from changing existing value(WriteFilteringMapSpec.groovy:52)
expected actual
1 0
WriteFilteringMapSpec > get written values from deep nesting (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

filter.getWrittenValues().size() == 3
|      |                  |      |
|      [:]                0      false
[a:[b:new-b-value, c:[d:d-value, e:new-e-value]]]

	at grails.config.external.WriteFilteringMapSpec.get written values from deep nesting(WriteFilteringMapSpec.groovy:68)
expected actual
3 0
WriteFilteringMapSpec > get written values with seeded map (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

filter.getWrittenValues().size() == 1
|      |                  |      |
|      [:]                0      false
[a:[b:b-value, c:c-value]]

	at grails.config.external.WriteFilteringMapSpec.get written values with seeded map(WriteFilteringMapSpec.groovy:31)
expected actual
1 0
WriteFilteringMapSpec > get written values without seeding (:grails-core:test in CI / Build Grails-Core (macos-latest, 21))
Condition not satisfied:

filter.getWrittenValues().size() == 1
|      |                  |      |
[:]    [:]                0      false

	at grails.config.external.WriteFilteringMapSpec.get written values without seeding(WriteFilteringMapSpec.groovy:41)
expected actual
1 0
WriteFilteringMapSpec > get written values from changing existing value (:grails-core:test in CI / Build Grails-Core (ubuntu-latest, 21))
Condition not satisfied:

filter.getWrittenValues().size() == 1
|      |                  |      |
|      [:]                0      false
[a:[b:new-b-value]]

	at grails.config.external.WriteFilteringMapSpec.get written values from changing existing value(WriteFilteringMapSpec.groovy:52)
expected actual
1 0

Muted Tests

Select tests to mute in this pull request:

  • TestHttpResponseSpec > xml secure default still allows inline doctype declarations with internal entities
  • TestHttpResponseSpec > xml uses a secure default slurper that does not resolve external entities
  • WriteFilteringMapSpec > get written values from changing existing value
  • WriteFilteringMapSpec > get written values from deep nesting
  • WriteFilteringMapSpec > get written values with seeded map
  • WriteFilteringMapSpec > get written values without seeding
  • XmlUtilsSpec > newXmlSlurper allows inline doctype declarations with internal entities
  • XmlUtilsSpec > newXmlSlurper blocks external entities

Reuse successful test results:

  • ♻️ Only rerun the tests that failed or were muted before

Click the checkbox to trigger a rerun:

  • Rerun jobs

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

None yet

Development

Successfully merging this pull request may close these issues.

2 participants