You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2021/09/23 03:37:01 UTC
[groovy] 01/02: GROOVY-10240: Support record grammar
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 3b2cb508f7c342d961ded3d3fa4e176635950f89
Author: Daniel Sun <su...@apache.org>
AuthorDate: Tue Sep 21 20:48:51 2021 +0800
GROOVY-10240: Support record grammar
---
src/antlr/GroovyLexer.g4 | 2 +
src/antlr/GroovyParser.g4 | 6 +-
.../apache/groovy/ast/tools/MethodNodeUtils.java | 15 +++++
.../apache/groovy/parser/antlr4/AstBuilder.java | 71 +++++++++++++++++++---
.../java/org/codehaus/groovy/ast/ClassNode.java | 11 ++++
.../org/codehaus/groovy/classgen/Verifier.java | 23 +++++--
.../groovy/classgen/asm/WriterController.java | 7 +++
.../transform/stc/StaticTypeCheckingVisitor.java | 16 ++++-
.../core/RecordDeclaration_01x.groovy | 25 ++++++++
.../core/RecordDeclaration_02x.groovy | 33 ++++++++++
.../core/RecordDeclaration_03x.groovy | 31 ++++++++++
.../core/RecordDeclaration_04x.groovy | 35 +++++++++++
.../core/RecordDeclaration_05x.groovy | 32 ++++++++++
.../core/RecordDeclaration_06x.groovy | 38 ++++++++++++
.../core/RecordDeclaration_07x.groovy | 30 +++++++++
.../fail/RecordDeclaration_01x.groovy | 21 +++++++
.../fail/RecordDeclaration_02x.groovy | 21 +++++++
.../fail/RecordDeclaration_03x.groovy | 21 +++++++
.../fail/RecordDeclaration_04x.groovy | 21 +++++++
.../fail/RecordDeclaration_05x.groovy | 21 +++++++
.../fail/RecordDeclaration_06x.groovy | 23 +++++++
.../fail/RecordDeclaration_07x.groovy | 23 +++++++
.../org/codehaus/groovy/classgen/RecordTest.groovy | 39 ++++++++++++
.../groovy/parser/antlr4/GroovyParserTest.groovy | 10 +++
.../groovy/parser/antlr4/SyntaxErrorTest.groovy | 10 +++
.../console/ui/text/SmartDocumentFilter.java | 3 +-
26 files changed, 572 insertions(+), 16 deletions(-)
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 6c49387..b66afc8 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -471,6 +471,8 @@ PERMITS : 'permits';
PRIVATE : 'private';
PROTECTED : 'protected';
PUBLIC : 'public';
+
+RECORD : 'record';
RETURN : 'return';
SEALED : 'sealed';
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 455f9e3..49d76c0 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -215,7 +215,7 @@ typeList
/**
- * t 0: class; 1: interface; 2: enum; 3: annotation; 4: trait
+ * t 0: class; 1: interface; 2: enum; 3: annotation; 4: trait; 5: record
*/
classDeclaration
locals[ int t ]
@@ -224,9 +224,11 @@ locals[ int t ]
| ENUM { $t = 2; }
| AT INTERFACE { $t = 3; }
| TRAIT { $t = 4; }
+ | RECORD { $t = 5; }
)
identifier
(nls typeParameters)?
+ (nls formalParameters)?
(nls EXTENDS nls scs=typeList)?
(nls IMPLEMENTS nls is=typeList)?
(nls PERMITS nls ps=typeList)?
@@ -1212,6 +1214,7 @@ identifier
| AS
| YIELD
| PERMITS
+ | RECORD
;
builtInType
@@ -1250,6 +1253,7 @@ keywords
| NON_SEALED
| PACKAGE
| PERMITS
+ | RECORD
| RETURN
| SEALED
| STATIC
diff --git a/src/main/java/org/apache/groovy/ast/tools/MethodNodeUtils.java b/src/main/java/org/apache/groovy/ast/tools/MethodNodeUtils.java
index 7feea9e..5b4b346 100644
--- a/src/main/java/org/apache/groovy/ast/tools/MethodNodeUtils.java
+++ b/src/main/java/org/apache/groovy/ast/tools/MethodNodeUtils.java
@@ -18,6 +18,7 @@
*/
package org.apache.groovy.ast.tools;
+import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.stmt.BlockStatement;
@@ -128,4 +129,18 @@ public class MethodNodeUtils {
}
return block;
}
+
+ /**
+ * Check if the {@link MethodNode} instance is getter candidate
+ *
+ * @param m the {@link MethodNode} instance
+ * @return {@code true} if the instance is getter candidate
+ * @since 4.0.0
+ */
+ public static boolean isGetterCandidate(MethodNode m) {
+ Parameter[] parameters = m.getParameters();
+ return m.isPublic() && !m.isStatic() && !m.isAbstract()
+ && (null == parameters || 0 == parameters.length)
+ && !ClassHelper.VOID_TYPE.equals(m.getReturnType());
+ }
}
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 3294e9b..67810ba 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -292,6 +292,7 @@ import org.codehaus.groovy.ast.tools.ClosureUtils;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
@@ -372,6 +373,7 @@ import static org.codehaus.groovy.runtime.DefaultGroovyMethods.last;
* Builds the AST from the parse tree generated by Antlr4.
*/
public class AstBuilder extends GroovyParserBaseVisitor<Object> {
+
public AstBuilder(final SourceUnit sourceUnit, final boolean groovydocEnabled, final boolean runtimeGroovydocEnabled) {
this.sourceUnit = sourceUnit;
this.moduleNode = new ModuleNode(sourceUnit);
@@ -1467,6 +1469,28 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
boolean isFinal = finalModifierNodeOptional.isPresent();
boolean isSealed = sealedModifierNodeOptional.isPresent();
boolean isNonSealed = nonSealedModifierNodeOptional.isPresent();
+
+ boolean isRecord = asBoolean(ctx.RECORD());
+ boolean hasRecordHeader = asBoolean(ctx.formalParameters());
+ if (isRecord) {
+ if (asBoolean(ctx.EXTENDS())) {
+ throw createParsingFailedException("No extends clause allowed for record declaration", ctx.EXTENDS());
+ }
+ if (!hasRecordHeader) {
+ throw createParsingFailedException("header declaration of record is expected", ctx.identifier());
+ }
+ if (isSealed) {
+ throw createParsingFailedException("`sealed` is not allowed for record declaration", sealedModifierNodeOptional.get());
+ }
+ if (isNonSealed) {
+ throw createParsingFailedException("`non-sealed` is not allowed for record declaration", nonSealedModifierNodeOptional.get());
+ }
+ } else {
+ if (hasRecordHeader) {
+ throw createParsingFailedException("header declaration is only allowed for record declaration", ctx.formalParameters());
+ }
+ }
+
if (isSealed && isNonSealed) {
throw createParsingFailedException("type cannot be defined with both `sealed` and `non-sealed`", nonSealedModifierNodeOptional.get());
}
@@ -1537,6 +1561,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
if (isInterfaceWithDefaultMethods || asBoolean(ctx.TRAIT())) {
classNode.addAnnotation(new AnnotationNode(ClassHelper.makeCached(Trait.class)));
}
+ if (isRecord) {
+ classNode.addAnnotation(new AnnotationNode(ClassHelper.makeWithoutCaching(RECORD_TYPE_NAME)));
+ }
classNode.addAnnotations(modifierManager.getAnnotations());
if (isInterfaceWithDefaultMethods) {
@@ -1558,24 +1585,26 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
classNode.setSuperClass(superClass);
classNode.setInterfaces(this.visitTypeList(ctx.is));
this.initUsingGenerics(classNode);
-
} else if (isInterface) {
classNode.setModifiers(classNode.getModifiers() | Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT);
classNode.setSuperClass(ClassHelper.OBJECT_TYPE.getPlainNodeReference());
classNode.setInterfaces(this.visitTypeList(ctx.scs));
this.initUsingGenerics(classNode);
this.hackMixins(classNode);
-
} else if (isEnum) {
classNode.setModifiers(classNode.getModifiers() | Opcodes.ACC_ENUM | Opcodes.ACC_FINAL);
classNode.setInterfaces(this.visitTypeList(ctx.is));
this.initUsingGenerics(classNode);
-
} else if (isAnnotation) {
classNode.setModifiers(classNode.getModifiers() | Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_ANNOTATION);
classNode.addInterface(ClassHelper.Annotation_TYPE);
this.hackMixins(classNode);
+ } else if (isRecord) {
+ classNode.setModifiers(classNode.getModifiers() | Opcodes.ACC_RECORD | Opcodes.ACC_FINAL);
+ classNode.setInterfaces(this.visitTypeList(ctx.is));
+ this.transformRecordHeaderToProperties(ctx, classNode);
+ this.initUsingGenerics(classNode);
} else {
throw createParsingFailedException("Unsupported class declaration: " + ctx.getText(), ctx);
}
@@ -1591,6 +1620,14 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
classNodeStack.push(classNode);
ctx.classBody().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
this.visitClassBody(ctx.classBody());
+ if (isRecord) {
+ Optional<FieldNode> fieldNodeOptional =
+ classNode.getFields().stream()
+ .filter(f -> !isTrue(f, IS_RECORD_GENERATED) && !f.isStatic()).findFirst();
+ if (fieldNodeOptional.isPresent()) {
+ createParsingFailedException("Instance field is not allowed in `record`", fieldNodeOptional.get());
+ }
+ }
classNodeStack.pop();
if (!(asBoolean(ctx.CLASS()) || asBoolean(ctx.TRAIT()))) {
@@ -1602,6 +1639,17 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
return classNode;
}
+ private void transformRecordHeaderToProperties(ClassDeclarationContext ctx, ClassNode classNode) {
+ Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
+ for (int i = 0; i < parameters.length; i++) {
+ Parameter parameter = parameters[i];
+ ModifierManager parameterModifierManager = parameter.getNodeMetaData(PARAMETER_MODIFIER_MANAGER);
+ FormalParameterContext parameterCtx = parameter.getNodeMetaData(PARAMETER_CONTEXT);
+ PropertyNode propertyNode = declareProperty(parameterCtx, parameterModifierManager, parameter.getType(), classNode, i, parameter, parameter.getName(), parameter.getModifiers(), parameter.getInitialExpression());
+ propertyNode.getField().putNodeMetaData(IS_RECORD_GENERATED, true);
+ }
+ }
+
@SuppressWarnings("unchecked")
private boolean containsDefaultMethods(final ClassDeclarationContext ctx) {
List<MethodDeclarationContext> methodDeclarationContextList =
@@ -2224,7 +2272,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
}
- private void declareProperty(final VariableDeclarationContext ctx, final ModifierManager modifierManager, final ClassNode variableType, final ClassNode classNode, final int i, final VariableExpression variableExpression, final String fieldName, final int modifiers, final Expression initialValue) {
+ private PropertyNode declareProperty(final GroovyParserRuleContext ctx, final ModifierManager modifierManager, final ClassNode variableType, final ClassNode classNode, final int i, final ASTNode startNode, final String fieldName, final int modifiers, final Expression initialValue) {
PropertyNode propertyNode;
FieldNode fieldNode = classNode.getDeclaredField(fieldName);
@@ -2259,7 +2307,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
if (i == 0) {
configureAST(fieldNode, ctx, initialValue);
} else {
- configureAST(fieldNode, variableExpression, initialValue);
+ configureAST(fieldNode, startNode, initialValue);
}
}
@@ -2269,8 +2317,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
if (i == 0) {
configureAST(propertyNode, ctx, initialValue);
} else {
- configureAST(propertyNode, variableExpression, initialValue);
+ configureAST(propertyNode, startNode, initialValue);
}
+ return propertyNode;
}
private void declareField(final VariableDeclarationContext ctx, final ModifierManager modifierManager, final ClassNode variableType, final ClassNode classNode, final int i, final VariableExpression variableExpression, final String fieldName, final int modifiers, final Expression initialValue) {
@@ -4659,8 +4708,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
}
}
+ ModifierManager modifierManager = new ModifierManager(this, this.visitVariableModifiersOpt(variableModifiersOptContext));
Parameter parameter =
- new ModifierManager(this, this.visitVariableModifiersOpt(variableModifiersOptContext))
+ modifierManager
.processParameter(
configureAST(
new Parameter(
@@ -4670,6 +4720,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
ctx
)
);
+ parameter.putNodeMetaData(PARAMETER_MODIFIER_MANAGER, modifierManager);
+ parameter.putNodeMetaData(PARAMETER_CONTEXT, ctx);
+
if (asBoolean(expressionContext)) {
parameter.setInitialExpression((Expression) this.visit(expressionContext));
@@ -5102,4 +5155,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
private static final String FLOATING_POINT_LITERAL_TEXT = "_FLOATING_POINT_LITERAL_TEXT";
private static final String ENCLOSING_INSTANCE_EXPRESSION = "_ENCLOSING_INSTANCE_EXPRESSION";
private static final String IS_YIELD_STATEMENT = "_IS_YIELD_STATEMENT";
+ private static final String PARAMETER_MODIFIER_MANAGER = "_PARAMETER_MODIFIER_MANAGER";
+ private static final String PARAMETER_CONTEXT = "_PARAMETER_CONTEXT";
+ private static final String IS_RECORD_GENERATED = "_IS_RECORD_GENERATED";
+ private static final String RECORD_TYPE_NAME = "groovy.transform.RecordType";
}
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index 6981c90..796998f 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -59,6 +59,7 @@ import static org.objectweb.asm.Opcodes.ACC_ANNOTATION;
import static org.objectweb.asm.Opcodes.ACC_ENUM;
import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_RECORD;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
@@ -1357,6 +1358,16 @@ public class ClassNode extends AnnotatedNode {
return (getModifiers() & ACC_INTERFACE) != 0;
}
+ /**
+ * Checks if the {@link ClassNode} instance represents {@code record}
+ *
+ * @return {@code true} if the instance represents {@code record}
+ * @since 4.0.0
+ */
+ public boolean isRecord() {
+ return (getModifiers() & ACC_RECORD) != 0;
+ }
+
public boolean isAbstract() {
return (getModifiers() & ACC_ABSTRACT) != 0;
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index 5425c24..5a79b26 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -29,6 +29,7 @@ import groovy.transform.NonSealed;
import groovy.transform.Sealed;
import groovy.transform.stc.POJO;
import org.apache.groovy.ast.tools.ClassNodeUtils;
+import org.apache.groovy.ast.tools.MethodNodeUtils;
import org.apache.groovy.util.BeanUtils;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
@@ -794,14 +795,24 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
if (node.isStatic()) {
getterModifiers &= ~ACC_FINAL;
}
+
if (getterBlock != null) {
- visitGetter(node, field, getterBlock, getterModifiers, getterName);
+ boolean toVisitGetter = true;
+ if (classNode.isRecord()) {
+ boolean isGetterDefined = classNode.getDeclaredMethods(name).stream()
+ .anyMatch(MethodNodeUtils::isGetterCandidate);
+ toVisitGetter = !isGetterDefined;
+ }
- if (node.getGetterName() == null && getterName.startsWith("get") && isPrimitiveBoolean(node.getType())) {
- String altGetterName = "is" + capitalize(name);
- MethodNode altGetter = classNode.getGetterMethod(altGetterName, !node.isStatic());
- if (methodNeedsReplacement(altGetter)) {
- visitGetter(node, field, getterBlock, getterModifiers, altGetterName);
+ if (toVisitGetter) {
+ visitGetter(node, field, getterBlock, getterModifiers, getterName);
+
+ if (node.getGetterName() == null && getterName.startsWith("get") && isPrimitiveBoolean(node.getType())) {
+ String altGetterName = "is" + capitalize(name);
+ MethodNode altGetter = classNode.getGetterMethod(altGetterName, !node.isStatic());
+ if (methodNeedsReplacement(altGetter)) {
+ visitGetter(node, field, getterBlock, getterModifiers, altGetterName);
+ }
}
}
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
index d5a59e1..5cddcff 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/WriterController.java
@@ -19,6 +19,7 @@
package org.codehaus.groovy.classgen.asm;
import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.InterfaceHelperClassNode;
@@ -46,6 +47,7 @@ import static org.codehaus.groovy.ast.ClassHelper.isGeneratedFunction;
public class WriterController {
private static final boolean LOG_CLASSGEN = getBooleanSafe("groovy.log.classgen");
+ private static final String RECORD_CLASS_NAME = "java.lang.Record";
private AsmClassGenerator acg;
private MethodVisitor methodVisitor;
@@ -99,6 +101,11 @@ public class WriterController {
this.bytecodeVersion = chooseBytecodeVersion(invokedynamic, config.isPreviewFeatures(), config.getTargetBytecode());
+ if (bytecodeVersion >= Opcodes.V16 && cn.isRecord()) {
+ // `java.lang.Record` has been added since JDK16
+ cn.setSuperClass(ClassHelper.makeWithoutCaching(RECORD_CLASS_NAME));
+ }
+
if (invokedynamic) {
this.invocationWriter = new InvokeDynamicWriter(this);
this.callSiteWriter = new IndyCallSiteWriter(this);
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index 9dda68f..aa58a9c 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -29,6 +29,7 @@ import groovy.transform.TypeCheckingMode;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.ClosureSignatureConflictResolver;
import groovy.transform.stc.ClosureSignatureHint;
+import org.apache.groovy.ast.tools.MethodNodeUtils;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
@@ -139,7 +140,8 @@ import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.IntStream;
-import static java.util.stream.Collectors.*;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
import static org.apache.groovy.util.BeanUtils.capitalize;
import static org.apache.groovy.util.BeanUtils.decapitalize;
import static org.codehaus.groovy.ast.ClassHelper.AUTOCLOSEABLE_TYPE;
@@ -219,6 +221,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
import static org.codehaus.groovy.ast.tools.WideningCategories.isBigDecCategory;
@@ -4736,8 +4739,19 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
&& !mn.isStatic() && !mn.isAbstract() && Traits.isTrait(mn.getDeclaringClass()))
).forEach(methods::add);
}
+
if (receiver.isInterface()) {
methods.addAll(OBJECT_TYPE.getMethods(name));
+ } else if (receiver.isRecord()) {
+ if (methods.stream().noneMatch(MethodNodeUtils::isGetterCandidate)) {
+ PropertyNode p = receiver.getProperty(name);
+ if (null != p) {
+ MethodNode getter = new MethodNode(p.getGetterName(), Modifier.PUBLIC, p.getType(), Parameter.EMPTY_ARRAY,
+ ClassNode.EMPTY_ARRAY, block(returnS(varX(p.getName()))));
+ getter.setDeclaringClass(receiver);
+ methods.add(getter);
+ }
+ }
}
if (methods.isEmpty() || receiver.isResolved()) {
diff --git a/src/test-resources/core/RecordDeclaration_01x.groovy b/src/test-resources/core/RecordDeclaration_01x.groovy
new file mode 100644
index 0000000..c29f887
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_01x.groovy
@@ -0,0 +1,25 @@
+/*
+ * 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 core
+
+record Fruit(String name, double price) {}
+def apple = new Fruit('Apple', 11.6)
+assert 'Apple' == apple.name()
+assert 11.6 == apple.price()
+
diff --git a/src/test-resources/core/RecordDeclaration_02x.groovy b/src/test-resources/core/RecordDeclaration_02x.groovy
new file mode 100644
index 0000000..ca2ef07
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_02x.groovy
@@ -0,0 +1,33 @@
+/*
+ * 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 core
+
+interface Eatable {
+ String eat()
+}
+
+record Fruit(String name, double price) implements Eatable {
+ @Override
+ String eat() {
+ return "$name eaten, $price used"
+ }
+}
+
+def apple = new Fruit('Apple', 11.6)
+assert 'Apple eaten, 11.6 used' == apple.eat()
diff --git a/src/test-resources/core/RecordDeclaration_03x.groovy b/src/test-resources/core/RecordDeclaration_03x.groovy
new file mode 100644
index 0000000..99d036d
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_03x.groovy
@@ -0,0 +1,31 @@
+/*
+ * 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 core
+
+@groovy.transform.CompileStatic
+record Fruit(String name, double price) {}
+
+@groovy.transform.CompileStatic
+void test() {
+ def apple = new Fruit('Apple', 11.6D)
+ assert 'Apple' == apple.name()
+ assert 11.6 == apple.price()
+}
+
+test()
diff --git a/src/test-resources/core/RecordDeclaration_04x.groovy b/src/test-resources/core/RecordDeclaration_04x.groovy
new file mode 100644
index 0000000..51398b1
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_04x.groovy
@@ -0,0 +1,35 @@
+/*
+ * 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 core
+
+@groovy.transform.CompileStatic
+record Fruit(String name, double price) {
+ String record() {
+ def record = "$name, $price"
+ return record
+ }
+}
+
+@groovy.transform.CompileStatic
+void test() {
+ def apple = new Fruit('Apple', 11.6D)
+ assert 'Apple, 11.6' == apple.record()
+}
+
+test()
diff --git a/src/test-resources/core/RecordDeclaration_05x.groovy b/src/test-resources/core/RecordDeclaration_05x.groovy
new file mode 100644
index 0000000..dbea02b
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_05x.groovy
@@ -0,0 +1,32 @@
+/*
+ * 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 core
+
+record Fruit(String name, double price) {
+ String name() {
+ return name
+ }
+ double price() {
+ return price
+ }
+}
+
+def apple = new Fruit('Apple', 11.6D)
+assert 'Apple' == apple.name()
+assert 11.6 == apple.price()
diff --git a/src/test-resources/core/RecordDeclaration_06x.groovy b/src/test-resources/core/RecordDeclaration_06x.groovy
new file mode 100644
index 0000000..ce82969
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_06x.groovy
@@ -0,0 +1,38 @@
+/*
+ * 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 core
+
+@groovy.transform.CompileStatic
+record Fruit(String name, double price) {
+ String name() {
+ return name
+ }
+ double price() {
+ return price
+ }
+}
+
+@groovy.transform.CompileStatic
+void test() {
+ def apple = new Fruit('Apple', 11.6D)
+ assert 'Apple' == apple.name()
+ assert 11.6 == apple.price()
+}
+
+test()
diff --git a/src/test-resources/core/RecordDeclaration_07x.groovy b/src/test-resources/core/RecordDeclaration_07x.groovy
new file mode 100644
index 0000000..c1ed9cc
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_07x.groovy
@@ -0,0 +1,30 @@
+/*
+ * 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 core
+
+record Fruit(String name, double price) {
+ public static final String CONST_1 = 'C1'
+
+ String eat() {
+ return "$name, $price, $CONST_1"
+ }
+}
+
+assert 'C1' == Fruit.CONST_1
+assert 'apple, 11.6, C1' == new Fruit('apple', 11.6).eat()
diff --git a/src/test-resources/fail/RecordDeclaration_01x.groovy b/src/test-resources/fail/RecordDeclaration_01x.groovy
new file mode 100644
index 0000000..b9b887e
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_01x.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.
+ */
+package core
+
+record Fruit(String name, double price) extends Object {}
diff --git a/src/test-resources/fail/RecordDeclaration_02x.groovy b/src/test-resources/fail/RecordDeclaration_02x.groovy
new file mode 100644
index 0000000..25269ba
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_02x.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.
+ */
+package core
+
+class Fruit(String name, double price) {}
diff --git a/src/test-resources/fail/RecordDeclaration_03x.groovy b/src/test-resources/fail/RecordDeclaration_03x.groovy
new file mode 100644
index 0000000..53d6c1b
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_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.
+ */
+package core
+
+record Fruit {}
diff --git a/src/test-resources/fail/RecordDeclaration_04x.groovy b/src/test-resources/fail/RecordDeclaration_04x.groovy
new file mode 100644
index 0000000..a4f3871
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_04x.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.
+ */
+package core
+
+sealed record Fruit(String name) {}
diff --git a/src/test-resources/fail/RecordDeclaration_05x.groovy b/src/test-resources/fail/RecordDeclaration_05x.groovy
new file mode 100644
index 0000000..f673dbf
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_05x.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.
+ */
+package core
+
+non-sealed record Fruit(String name) {}
diff --git a/src/test-resources/fail/RecordDeclaration_06x.groovy b/src/test-resources/fail/RecordDeclaration_06x.groovy
new file mode 100644
index 0000000..80de1ad
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_06x.groovy
@@ -0,0 +1,23 @@
+/*
+ * 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 core
+
+record Fruit(String name) {
+ private String price
+}
diff --git a/src/test-resources/fail/RecordDeclaration_07x.groovy b/src/test-resources/fail/RecordDeclaration_07x.groovy
new file mode 100644
index 0000000..cbb8062
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_07x.groovy
@@ -0,0 +1,23 @@
+/*
+ * 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 core
+
+record Fruit(String name) {
+ String price
+}
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
new file mode 100644
index 0000000..b550c02
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
@@ -0,0 +1,39 @@
+/*
+ * 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 org.codehaus.groovy.classgen
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.isAtLeastJdk
+import static org.junit.Assume.assumeTrue
+
+@CompileStatic
+class RecordTest {
+ @Test
+ void testRecordOnJDK16plus() {
+ assumeTrue(isAtLeastJdk('16.0'))
+ assertScript(new GroovyShell(new CompilerConfiguration(targetBytecode: CompilerConfiguration.JDK16)), '''
+ record RecordJDK16plus(String name) {}
+ assert java.lang.Record == RecordJDK16plus.class.getSuperclass()
+ ''')
+ }
+}
diff --git a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index b2b3e90..5b4881a 100644
--- a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -357,6 +357,16 @@ final class GroovyParserTest extends GroovyTestCase {
doTest('core/TraitDeclaration_05.groovy')
}
+ void "test groovy core - RecordDeclaration"() {
+ doRunAndTestAntlr4('core/RecordDeclaration_01x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_02x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_03x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_04x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_05x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_06x.groovy')
+ doRunAndTestAntlr4('core/RecordDeclaration_07x.groovy')
+ }
+
void "test groovy core - AnnotationDeclaration"() {
doTest('core/AnnotationDeclaration_01.groovy')
}
diff --git a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index 0056180..a28ed02 100644
--- a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -427,6 +427,16 @@ final class SyntaxErrorTest extends GroovyTestCase {
TestUtils.shouldFail('fail/Trait_01.groovy')
}
+ void 'test groovy core - Record'() {
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_01x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_02x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_03x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_04x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_05x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_06x.groovy')
+ TestUtils.doRunAndShouldFail('fail/RecordDeclaration_07x.groovy')
+ }
+
void 'test groovy core - Array'() {
TestUtils.doRunAndShouldFail('fail/Array_01x.groovy')
TestUtils.doRunAndShouldFail('fail/Array_02x.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 25c8bab..7831e1e 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
@@ -90,6 +90,7 @@ import static org.apache.groovy.parser.antlr4.GroovyLexer.PERMITS;
import static org.apache.groovy.parser.antlr4.GroovyLexer.PRIVATE;
import static org.apache.groovy.parser.antlr4.GroovyLexer.PROTECTED;
import static org.apache.groovy.parser.antlr4.GroovyLexer.PUBLIC;
+import static org.apache.groovy.parser.antlr4.GroovyLexer.RECORD;
import static org.apache.groovy.parser.antlr4.GroovyLexer.RETURN;
import static org.apache.groovy.parser.antlr4.GroovyLexer.SEALED;
import static org.apache.groovy.parser.antlr4.GroovyLexer.SEMI;
@@ -381,7 +382,7 @@ public class SmartDocumentFilter extends DocumentFilter {
for (int t : Arrays.asList(AS, DEF, IN, TRAIT, THREADSAFE,
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, PACKAGE, PERMITS, PRIVATE, PROTECTED, PUBLIC, RETURN, SEALED, STATIC, STRICTFP, SUPER, SWITCH, SYNCHRONIZED,
+ NATIVE, NEW, NON_SEALED, PACKAGE, PERMITS, PRIVATE, PROTECTED, PUBLIC, RECORD, RETURN, SEALED, STATIC, STRICTFP, SUPER, SWITCH, SYNCHRONIZED,
THIS, THROW, THROWS, TRANSIENT, TRY, VOID, VOLATILE, WHILE, YIELD, NullLiteral, BooleanLiteral)) {
Style style = createDefaultStyleByTokenType(t);
StyleConstants.setBold(style, true);