[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558
[Canary] Grails 8 on Groovy 6.0.0-SNAPSHOT#15558jamesfredley wants to merge 35 commits intograils8-groovy5-sb4from
Conversation
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.
This comment has been minimized.
This comment has been minimized.
# Conflicts: # dependencies.gradle
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
This comment was marked as outdated.
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
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Final wrap-up: Groovy 5 / Groovy 6 workaround audit completeThis 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 line5 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
Verified upstream PRs that unblock our remaining workarounds (NOT yet merged to master)
Verified upstream JIRAs already in build 508+ master HEAD
Issues that NEED to be filed upstream (no matching JIRA / PR found)
Forge integration tests (3 workarounds remaining there)
Net delta
Assisted-by: claude-code:claude-opus-4-7 |
Final wrap-up: workaround burndown audit completeAfter end-to-end testing against Burndown count
Verified upstream PRs unblock our remaining workarounds
Verified upstream JIRAs already in master and removed our workaround
Issues to file upstream (no matching JIRA / PR found in
|
aadc3ff to
878adaf
Compare
2bd7667 to
3f485f3
Compare
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.
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
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
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
Removed since Groovy 5 (Groovy 6 fixed them)
Net effectWorkaround surface area on this canary is now:
cc @paulk-asert - the four "no upstream PR yet" reproducers ( The PR description has the full per-site inventory. |
🔎 No tests executed 🔎🏷️ Commit: e6332ed Learn more about TestLens at testlens.app. |
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
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.
grails-gsp/.../GroovyPageCompiler.groovyListHashMapwrites inAnnotationNode.isTargetAllowed -> NodeMetaDataHandler.getNodeMetaDatagrails-views-core/.../AbstractGroovyTemplateCompiler.groovygrails-datamapping-validation/.../DefaultConstraintFactory.groovyVerifyErroron the synthesised lower-arity bridge constructorgrails-datamapping-core/.../MappingContextAwareConstraintFactory.groovygrails-geb/.../testFixtures/grails/plugin/geb/support/ContainerSupport.groovy@CompileStaticon a trait with static fields produces invalid bytecode for theTrait$Helperstatic setters under indy=false (VerifyError: get long/double overflows locals). Workaround:@CompileDynamicReal 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.
grails-datamapping-core/.../GormEntityTransformation.groovy(per-entity ASTObject get(String)shim) + drop trait-static guard fromGormEntityMetaClassImplpicks up the inheritedObject 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 topropertyMissing(String). Manifests asUnknown entity: java.util.LinkedHashMapand NPE inHibernateRuntimeUtils.setupErrorsPropertyacrossDataServiceConnectionRoutingSpecandCrossLayerMultiDataSourceSpec(16+ failures).grails-validation/.../Validateable.groovy(resolveDefaultNullable(Class)reflection-based dispatch)TraitReceiverTransformerrewritesthis.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.grails-core/.../template/TemplateRendererImpl.groovy(statically-typedrender(Map)body) +grails-scaffolding/.../GenerateControllerCommand.groovy(typed positionaltemplateRenderer.render(Resource, File, Map, boolean)viagenerateFile()helper)@CompileStatic, calling an overloadedrender(Map<String,Object>)against a multi-overload interface reference silently no-ops (no exception, no warning, method body never entered). Reproduces with@Delegateforwarder, 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.grails-geb/.../testFixtures/grails/plugin/geb/ContainerGebConfiguration.groovy(interface->trait conversion)$getCallSiteArray()causes JVMIncompatibleClassChangeError: Method '...$getCallSiteArray()' must be InterfaceMethodref constantwhen consumed. Workaround: convertIContainerGebConfigurationfrominterfacetotrait.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- twoManyToMany.isAssignableFrom(...)swaps. Real bug: Groovy 5/6@CompileStaticincorrect smart-cast in the else branch ofif (cond && !(x instanceof Y)). Reproducer: SmartCastCheck.groovy.grails-bootstrap/.../NavigableMap.groovy- one-linecontainsKey + getchange inresolveConfigMapValueplus areadWithoutCreatinghelper. Real bug:[]operator on aConfigObjectmutates it by creating empty entries on missing-key reads, recursing infinitely throughmergeMaps -> mergeMapEntry.grails-core/.../GrailsASTUtils.java,grails-datastore-core/.../AstUtils.groovy,grails-datamapping-core/.../AbstractMethodDecoratingTransformation.groovy- try/catch aroundVariableScopeVisitor+ non-nullVariableScopeguard onClosureExpression. Real bug: Groovy 5/6VariableScopeVisitorNPEs during canonicalisation on certain Grails AST transformation outputs (e.g.DataServiceRoutingProductDataService.groovyin grails-datamapping-tck).grails-rest-transforms/.../ResourceTransform.groovy- same family asAbstractMethodDecoratingTransformation's non-nullVariableScopeguard.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- removedgetDefaultMessageFromBundlefallbackGroovyConfigPropertySourceLoader.groovy- removed recursivetoRegularMap(ConfigObject)HibernateEntityTransformation.groovy- restored toinstanceof InnerClassNodeControllerActionTransformer.java- restored toDefaultGroovyMethods.count(Iterable, Closure)(GROOVY-11911 merged 2026-04-26)BsonPersistentEntityCodec.groovy- removedresolvePropertyType()hierarchy walkerTraitPropertyAccessStrategy.java- removedis-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
Spock blocker
Spock has no Groovy 6-compatible artifact yet. The build sets
-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=trueon everyGroovyCompileandTesttask 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 inCompilePluginand re-verify.What changes when each upstream PR merges
GroovyPageCompilerandAbstractGroovyTemplateCompiler; restore concurrent GSP / view template compilation.DefaultConstraintFactoryandMappingContextAwareConstraintFactoryback to one constructor with a default.ContainerSupportfrom@CompileDynamicback to@CompileStaticand 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.