You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/05/01 12:25:29 UTC
(groovy) branch master updated: GROOVY-11977: val type placeholder for final declarations (#2464)
This is an automated email from the ASF dual-hosted git repository.
daniellansun 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 7203ff7dd7 GROOVY-11977: val type placeholder for final declarations (#2464)
7203ff7dd7 is described below
commit 7203ff7dd7d9498e994ecbff4a281b8d00b7bbef
Author: Paul King <pa...@asert.com.au>
AuthorDate: Fri May 1 22:25:23 2026 +1000
GROOVY-11977: val type placeholder for final declarations (#2464)
---
src/antlr/GroovyLexer.g4 | 1 +
src/antlr/GroovyParser.g4 | 6 ++-
.../apache/groovy/parser/antlr4/AstBuilder.java | 14 +++---
.../java/org/codehaus/groovy/ast/ModifierNode.java | 8 ++-
src/test-resources/core/Val_01x.groovy | 57 ++++++++++++++++++++++
src/test-resources/fail/Val_01x.groovy | 20 ++++++++
src/test-resources/fail/Val_02x.groovy | 20 ++++++++
src/test-resources/fail/Val_03x.groovy | 21 ++++++++
src/test/groovy/bugs/Groovy5358.groovy | 18 +++----
.../groovy/groovy/transform/stc/LambdaTest.groovy | 2 +-
.../groovy/parser/antlr4/GroovyParserTest.groovy | 5 ++
.../groovy/parser/antlr4/SyntaxErrorTest.groovy | 7 +++
.../console/ui/text/SmartDocumentFilter.java | 3 +-
13 files changed, 162 insertions(+), 20 deletions(-)
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 803afd8bc3..7f4ff706fa 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -483,6 +483,7 @@ THROW : 'throw';
THROWS : 'throws';
TRANSIENT : 'transient';
TRY : 'try';
+VAL : 'val';
VAR : 'var';
VOID : 'void';
VOLATILE : 'volatile';
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index d0b39c0b7a..577c3d6ff9 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -134,6 +134,7 @@ modifier
| TRANSIENT
| VOLATILE
| DEF
+ | VAL
| VAR
)
;
@@ -175,6 +176,7 @@ variableModifier
: annotation
| m=( FINAL
| DEF
+ | VAL
| VAR
// Groovy supports declaring local variables as instance/class fields,
// e.g. import groovy.transform.*; @Field static List awe = [1, 2, 3]
@@ -706,7 +708,7 @@ enhancedForControl
;
indexVariable
- : (BuiltInPrimitiveType | DEF | VAR)? identifier
+ : (BuiltInPrimitiveType | DEF | VAL | VAR)? identifier
;
originalForControl
@@ -1254,6 +1256,7 @@ identifier
| RECORD
| SEALED
| TRAIT
+ | VAL
| VAR
| YIELD
;
@@ -1312,6 +1315,7 @@ keywords
| TRAIT
| THREADSAFE
| TRY
+ | VAL
| VAR
| VOLATILE
| WHILE
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 aa43a3feff..626215e5d7 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -1225,8 +1225,8 @@ 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)) {
- throw createParsingFailedException("var cannot be used for type declarations", ctx.identifier());
+ if ("var".equals(className) || "val".equals(className)) {
+ throw createParsingFailedException(className + " cannot be used for type declarations", ctx.identifier());
}
boolean isAnnotation = asBoolean(ctx.AT());
@@ -1747,8 +1747,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
throw createParsingFailedException("Only record can have compact constructor", ctx);
}
- if (new ModifierManager(this, ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS)).containsAny(VAR)) {
- throw createParsingFailedException("var cannot be used for compact constructor declaration", ctx);
+ if (new ModifierManager(this, ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS)).containsAny(VAL, VAR)) {
+ throw createParsingFailedException("val/var cannot be used for compact constructor declaration", ctx);
}
String methodName = this.visitMethodName(ctx.methodName());
@@ -1785,8 +1785,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) {
ModifierManager modifierManager = createModifierManager(ctx);
- if (modifierManager.containsAny(VAR)) {
- throw createParsingFailedException("var cannot be used for method declarations", ctx);
+ if (modifierManager.containsAny(VAL, VAR)) {
+ throw createParsingFailedException("val/var cannot be used for method return types", ctx);
}
String methodName = this.visitMethodName(ctx.methodName());
@@ -4691,7 +4691,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return true;
}
- if (hasReturnType && (modifierManager.containsAny(DEF, VAR))) {
+ if (hasReturnType && (modifierManager.containsAny(DEF, VAL, VAR))) {
return true;
}
diff --git a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
index e76e1b4849..079364c4ab 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
@@ -38,6 +38,7 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.STATIC;
import static org.apache.groovy.parser.antlr4.GroovyParser.STRICTFP;
import static org.apache.groovy.parser.antlr4.GroovyParser.SYNCHRONIZED;
import static org.apache.groovy.parser.antlr4.GroovyParser.TRANSIENT;
+import static org.apache.groovy.parser.antlr4.GroovyParser.VAL;
import static org.apache.groovy.parser.antlr4.GroovyParser.VAR;
import static org.apache.groovy.parser.antlr4.GroovyParser.VOLATILE;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.asBoolean;
@@ -56,6 +57,7 @@ public class ModifierNode extends ASTNode {
public static final Map<Integer, Integer> MODIFIER_OPCODE_MAP = Maps.of(
ANNOTATION_TYPE, 0,
DEF, 0,
+ VAL, Opcodes.ACC_FINAL,
VAR, 0,
NATIVE, Opcodes.ACC_NATIVE,
@@ -129,7 +131,11 @@ public class ModifierNode extends ASTNode {
}
public boolean isDef() {
- return Objects.equals(DEF, this.type) || Objects.equals(VAR, this.type);
+ return Objects.equals(DEF, this.type) || Objects.equals(VAL, this.type) || Objects.equals(VAR, this.type);
+ }
+
+ public boolean isVal() {
+ return Objects.equals(VAL, this.type);
}
public Integer getType() {
diff --git a/src/test-resources/core/Val_01x.groovy b/src/test-resources/core/Val_01x.groovy
new file mode 100644
index 0000000000..e9444001cc
--- /dev/null
+++ b/src/test-resources/core/Val_01x.groovy
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+// basic val declaration
+val name = "Groovy"
+assert "Groovy" == name
+
+// val as closure parameter name
+[1, 2, 3].each { val ->
+ assert 0 < val && val < 4
+}
+
+// val as variable name (contextual keyword)
+val val = "val variable name"
+assert "val variable name" == val
+
+// val in map key
+def m = [val: 42]
+assert m.val == 42
+
+// val with different types
+val x = 42
+assert x == 42
+val s = "hello"
+assert s.class == String
+
+// shallow finality - mutation OK
+val list = [1, 2, 3]
+list << 4
+assert list == [1, 2, 3, 4]
+
+// final val is redundant but works
+final val y = 10
+assert y == 10
+
+// for loop with val
+for (val i in [1, 2, 3]) { assert i > 0 }
+
+// GString interpolation
+val g = 99
+assert "$g" == "99"
diff --git a/src/test-resources/fail/Val_01x.groovy b/src/test-resources/fail/Val_01x.groovy
new file mode 100644
index 0000000000..0748f66b32
--- /dev/null
+++ b/src/test-resources/fail/Val_01x.groovy
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+class val {}
diff --git a/src/test-resources/fail/Val_02x.groovy b/src/test-resources/fail/Val_02x.groovy
new file mode 100644
index 0000000000..ed6e519f7d
--- /dev/null
+++ b/src/test-resources/fail/Val_02x.groovy
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+val someMethod() {}
diff --git a/src/test-resources/fail/Val_03x.groovy b/src/test-resources/fail/Val_03x.groovy
new file mode 100644
index 0000000000..9751976344
--- /dev/null
+++ b/src/test-resources/fail/Val_03x.groovy
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+val x = 1
+x = 2
diff --git a/src/test/groovy/bugs/Groovy5358.groovy b/src/test/groovy/bugs/Groovy5358.groovy
index d6d6f63110..9c7fcec5e8 100644
--- a/src/test/groovy/bugs/Groovy5358.groovy
+++ b/src/test/groovy/bugs/Groovy5358.groovy
@@ -71,25 +71,25 @@ final class Groovy5358 {
void testSetPropertyOverrides() {
assertScript '''
class FooWorksAsMap {
- def val
+ def stored
void setProperty(String name, value) {
- val = "OK:FooWorksAsMap.$value"
+ stored = "OK:FooWorksAsMap.$value"
}
}
class BarWorksAsMap {
- def val
+ def stored
}
@Category(BarWorksAsMap) class C {
void setProperty(String name, value) {
- setVal("OK:BarWorksAsMap.$value")
+ setStored("OK:BarWorksAsMap.$value")
}
}
BarWorksAsMap.mixin C
class BazWorksAsMap {
- def val
+ def stored
}
BazWorksAsMap.metaClass.setProperty = { String name, value ->
- setVal("OK:BazWorksAsMap.$value")
+ setStored("OK:BazWorksAsMap.$value")
}
def objects = [
@@ -99,10 +99,10 @@ final class Groovy5358 {
[:]
]
for (def obj in objects) {
- def which = "${obj.getClass().getSimpleName()}.val"
+ def which = "${obj.getClass().getSimpleName()}.stored"
try {
- obj.val = which.startsWith('LinkedHashMap') ? "OK:LinkedHashMap.bar" : 'bar'
- assert obj.val.startsWith('OK:') : "$which -> $obj.val"
+ obj.stored = which.startsWith('LinkedHashMap') ? "OK:LinkedHashMap.bar" : 'bar'
+ assert obj.stored.startsWith('OK:') : "$which -> $obj.stored"
} catch (any) {
assert false : "$which -> FAIL:$any"
}
diff --git a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
index 4c434e6a29..37eab1575c 100644
--- a/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/LambdaTest.groovy
@@ -817,7 +817,7 @@ final class LambdaTest {
this.val = v
}
String toString() {
- val as String
+ this.val as String
}
def <Out> Value<Out> replace(Supplier<Out> supplier) {
new Value<>(supplier.get())
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index 575b54bda6..3ca7c7b28c 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -483,6 +483,11 @@ final class GroovyParserTest {
doRunAndTestAntlr4('core/SafeChainOperator.groovy')
}
+ @Test
+ void 'groovy core - val'() {
+ doRunAndTestAntlr4('core/Val_01x.groovy')
+ }
+
@Test
void 'groovy core - var'() {
doRunAndTestAntlr4('core/Var_01x.groovy')
diff --git a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index 788f47e8ef..bbe0175f72 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -480,6 +480,13 @@ final class SyntaxErrorTest {
TestUtils.doRunAndShouldFail('fail/MethodCall_01x.groovy')
}
+ @Test
+ void 'groovy core - val'() {
+ TestUtils.doRunAndShouldFail('fail/Val_01x.groovy')
+ TestUtils.doRunAndShouldFail('fail/Val_02x.groovy')
+ TestUtils.doRunAndShouldFail('fail/Val_03x.groovy')
+ }
+
@Test
void 'groovy core - var'() {
TestUtils.doRunAndShouldFail('fail/Var_01x.groovy')
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
index 25dcc7d393..9d639328ad 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/text/SmartDocumentFilter.java
@@ -120,6 +120,7 @@ import static org.apache.groovy.parser.antlr4.GroovyLexer.TRAIT;
import static org.apache.groovy.parser.antlr4.GroovyLexer.TRANSIENT;
import static org.apache.groovy.parser.antlr4.GroovyLexer.TRY;
import static org.apache.groovy.parser.antlr4.GroovyLexer.UNEXPECTED_CHAR;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.VAL;
import static org.apache.groovy.parser.antlr4.GroovyLexer.VAR;
import static org.apache.groovy.parser.antlr4.GroovyLexer.VOID;
import static org.apache.groovy.parser.antlr4.GroovyLexer.VOLATILE;
@@ -138,7 +139,7 @@ public class SmartDocumentFilter extends DocumentFilter {
*/
public static final List<Integer> HIGHLIGHTED_TOKEN_TYPE_LIST =
Arrays.asList(AS, ASYNC, AWAIT, DEF, DEFER, IN, TRAIT, THREADSAFE,
- VAR, BuiltInPrimitiveType, ABSTRACT, ASSERT, BREAK, CASE, CATCH, CLASS, CONST, CONTINUE, DEFAULT, DO,
+ VAL, VAR, BuiltInPrimitiveType, ABSTRACT, ASSERT, BREAK, CASE, CATCH, CLASS, CONST, CONTINUE, DEFAULT, DO,
ELSE, ENUM, EXTENDS, FINAL, FINALLY, FOR, IF, GOTO, IMPLEMENTS, IMPORT, INSTANCEOF, INTERFACE,
NATIVE, NEW, NON_SEALED, NOT_IN, NOT_INSTANCEOF, PACKAGE, PERMITS, PRIVATE, PROTECTED, PUBLIC,
RECORD, RETURN, SEALED, STATIC, STRICTFP, SUPER, SWITCH, SYNCHRONIZED,