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:00 UTC

[groovy] branch master updated (ccbc21d -> 3097295)

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git.


    from ccbc21d  revert try 17 also for github actions
     new 3b2cb50  GROOVY-10240: Support record grammar
     new 3097295  Support `record` compact constructor (closes #1620)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/antlr/GroovyLexer.g4                           |   2 +
 src/antlr/GroovyParser.g4                          |  11 +-
 .../apache/groovy/ast/tools/MethodNodeUtils.java   |  15 +++
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 150 +++++++++++++++++++--
 .../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 ++-
 ...tion_08.groovy => RecordDeclaration_01x.groovy} |  10 +-
 .../core/RecordDeclaration_02x.groovy}             |  18 +--
 .../core/RecordDeclaration_03x.groovy}             |  16 ++-
 .../RecordDeclaration_04x.groovy}                  |  28 ++--
 .../core/RecordDeclaration_05x.groovy}             |  15 ++-
 .../RecordDeclaration_06x.groovy}                  |  31 ++---
 ...nge_02x.groovy => RecordDeclaration_07x.groovy} |  13 +-
 .../core/RecordDeclaration_08x.groovy}             |  42 ++++--
 .../core/RecordDeclaration_09x.groovy              |  56 ++++++++
 ...tion_02x.groovy => ClassDeclaration_01x.groovy} |   0
 .../fail/ClassDeclaration_02x.groovy               |  13 +-
 .../RecordDeclaration_01x.groovy}                  |   3 +-
 .../RecordDeclaration_02x.groovy}                  |   3 +-
 .../RecordDeclaration_03x.groovy}                  |   3 +-
 .../RecordDeclaration_04x.groovy}                  |   3 +-
 .../RecordDeclaration_05x.groovy}                  |   3 +-
 .../RecordDeclaration_06x.groovy}                  |   5 +-
 .../RecordDeclaration_07x.groovy}                  |   5 +-
 .../RecordDeclaration_08x.groovy}                  |   9 +-
 .../codehaus/groovy/classgen/RecordTest.groovy}    |  26 ++--
 .../groovy/parser/antlr4/GroovyParserTest.groovy   |  12 ++
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  18 ++-
 .../console/ui/text/SmartDocumentFilter.java       |   3 +-
 31 files changed, 452 insertions(+), 118 deletions(-)
 copy src/test-resources/core/{ClassDeclaration_08.groovy => RecordDeclaration_01x.groovy} (85%)
 copy src/{main/java/org/codehaus/groovy/runtime/dgmimpl/arrays/ArrayPutAtMetaMethod.java => test-resources/core/RecordDeclaration_02x.groovy} (76%)
 copy src/{test/org/codehaus/groovy/transform/traitx/Groovy7215SupportTrait.groovy => test-resources/core/RecordDeclaration_03x.groovy} (79%)
 copy src/test-resources/{bugs/BUG-GROOVY-8613.groovy => core/RecordDeclaration_04x.groovy} (78%)
 copy src/{main/java/org/codehaus/groovy/runtime/wrappers/DoubleWrapper.java => test-resources/core/RecordDeclaration_05x.groovy} (80%)
 copy src/test-resources/{bugs/BUG-GROOVY-8613.groovy => core/RecordDeclaration_06x.groovy} (76%)
 copy src/test-resources/core/{BreakingChange_02x.groovy => RecordDeclaration_07x.groovy} (77%)
 copy src/{test/groovy/bugs/Groovy8339Bug.groovy => test-resources/core/RecordDeclaration_08x.groovy} (50%)
 create mode 100644 src/test-resources/core/RecordDeclaration_09x.groovy
 copy src/test-resources/fail/{ClassDeclaration_02x.groovy => ClassDeclaration_01x.groovy} (100%)
 copy src/test-resources/{core/Annotation_04.groovy => fail/RecordDeclaration_01x.groovy} (93%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_02x.groovy} (95%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_03x.groovy} (98%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_04x.groovy} (95%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_05x.groovy} (95%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_06x.groovy} (93%)
 copy src/test-resources/{core/Annotation_02.groovy => fail/RecordDeclaration_07x.groovy} (94%)
 copy src/test-resources/{core/ClassDeclaration_08.groovy => fail/RecordDeclaration_08x.groovy} (78%)
 copy src/test/groovy/{bugs/Groovy9271.groovy => org/codehaus/groovy/classgen/RecordTest.groovy} (64%)

[groovy] 01/02: GROOVY-10240: Support record grammar

Posted by pa...@apache.org.
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);

