Skip to content
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
013cb13
canary: test Groovy 6.0.0-SNAPSHOT
jamesfredley Apr 5, 2026
4463494
fix: Groovy 6 VerifyError in DefaultConstraintFactory default parameter
jamesfredley Apr 5, 2026
ba0df88
Merge branch 'grails8-groovy5-sb4' into grails8-groovy6-canary
jamesfredley Apr 6, 2026
c19037d
fix: Groovy 6 compile fixes - Spock version check and CycloneDX license
jamesfredley Apr 6, 2026
ddd7325
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 6, 2026
0dcb652
fix: Groovy 6 genericGetMethod regression breaks property access on G…
jamesfredley Apr 6, 2026
a6d06f5
fix: GormEntity.get(String) throws MissingPropertyException when GORM…
jamesfredley Apr 6, 2026
99bf0be
fix: centralize Spock version check and add jline 4.0.7 CycloneDX ove…
jamesfredley Apr 6, 2026
8e4fba5
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 6, 2026
cfd7605
fix: Groovy 6 remaining test fixes
jamesfredley Apr 6, 2026
b2bd213
fix: resolve all remaining test failures on Groovy 6
jamesfredley Apr 6, 2026
24eb53d
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 6, 2026
027c203
fix: CodeNarc UnnecessaryDotClass in DefaultConstraintFactory
jamesfredley Apr 6, 2026
b225663
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 6, 2026
b9ac44c
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 6, 2026
52aad53
fix: GormEntity.get(String) delegates to staticPropertyMissing for GO…
jamesfredley Apr 6, 2026
ea41a40
Merge branch 'grails8-groovy5-sb4' into grails8-groovy6-canary
jamesfredley Apr 25, 2026
678bdf1
fix: harden Groovy 6 canary against snapshot drift and add regression…
jamesfredley Apr 25, 2026
c2aa269
fix: Groovy 6 closure dispatch regression in ControllerActionTransformer
jamesfredley Apr 25, 2026
8e9cdbc
fix: move Groovy 6 generic-getter guard from GormEntity trait to AST
jamesfredley Apr 25, 2026
ddc7ea2
fix: serialise GSP compilation under Groovy 6 to dodge ListHashMap race
jamesfredley Apr 25, 2026
95e9212
style: drop a leftover consecutive blank line in GormEntity
jamesfredley Apr 25, 2026
54b718b
fix(sbom): revert LICENSE_GROUP_MAPPING per @jdaugherty review
jamesfredley Apr 25, 2026
2619635
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 25, 2026
48598b4
test(forge): capture both stdout and stderr from generated-app gradle…
jamesfredley Apr 25, 2026
5d71eb4
fix(forge): bypass Spock Groovy-version check in generated app build.…
jamesfredley Apr 25, 2026
18b9e52
fix: serialise GSON/views template compilation under Groovy 6
jamesfredley Apr 25, 2026
3e9f517
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 25, 2026
de1cf4a
Merge remote-tracking branch 'origin/grails8-groovy5-sb4' into grails…
jamesfredley Apr 25, 2026
8af3b23
docs: drop incorrect GROOVY-11829 citation from GormEntity workaround…
jamesfredley Apr 26, 2026
3f485f3
Merge grails8-groovy5-sb4 audit work into grails8-groovy6-canary
jamesfredley Apr 27, 2026
ee77652
Add standalone reproducer link to GormEntity get(String) AST shim
jamesfredley Apr 27, 2026
fb717d3
Validateable: link standalone reproducer for the TraitReceiverTransfo…
jamesfredley Apr 27, 2026
e7f2478
Merge grails8-groovy5-sb4: link reproducer + Validateable javadoc + r…
jamesfredley Apr 27, 2026
e6332ed
ContainerSupport: link upstream PR #2495 (GROOVY-11968) and standalon…
jamesfredley Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class CompilePlugin implements Plugin<Project> {
it.options.encoding = StandardCharsets.UTF_8.name()
it.options.fork = true
it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G']
it.options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
if (System.getenv('SUPPRESS_DEPRECATION_WARNINGS') == 'true') {
it.options.compilerArgs += ['-Xlint:-removal']
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,32 @@ class SbomPlugin implements Plugin<Project> {
'pkg:maven/com.oracle.coherence.ce/coherence-bom@25.03.1?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/com.oracle.coherence.ce/coherence-bom@25.03.2?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/com.oracle.coherence.ce/coherence-bom@22.06.2?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // legacy jline:jline group, BSD-2; maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/opensymphony/sitemesh@2.6.0?type=jar' : 'OpenSymphony', // custom license approved by legal LEGAL-707
'pkg:maven/org.antlr/antlr4-runtime@4.7.2?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/org.jline/jansi@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline@3.30.6?type=jar' : 'BSD-3-Clause', // direct dependency declared at jline.version in dependencies.gradle
'pkg:maven/org.jline/jline-builtins@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-console@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-native@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-reader@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-style@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-terminal@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-terminal-jansi@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-terminal-jna@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jline/jline-terminal-jni@3.30.9?type=jar' : 'BSD-3-Clause', // jline group resolved at 3.30.9 transitively via groovy-groovysh; main org.jline:jline pinned at 3.30.6 directly
'pkg:maven/org.jruby/jzlib@1.1.5?type=jar' : 'BSD-3-Clause', // https://web.archive.org/web/20240822213507/http://www.jcraft.com/jzlib/LICENSE.txt shows it's a 3 clause
'pkg:maven/org.liquibase.ext/liquibase-hibernate5@4.27.0?type=jar': 'Apache-2.0', // maps incorrectly because of https://github.com/liquibase/liquibase/issues/2445 & the base pom does not define a license
]

/**
* Group-level license overrides applied AFTER {@link #LICENSE_MAPPING} fails to match.
* The key is a purl prefix (e.g. {@code 'pkg:maven/org.jline/'}) and the value is the
* SPDX license id to force for any artifact whose bomRef starts with that prefix.
*
* This exists for groups that:
* (a) have a stable license across all artifacts and versions, AND
* (b) suffer from cyclonedx-core-java#205 (license is misreported), AND
* (c) are pulled transitively by SNAPSHOT dependencies (e.g. groovy-groovysh ->
* org.jline:* drifts on every Groovy SNAPSHOT bump), making per-version entries
* unmaintainable.
*
* Only add a group entry when ALL three conditions hold. Per-version entries in
* {@link #LICENSE_MAPPING} should still be preferred for one-off overrides.
*/
private static Map<String, String> LICENSE_GROUP_MAPPING = [
'pkg:maven/org.jline/': 'BSD-3-Clause', // entire org.jline group is BSD-3-Clause; cyclonedx misreports it (cyclonedx-core-java#205) and versions drift via groovy-groovysh on every SNAPSHOT bump
Comment thread
jamesfredley marked this conversation as resolved.
Outdated
]

// we don't distribute these so these licenses are considered acceptable, but we still prefer ASF licenses.
// Require a whitelist of any case of category X licenses to prevent accidental inclusion in a distributed artifact
// this list will need to be updated anytime we change versions so we can revise the licenses
Expand Down Expand Up @@ -328,6 +336,21 @@ class SbomPlugin implements Plugin<Project> {
return licenseBlock
}

// Fallback: group-level override matched by purl prefix. See LICENSE_GROUP_MAPPING
// for criteria (stable license + cyclonedx misreport + SNAPSHOT version drift).
def groupOverride = LICENSE_GROUP_MAPPING.find { prefix, _ -> bomRef.startsWith(prefix) }
if (groupOverride) {
def licenseId = groupOverride.value
logger.lifecycle('Forcing license for {} to {} via group rule {}', bomRef, licenseId, groupOverride.key)

def licenseBlock = LICENSES[licenseId]
if (!licenseBlock) {
throw new GradleException("Cannot find license information for id ${licenseId} to use for bomRef ${bomRef} in project ${projectName}")
}

return licenseBlock
}

if (!(licenseChoices instanceof List) || licenseChoices.isEmpty()) {
throw new GradleException("No License was found for dependency: ${bomRef} in project ${projectName}")
}
Expand Down
2 changes: 1 addition & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ ext {
'commons-codec.version' : '1.21.0',
'commons-lang3.version' : '3.20.0',
'geb-spock.version' : '8.0.1',
'groovy.version' : '5.0.6-SNAPSHOT',
'groovy.version' : '6.0.0-SNAPSHOT',
'jquery.version' : '3.7.1',
'liquibase-hibernate5.version': '4.27.0',
'mongodb.version' : '5.6.4',
Expand Down
6 changes: 1 addition & 5 deletions gradle/functional-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ configurations.configureEach {
}
}

tasks.named('compileTestGroovy') {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

tasks.named('compileGroovy') {
tasks.withType(GroovyCompile).configureEach {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

Expand Down
4 changes: 4 additions & 0 deletions gradle/hibernate5-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies {
add('testRuntimeOnly', 'org.objenesis:objenesis')
}

tasks.withType(GroovyCompile).configureEach {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

tasks.withType(Test).configureEach {
onlyIf {
![
Expand Down
4 changes: 4 additions & 0 deletions gradle/mongodb-forked-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies {
add('testRuntimeOnly', 'org.objenesis:objenesis')
}

tasks.withType(GroovyCompile).configureEach {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

tasks.named('compileTestGroovy', GroovyCompile) {
groovyOptions.forkOptions.jvmArgs = ['-Xmx768m']
}
Expand Down
4 changes: 4 additions & 0 deletions gradle/mongodb-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies {
add('testRuntimeOnly', 'org.objenesis:objenesis')
}

tasks.withType(GroovyCompile).configureEach {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

tasks.named('compileTestGroovy', GroovyCompile) {
groovyOptions.forkOptions.jvmArgs = ['-Xmx768m']
}
Expand Down
11 changes: 6 additions & 5 deletions gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ dependencies {
add('testRuntimeOnly', 'org.objenesis:objenesis')
}

tasks.named('compileTestGroovy') {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

tasks.named('compileGroovy') {
// Disable Spock's compile-time Groovy version check on ALL Groovy compile
// tasks. Spock's SpockTransform is registered via META-INF services and
// the Groovy compiler loads every AST transform on the classpath, so even
// main source sets trip the version check when Groovy is newer than the
// Spock artifact's groovy variant.
tasks.withType(GroovyCompile).configureEach {
options.forkOptions.jvmArgs += ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
import org.codehaus.groovy.classgen.GeneratorContext;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.codehaus.groovy.transform.trait.Traits;
Expand Down Expand Up @@ -267,12 +266,12 @@ private void processMethods(ClassNode classNode, SourceUnit source,
if (methodShouldBeConfiguredAsControllerAction(method)) {
final List<MethodNode> declaredMethodsWithThisName = classNode.getDeclaredMethods(method.getName());
if (declaredMethodsWithThisName != null) {
final int numberOfNonExceptionHandlerMethodsWithThisName = DefaultGroovyMethods.count((Iterable) declaredMethodsWithThisName, new Closure(this) {
@Override
public Object call(Object object) {
return !isExceptionHandlingMethod((MethodNode) object);
int numberOfNonExceptionHandlerMethodsWithThisName = 0;
for (MethodNode candidate : declaredMethodsWithThisName) {
if (!isExceptionHandlingMethod(candidate)) {
numberOfNonExceptionHandlerMethodsWithThisName++;
}
}).intValue();
}
if (numberOfNonExceptionHandlerMethodsWithThisName > 1) {
String message = "Controller actions may not be overloaded. The [" +
method.getName() +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.grails.datastore.mapping.model.types.ToOne
import org.grails.datastore.mapping.query.api.BuildableCriteria
import org.grails.datastore.mapping.query.api.Criteria
import org.grails.datastore.mapping.reflect.EntityReflector
import org.codehaus.groovy.runtime.InvokerHelper

/**
*
Expand Down Expand Up @@ -591,12 +592,45 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D

/**
* Retrieves an object from the datastore. eg. Book.get(1)
*
* Groovy 6 (GROOVY-11829) relaxed {@code MetaClassImpl.isGenericGetMethod} to
* accept {@code get(Serializable)} as a generic property getter where Groovy 5
* required {@code get(String)}. Without the {@link #get(String)} overload below,
* every dynamic property access on a GORM entity (e.g. {@code Book.name},
* {@code Book.simpleName}) routes through this method instead of
* {@link Class#getName()}, breaking property resolution and triggering false
* "GORM not initialized" errors during class loading.
*/
@Generated
static D get(Serializable id) {
currentGormStaticApi().get(id)
}

/**
* Groovy 6 generic-property-getter overload (see GROOVY-11829). Resolves
* {@link Class} bean properties first, then falls back to GORM's
* {@code propertyMissing} (handles datasource qualifiers and dynamic GORM
* properties), and finally to {@link #get(Serializable)} for entity-by-ID
* lookups. Throws {@link MissingPropertyException} when nothing matches so
* Groovy's MOP can continue normal property resolution.
*/
@Generated
static Object get(String nameOrId) {
MetaProperty mp = InvokerHelper.getMetaClass(Class).hasProperty(this, nameOrId)
if (mp != null) {
return mp.getProperty(this)
}
try {
return currentGormStaticApi().propertyMissing(nameOrId)
} catch (MissingPropertyException | IllegalStateException ignore) {
try {
return currentGormStaticApi().get((Serializable) nameOrId)
} catch (IllegalStateException e) {
throw new MissingPropertyException(nameOrId, this)
}
}
}

/**
* Retrieves an object from the datastore. eg. Book.read(1)
*
Expand Down Expand Up @@ -854,6 +888,10 @@ trait GormEntity<D> implements GormValidateable, DirtyCheckable, GormEntityApi<D
*/
@Generated
static Object staticPropertyMissing(String property) {
MetaProperty mp = InvokerHelper.getMetaClass(Class).hasProperty(this, property)
if (mp != null) {
return mp.getProperty(this)
}
try {
currentGormStaticApi().propertyMissing(property)
} catch (IllegalStateException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class MappingContextAwareConstraintFactory extends DefaultConstraintFactory {

final MappingContext mappingContext

MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext, List<Class> targetTypes = [Object]) {
MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext) {
this(constraintClass, messageSource, mappingContext, [Object] as List<Class>)
}

MappingContextAwareConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, MappingContext mappingContext, List<Class> targetTypes) {
super(constraintClass, messageSource, targetTypes)
this.mappingContext = mappingContext
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,38 @@ class GormEntityTransformSpec extends Specification{
thrown(MissingPropertyException)
}

void 'test Groovy 6 genericGetMethod regression workaround (GROOVY-11829)'() {
expect: 'Class bean properties remain accessible via dynamic property access (the workaround target)'
Book.simpleName == 'Book'
Book.name.endsWith('Book')

and: 'the get(String) Groovy-6 compatibility overload exists and is @Generated'
def getStringMethod = Book.getMethod('get', String)
getStringMethod != null
getStringMethod.isAnnotationPresent(Generated)

and: 'the original get(Serializable) overload still exists for entity-by-id lookups'
def getSerializableMethod = Book.getMethod('get', Serializable)
getSerializableMethod != null
getSerializableMethod.isAnnotationPresent(Generated)
}

void 'test get(String) throws MissingPropertyException when GORM not initialized and string is not a Class property'() {
when: 'a name that is neither a Class property nor a known qualifier is passed'
Book.get('definitelyNotAClassPropertyOrEntityIdABCXYZ')

then: 'we do NOT leak the IllegalStateException raised by uninitialized GORM'
thrown(MissingPropertyException)
}

void 'test get(String) returns Class bean property when name matches Class property and GORM not initialized'() {
expect: 'explicit get("simpleName") returns the Class.simpleName because the Groovy 6 generic-getter workaround intercepts Class properties before delegating to the GORM static API'
Book.get('simpleName') == 'Book'

and: 'this is a documented behavior change vs Grails on Groovy 5: prior to GROOVY-11829, Book.get("simpleName") would call get(Serializable) and attempt an entity-by-id lookup. See GormEntity.get(String) docstring.'
Book.get('canonicalName') == Book.canonicalName
}

void 'test that all GormEntity/GormValidateable trait methods are marked as Generated'() {

expect: 'all GormEntity methods are marked as Generated on implementation class'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ class DefaultConstraintFactory implements ConstraintFactory {

protected final Constructor constraintConstructor

DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, List<Class> targetTypes = [Object]) {
DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource) {
this(constraintClass, messageSource, [Object] as List<Class>)
}

DefaultConstraintFactory(Class<? extends Constraint> constraintClass, MessageSource messageSource, List<Class> targetTypes) {
this.type = constraintClass
this.name = Introspector.decapitalize(constraintClass.simpleName) - 'Constraint'
this.messageSource = messageSource
Expand Down
Loading