This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 19826e18dc GROOVY-11994: GEP-16: Missing groovy.val.enabled flag
19826e18dc is described below
commit 19826e18dcb7bad60beadaae219a5ca7b90f827c
Author: Paul King <[email protected]>
AuthorDate: Tue May 5 06:47:30 2026 +1000
GROOVY-11994: GEP-16: Missing groovy.val.enabled flag
---
src/antlr/GroovyLexer.g4 | 12 +-
.../apache/groovy/parser/antlr4/AstBuilder.java | 3 +-
src/test/groovy/groovy/ValDisabledTest.groovy | 135 +++++++++++++++++++++
.../m12n/ExtensionModuleHelperForTests.groovy | 20 ++-
4 files changed, 166 insertions(+), 4 deletions(-)
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 7f4ff706fa..c5215b4c1c 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -50,6 +50,16 @@ options {
private int lastTokenType;
private int invalidDigitCount;
+ /**
+ * When {@code false}, the {@code val} keyword is treated as a regular
+ * identifier (lexed as IDENTIFIER, not VAL). This can be used as a porting
+ * aid for migrating to Groovy 6 if affected by the known breaking edge
cases.
+ * Controlled by system property {@code groovy.val.enabled} (default:
{@code true}).
+ */
+ private static final boolean VAL_ENABLED =
+ Boolean.parseBoolean(System.getProperty("groovy.val.enabled",
"true"));
+ private boolean isValEnabled() { return VAL_ENABLED; }
+
/**
* Record the index and token type of the current token while emitting
tokens.
*/
@@ -483,7 +493,7 @@ THROW : 'throw';
THROWS : 'throws';
TRANSIENT : 'transient';
TRY : 'try';
-VAL : 'val';
+VAL : 'val' {isValEnabled()}?;
VAR : 'var';
VOID : 'void';
VOLATILE : 'volatile';
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 770a0cad50..bc8f2cf2b9 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -1225,7 +1225,7 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
public ClassNode visitClassDeclaration(final ClassDeclarationContext ctx) {
String packageName =
Optional.ofNullable(this.moduleNode.getPackageName()).orElse("");
String className = this.visitIdentifier(ctx.identifier());
- if ("var".equals(className) || "val".equals(className)) {
+ if ("var".equals(className) || (VAL_ENABLED &&
"val".equals(className))) {
throw createParsingFailedException(className + " cannot be used
for type declarations", ctx.identifier());
}
@@ -4952,6 +4952,7 @@ public class AstBuilder extends
GroovyParserBaseVisitor<Object> {
private int visitingArrayInitializerCount;
private static final int SLL_THRESHOLD =
SystemUtil.getIntegerSafe("groovy.antlr4.sll.threshold", -1);
+ private static final boolean VAL_ENABLED =
Boolean.parseBoolean(System.getProperty("groovy.val.enabled", "true"));
private static final String QUESTION_STR = "?";
private static final String DOT_STR = ".";
diff --git a/src/test/groovy/groovy/ValDisabledTest.groovy
b/src/test/groovy/groovy/ValDisabledTest.groovy
new file mode 100644
index 0000000000..3eebe4aed7
--- /dev/null
+++ b/src/test/groovy/groovy/ValDisabledTest.groovy
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy
+
+import org.junit.jupiter.api.Test
+
+import static
org.codehaus.groovy.runtime.m12n.ExtensionModuleHelperForTests.doInFork
+
+/**
+ * Tests that when {@code groovy.val.enabled=false}, GEP-16 breaking
+ * changes are resolved and {@code val} behaves as a regular identifier.
+ *
+ * Each test runs in a freshly forked JVM (compile + execution) with the
+ * property set, so the lexer's {@code static final VAL_ENABLED} is
+ * initialised to {@code false}.
+ */
+final class ValDisabledTest {
+
+ private static final List<String> JVM_ARGS = ['-Dgroovy.val.enabled=false']
+
+ private static void doInForkWithValDisabled(String script) {
+ // Wrap each snippet in assertScript so top-level class declarations
work
+ // (the snippet is otherwise placed inside a method body where local
+ // classes are not supported).
+ doInFork('java.lang.Object', "assertScript '''${script.replace("'",
"\\'")}'''", JVM_ARGS)
+ }
+
+ @Test
+ void testFieldNamedValBeforeMethod() {
+ doInForkWithValDisabled '''
+ class Foo {
+ def val
+ void doSomething() {}
+ }
+ def f = new Foo()
+ f.val = 42
+ assert f.val == 42
+ '''
+ }
+
+ @Test
+ void testValAsCastExpression() {
+ doInForkWithValDisabled '''
+ def val = 42
+ def result = val as String
+ assert result == "42"
+ '''
+ }
+
+ @Test
+ void testClassNamedVal() {
+ doInForkWithValDisabled '''
+ class val {
+ int x
+ }
+ def v = new val(x: 5)
+ assert v.x == 5
+ '''
+ }
+
+ @Test
+ void testValAsMethodReturnType() {
+ doInForkWithValDisabled '''
+ class val {
+ int x
+ }
+ import val as Val
+ class Foo {
+ Val bar() { new val(x: 99) }
+ }
+ assert new Foo().bar().x == 99
+ '''
+ }
+
+ @Test
+ void testValAsExplicitType() {
+ doInForkWithValDisabled '''
+ class val {
+ int x
+ }
+ val v = new val(x: 7)
+ assert v.x == 7
+ '''
+ }
+
+ @Test
+ void testDefValAssignment() {
+ doInForkWithValDisabled '''
+ def val = 1
+ assert val == 1
+ '''
+ }
+
+ @Test
+ void testValReassignment() {
+ doInForkWithValDisabled '''
+ def val = 1
+ val = 2
+ assert val == 2
+ '''
+ }
+
+ @Test
+ void testValAsMapKey() {
+ doInForkWithValDisabled '''
+ def m = [val: 42]
+ assert m.val == 42
+ '''
+ }
+
+ @Test
+ void testValPropertyAccess() {
+ doInForkWithValDisabled '''
+ class Foo { def val = "hello" }
+ def f = new Foo()
+ assert f.val == "hello"
+ '''
+ }
+}
diff --git
a/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy
b/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy
index 2281e11aa1..bd36f05855 100644
---
a/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy
+++
b/src/test/groovy/org/codehaus/groovy/runtime/m12n/ExtensionModuleHelperForTests.groovy
@@ -25,6 +25,10 @@ final class ExtensionModuleHelperForTests {
private ExtensionModuleHelperForTests() {}
static void doInFork(String baseTestClass = 'java.lang.Object', String
code) {
+ doInFork(baseTestClass, code, Collections.<String>emptyList())
+ }
+
+ static void doInFork(String baseTestClass, String code, List<String>
extraJvmArgs) {
File baseDir = File.createTempDir()
File sourceFile = new File(baseDir, 'Temp.groovy')
sourceFile << """import org.codehaus.groovy.runtime.m12n.*
@@ -69,6 +73,7 @@ final class ExtensionModuleHelperForTests {
~/Picked up _JAVA_OPTIONS: .*/
]
def jvmArgs = []
+ jvmArgs.addAll(extraJvmArgs)
if (Runtime.version().feature() == 25) {
// JEP 471/498: silence terminal-deprecation warnings for
sun.misc.Unsafe
// memory-access methods called from agents on the inherited
classpath
@@ -78,8 +83,19 @@ final class ExtensionModuleHelperForTests {
}
try {
ant.with {
- taskdef(name: 'groovyc', classname:
'org.codehaus.groovy.ant.Groovyc')
- groovyc(srcdir: baseDir.absolutePath, destdir:
baseDir.absolutePath, includes: 'Temp.groovy', fork: true)
+ // Compile via FileSystemCompilerFacade in a forked JVM (same
as forked groovyc),
+ // but using ant.java so we can attach arbitrary JVM args
(e.g. system properties).
+ java(classname:
'org.codehaus.groovy.ant.FileSystemCompilerFacade', fork: 'true', failonerror:
'true') {
+ jvmArgs.each { jvmarg(value: it) }
+ classpath {
+ cp.each { pathelement location: it }
+ }
+ arg(value: '--classpath')
+ arg(value: cp.join(File.pathSeparator))
+ arg(value: '-d')
+ arg(value: baseDir.absolutePath)
+ arg(value: sourceFile.absolutePath)
+ }
java(classname: 'Temp', fork: 'true', outputproperty: 'out',
errorproperty: 'err') {
jvmArgs.each { jvmarg(value: it) }
classpath {