diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4 index 7f4ff706fab..c5215b4c1c0 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 770a0cad503..bc8f2cf2b91 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 ClassNode visitTypeDeclaration(final TypeDeclarationContext ctx) { 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 List getDeclarationExpressions() { 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 00000000000..3eebe4aed7f --- /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 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 2281e11aa1d..bd36f058556 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.emptyList()) + } + + static void doInFork(String baseTestClass, String code, List 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 {