[groovy] 02/02: Support `record` compact constructor (closes #1620)

Posted by pa...@apache.org.
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 3097295fbde21675b8b007b3cf99515c30dfb3b8
Author: Daniel Sun <su...@apache.org>
AuthorDate: Wed Sep 22 16:43:42 2021 +0800

    Support `record` compact constructor (closes #1620)
---
 src/antlr/GroovyParser.g4                          |  5 ++
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 81 ++++++++++++++++++++--
 .../core/RecordDeclaration_08x.groovy              | 55 +++++++++++++++
 .../core/RecordDeclaration_09x.groovy              | 56 +++++++++++++++
 ...tion_02x.groovy => ClassDeclaration_01x.groovy} |  0
 .../fail/ClassDeclaration_02x.groovy               | 13 +++-
 ...ion_02x.groovy => RecordDeclaration_08x.groovy} | 10 ++-
 .../groovy/parser/antlr4/GroovyParserTest.groovy   |  2 +
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  8 ++-
 9 files changed, 218 insertions(+), 12 deletions(-)

diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 49d76c0..061d26d 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -264,6 +264,7 @@ memberDeclaration[int t]
     :   methodDeclaration[0, $t]
     |   fieldDeclaration
     |   modifiersOpt classDeclaration
+    |   compactConstructorDeclaration
     ;
 
 /**
@@ -283,6 +284,10 @@ methodDeclaration[int t, int ct]
         )?
     ;
 
+compactConstructorDeclaration
+    :   modifiers methodName nls methodBody
+    ;
+
 methodName
     :   identifier
     |   stringLiteral
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 67810ba..fcb04fc 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -21,9 +21,11 @@ package org.apache.groovy.parser.antlr4;
 import groovy.lang.Tuple2;
 import groovy.lang.Tuple3;
 import groovy.transform.CompileStatic;
+import groovy.transform.MapConstructor;
 import groovy.transform.NonSealed;
 import groovy.transform.Sealed;
 import groovy.transform.Trait;
+import groovy.transform.TupleConstructor;
 import org.antlr.v4.runtime.ANTLRErrorListener;
 import org.antlr.v4.runtime.CharStream;
 import org.antlr.v4.runtime.CharStreams;
@@ -38,6 +40,7 @@ import org.antlr.v4.runtime.misc.Interval;
 import org.antlr.v4.runtime.misc.ParseCancellationException;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.TerminalNode;
+import org.apache.groovy.internal.util.Function;
 import org.apache.groovy.parser.antlr4.GroovyParser.AdditiveExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.AndExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.AnnotatedQualifiedClassNameContext;
@@ -74,6 +77,7 @@ import org.apache.groovy.parser.antlr4.GroovyParser.ClosureOrLambdaExpressionCon
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandArgumentContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandExpressionContext;
+import org.apache.groovy.parser.antlr4.GroovyParser.CompactConstructorDeclarationContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CompilationUnitContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.ConditionalExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.ConditionalStatementContext;
@@ -292,7 +296,6 @@ 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;
@@ -356,9 +359,14 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.SUB;
 import static org.apache.groovy.parser.antlr4.GroovyParser.VAR;
 import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureAST;
 import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.cloneParams;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
@@ -368,6 +376,8 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 import static org.codehaus.groovy.classgen.asm.util.TypeUtil.isPrimitiveType;
 import static org.codehaus.groovy.runtime.DefaultGroovyMethods.asBoolean;
 import static org.codehaus.groovy.runtime.DefaultGroovyMethods.last;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
 
 /**
  * Builds the AST from the parse tree generated by Antlr4.
@@ -1512,8 +1522,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
         int modifiers = modifierManager.getClassModifiersOpValue();
 
-        boolean syntheticPublic = ((modifiers & Opcodes.ACC_SYNTHETIC) != 0);
-        modifiers &= ~Opcodes.ACC_SYNTHETIC;
+        boolean syntheticPublic = ((modifiers & ACC_SYNTHETIC) != 0);
+        modifiers &= ~ACC_SYNTHETIC;
 
         ClassNode classNode, outerClass = classNodeStack.peek();
 
@@ -1641,6 +1651,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     private void transformRecordHeaderToProperties(ClassDeclarationContext ctx, ClassNode classNode) {
         Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
+        classNode.putNodeMetaData(RECORD_HEADER, parameters);
         for (int i = 0; i < parameters.length; i++) {
             Parameter parameter = parameters[i];
             ModifierManager parameterModifierManager = parameter.getNodeMetaData(PARAMETER_MODIFIER_MANAGER);
@@ -1835,6 +1846,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         } else if (asBoolean(ctx.fieldDeclaration())) {
             ctx.fieldDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
             this.visitFieldDeclaration(ctx.fieldDeclaration());
+        } else if (asBoolean(ctx.compactConstructorDeclaration())) {
+            ctx.compactConstructorDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
+            this.visitCompactConstructorDeclaration(ctx.compactConstructorDeclaration());
         } else if (asBoolean(ctx.classDeclaration())) {
             ctx.classDeclaration().putNodeMetaData(TYPE_DECLARATION_MODIFIERS, this.visitModifiersOpt(ctx.modifiersOpt()));
             ctx.classDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
@@ -1929,6 +1943,61 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
+    public MethodNode visitCompactConstructorDeclaration(CompactConstructorDeclarationContext ctx) {
+        ClassNode classNode = ctx.getNodeMetaData(CLASS_DECLARATION_CLASS_NODE);
+        Objects.requireNonNull(classNode, "classNode should not be null");
+
+        if (!classNode.isRecord()) {
+            createParsingFailedException("Only `record` can have compact constructor", ctx);
+        }
+
+        ModifierManager modifierManager = new ModifierManager(this, this.visitModifiers(ctx.modifiers()));
+
+        if (modifierManager.containsAny(VAR)) {
+            throw createParsingFailedException("var cannot be used for compact constructor declaration", ctx);
+        }
+
+        String methodName = this.visitMethodName(ctx.methodName());
+        String className = classNode.getNodeMetaData(CLASS_NAME);
+        if (!methodName.equals(className)) {
+            createParsingFailedException("Compact constructor should have the same name with record: " + className, ctx.methodName());
+        }
+        ClassNode returnType = ClassHelper.VOID_TYPE;
+
+        Parameter[] header = classNode.getNodeMetaData(RECORD_HEADER);
+        Objects.requireNonNull(classNode, "record header should not be null");
+        Parameter[] parameters = cloneParams(header);
+        Statement code = this.visitMethodBody(ctx.methodBody());
+        MethodNode methodNode = classNode.addSyntheticMethod(RECORD_COMPACT_CONSTRUCTOR_NAME, ACC_PRIVATE, returnType, parameters, ClassNode.EMPTY_ARRAY, code);
+
+        modifierManager.attachAnnotations(methodNode);
+        attachMapConstructorAnnotationToRecord(classNode, parameters);
+        attachTupleConstructorAnnotationToRecord(classNode, parameters);
+
+        return methodNode;
+    }
+
+    private void attachMapConstructorAnnotationToRecord(ClassNode classNode, Parameter[] parameters) {
+        doAttachConstructorAnnotationToRecord(classNode, MapConstructor.class,
+                parameters, p -> castX(p.getOriginType(), callX(varX("args"), "get", args(constX(p.getName())))));
+    }
+
+    private void attachTupleConstructorAnnotationToRecord(ClassNode classNode, Parameter[] parameters) {
+        doAttachConstructorAnnotationToRecord(classNode, TupleConstructor.class,
+                parameters, p -> castX(p.getOriginType(), varX(p.getName())));
+    }
+
+    private void doAttachConstructorAnnotationToRecord(ClassNode classNode, Class<?> annotationClass, Parameter[] parameters, Function<? super Parameter, ? extends Expression> mapper) {
+        AnnotationNode tupleConstructorAnnotationNode = new AnnotationNode(ClassHelper.makeCached(annotationClass));
+        List<Expression> argExpressionList =
+                Arrays.stream(parameters)
+                        .map(mapper::apply)
+                        .collect(Collectors.toList());
+        tupleConstructorAnnotationNode.setMember("pre", closureX(block(stmt(callX(varX("this"), RECORD_COMPACT_CONSTRUCTOR_NAME, args(argExpressionList))))));
+        classNode.addAnnotation(tupleConstructorAnnotationNode);
+    }
+
+    @Override
     public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) {
         ModifierManager modifierManager = createModifierManager(ctx);
 
@@ -2043,7 +2112,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     private MethodNode createScriptMethodNode(final ModifierManager modifierManager, final String methodName, final ClassNode returnType, final Parameter[] parameters, final ClassNode[] exceptions, final Statement code) {
         MethodNode methodNode = new MethodNode(
                 methodName,
-                modifierManager.containsAny(PRIVATE) ? Opcodes.ACC_PRIVATE : Opcodes.ACC_PUBLIC,
+                modifierManager.containsAny(PRIVATE) ? ACC_PRIVATE : Opcodes.ACC_PUBLIC,
                 returnType,
                 parameters,
                 exceptions,
@@ -2300,7 +2369,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             classNode.addProperty(propertyNode);
 
             fieldNode = propertyNode.getField();
-            fieldNode.setModifiers(modifiers & ~Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE);
+            fieldNode.setModifiers(modifiers & ~Opcodes.ACC_PUBLIC | ACC_PRIVATE);
             fieldNode.setSynthetic(!classNode.isInterface());
             modifierManager.attachAnnotations(fieldNode);
             modifierManager.attachAnnotations(propertyNode);
@@ -5158,5 +5227,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     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_HEADER = "_RECORD_HEADER";
     private static final String RECORD_TYPE_NAME = "groovy.transform.RecordType";
+    private static final String RECORD_COMPACT_CONSTRUCTOR_NAME = "$compactInit";
 }
diff --git a/src/test-resources/core/RecordDeclaration_08x.groovy b/src/test-resources/core/RecordDeclaration_08x.groovy
new file mode 100644
index 0000000..d1ced5d
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_08x.groovy
@@ -0,0 +1,55 @@
+/*
+ *  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 Person(String name, int age) {
+    public Person {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
+}
+
+assert 'core.Person(name:Daniel, age:37)' == new Person('Daniel', 37).toString()
+try {
+    new Person('Peter', 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person('Devil', 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
+
+try {
+    new Person(name: 'Peter', age: 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person(name: 'Devil', age: 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
diff --git a/src/test-resources/core/RecordDeclaration_09x.groovy b/src/test-resources/core/RecordDeclaration_09x.groovy
new file mode 100644
index 0000000..53027b1
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_09x.groovy
@@ -0,0 +1,56 @@
+/*
+ *  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 Person(String name, int age) {
+    public Person {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
+}
+
+assert 'core.Person(name:Daniel, age:37)' == new Person('Daniel', 37).toString()
+try {
+    new Person('Peter', 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person('Devil', 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
+
+try {
+    new Person(name: 'Peter', age: 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person(name: 'Devil', age: 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
diff --git a/src/test-resources/fail/ClassDeclaration_02x.groovy b/src/test-resources/fail/ClassDeclaration_01x.groovy
similarity index 100%
copy from src/test-resources/fail/ClassDeclaration_02x.groovy
copy to src/test-resources/fail/ClassDeclaration_01x.groovy
diff --git a/src/test-resources/fail/ClassDeclaration_02x.groovy b/src/test-resources/fail/ClassDeclaration_02x.groovy
index 662642d..5e006e3 100644
--- a/src/test-resources/fail/ClassDeclaration_02x.groovy
+++ b/src/test-resources/fail/ClassDeclaration_02x.groovy
@@ -16,7 +16,14 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-class Foo {}
-new Foo() {
-    Foo() {}
+package fail
+
+class Person {
+    String name
+    int age
+
+    public Person {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
 }
diff --git a/src/test-resources/fail/ClassDeclaration_02x.groovy b/src/test-resources/fail/RecordDeclaration_08x.groovy
similarity index 76%
copy from src/test-resources/fail/ClassDeclaration_02x.groovy
copy to src/test-resources/fail/RecordDeclaration_08x.groovy
index 662642d..57a1f98 100644
--- a/src/test-resources/fail/ClassDeclaration_02x.groovy
+++ b/src/test-resources/fail/RecordDeclaration_08x.groovy
@@ -16,7 +16,11 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-class Foo {}
-new Foo() {
-    Foo() {}
+package core
+
+record Person(String name, int age) {
+    public Person123 {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
 }
diff --git a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index 5b4881a..e1dabc4 100644
--- a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -365,6 +365,8 @@ final class GroovyParserTest extends GroovyTestCase {
         doRunAndTestAntlr4('core/RecordDeclaration_05x.groovy')
         doRunAndTestAntlr4('core/RecordDeclaration_06x.groovy')
         doRunAndTestAntlr4('core/RecordDeclaration_07x.groovy')
+        doRunAndTestAntlr4('core/RecordDeclaration_08x.groovy')
+        doRunAndTestAntlr4('core/RecordDeclaration_09x.groovy')
     }
 
     void "test groovy core - AnnotationDeclaration"() {
diff --git a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index a28ed02..3a660ee 100644
--- a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -192,10 +192,15 @@ final class SyntaxErrorTest extends GroovyTestCase {
     }
 
     void 'test groovy core - ClassDeclaration 1'() {
-        TestUtils.doRunAndShouldFail('fail/ClassDeclaration_02x.groovy')
+        TestUtils.doRunAndShouldFail('fail/ClassDeclaration_01x.groovy')
     }
 
     void 'test groovy core - ClassDeclaration 2'() {
+        TestUtils.doRunAndShouldFail('fail/ClassDeclaration_02x.groovy')
+    }
+
+
+    void 'test groovy core - ClassDeclaration 3'() {
         def err = expectParseError '''\
             |class C extends Object, Number {}
             |'''.stripMargin()
@@ -435,6 +440,7 @@ final class SyntaxErrorTest extends GroovyTestCase {
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_05x.groovy')
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_06x.groovy')
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_07x.groovy')
+        TestUtils.doRunAndShouldFail('fail/RecordDeclaration_08x.groovy')
     }
 
     void 'test groovy core - Array'() {