Skip to content

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

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

[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558
jamesfredley wants to merge 35 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. All Groovy 6 workarounds in the working tree below have been audited against the absolute latest snapshot and reduced to the minimum acceptable set. CI build/functional/Hibernate5/Mongodb/Forge suites green.

Snapshot baseline

Component Version
Apache Groovy 6.0.0-SNAPSHOT build #518, timestamp 2026-04-27 14:33:02 UTC (apache/groovy master HEAD as of 2026-04-27 15:50 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.


Outstanding workaround inventory (audited 2026-04-27 against build #518)

Tracked by an OPEN upstream PR

Workaround stays until the PR merges and a fresh snapshot publishes; then revert and re-validate.

Workaround site Symptom Upstream PR / JIRA
grails-gsp/.../GroovyPageCompiler.groovy Default GSP compile parallelism to 1; concurrent ListHashMap writes in AnnotationNode.isTargetAllowed -> NodeMetaDataHandler.getNodeMetaData OPEN: GROOVY-11966 / apache/groovy#2492 by @paulk-asert
grails-views-core/.../AbstractGroovyTemplateCompiler.groovy Same parallelism guard for GSON / Markup view compilation Same: GROOVY-11966 / #2492
grails-datamapping-validation/.../DefaultConstraintFactory.groovy Two explicit constructors instead of one with a default; dodges VerifyError on the synthesised lower-arity bridge constructor OPEN: GROOVY-11967 / apache/groovy#2493 by @paulk-asert
grails-datamapping-core/.../MappingContextAwareConstraintFactory.groovy Same two-constructor split Same: GROOVY-11967 / #2493
grails-geb/.../testFixtures/grails/plugin/geb/support/ContainerSupport.groovy @CompileStatic on a trait with static fields produces invalid bytecode for the Trait$Helper static setters under indy=false (VerifyError: get long/double overflows locals). Workaround: @CompileDynamic OPEN: GROOVY-11968 / apache/groovy#2495 by @paulk-asert (explicit GROOVY-11907 follow-up). Standalone reproducer: groovy5-compiledynamic-trait-bug/quick-checks/TraitStaticFieldsCheck.groovy

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 #518.

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
grails-geb/.../testFixtures/grails/plugin/geb/ContainerGebConfiguration.groovy (interface->trait conversion) Under indy=false, an interface compiled with $getCallSiteArray() causes JVM IncompatibleClassChangeError: Method '...$getCallSiteArray()' must be InterfaceMethodref constant when consumed. Workaround: convert IContainerGebConfiguration from interface to trait. groovy5-compiledynamic-trait-bug/quick-checks/InterfaceDefaultsCheck.groovy - works on Groovy 4 + indy=false, fails on Groovy 5/6 + indy=false

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-data-mongodb/core/.../PersistentEntityCodec.groovy - two ManyToMany.isAssignableFrom(...) swaps. Real bug: Groovy 5/6 @CompileStatic incorrect smart-cast in the else branch of if (cond && !(x instanceof Y)). Reproducer: SmartCastCheck.groovy.
  • 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.

Removed in this canary (Groovy 6 fixed them vs Groovy 5)

Each was a Groovy 5 workaround inherited from #15557, removed here after verifying the fix landed in Groovy 6:

  • AbstractConstraint.java - removed getDefaultMessageFromBundle fallback
  • GroovyConfigPropertySourceLoader.groovy - removed recursive toRegularMap(ConfigObject)
  • HibernateEntityTransformation.groovy - restored to instanceof InnerClassNode
  • ControllerActionTransformer.java - restored to DefaultGroovyMethods.count(Iterable, Closure) (GROOVY-11911 merged 2026-04-26)
  • BsonPersistentEntityCodec.groovy - removed resolvePropertyType() hierarchy walker
  • TraitPropertyAccessStrategy.java - removed is-prefix fallback (GROOVY-11512 in 6.0.0-alpha)

Plus earlier branch commits already removed: HibernateEntity static SQL methods, JspTagImpl @CompileDynamic, ClassPropertyFetcherTests generic trait, MongoCodecSession [name]++, GROOVY-11907 trait static members in Geb and scaffolding helpers.


Build status

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

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 upstream PR merges

  1. GRAILS-10803: Can't see chinese in log console in 2.3.x #2492 / GROOVY-11966 merges: drop the parallelism guards in GroovyPageCompiler and AbstractGroovyTemplateCompiler; restore concurrent GSP / view template compilation.
  2. GRAILS-6219: Under certain circumstances the DefaultGrailsDomainClass's metaClass is a MetaClassImpl, not an ExpandoMetaClass #2493 / GROOVY-11967 merges: collapse the two-constructor splits in DefaultConstraintFactory and MappingContextAwareConstraintFactory back to one constructor with a default.
  3. GRAILS-6530: Groovydoc generation includes duplicates #2495 / GROOVY-11968 merges: revert ContainerSupport from @CompileDynamic back to @CompileStatic and re-validate :grails-test-examples-geb:integrationTest -PgrailsIndy=false.

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

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.

@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Apr 27, 2026

🔎 No tests executed 🔎

🏷️ Commit: e6332ed
▶️ Tests: 0 executed
⚪️ Checks: 0/0 completed


Learn more about TestLens at testlens.app.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants