You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by em...@apache.org on 2022/11/23 00:12:37 UTC

[groovy] branch master updated: GROOVY-10854: control `record` generation using `@RecordType` annotation

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8e586cfa53 GROOVY-10854: control `record` generation using `@RecordType` annotation
8e586cfa53 is described below

commit 8e586cfa53a91ed8de7d29db32d1d12c81ed3d06
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Tue Nov 22 17:56:16 2022 -0600

    GROOVY-10854: control `record` generation using `@RecordType` annotation
---
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 438 ++++-------------
 .../org/codehaus/groovy/classgen/RecordTest.groovy | 535 +++++++++++----------
 2 files changed, 393 insertions(+), 580 deletions(-)

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 6bf6fba056..d35969cc80 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -20,11 +20,7 @@ package org.apache.groovy.parser.antlr4;
 
 import groovy.lang.Tuple2;
 import groovy.lang.Tuple3;
-import groovy.transform.CompileStatic;
-import groovy.transform.NonSealed;
-import groovy.transform.Sealed;
-import groovy.transform.Trait;
-import groovy.transform.TupleConstructor;
+import groovy.transform.*;
 import org.antlr.v4.runtime.ANTLRErrorListener;
 import org.antlr.v4.runtime.CharStream;
 import org.antlr.v4.runtime.CharStreams;
@@ -39,177 +35,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.ast.tools.AnnotatedNodeUtils;
-import org.apache.groovy.parser.antlr4.GroovyParser.AdditiveExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AndExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AnnotatedQualifiedClassNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AnnotationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AnnotationNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AnnotationsOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AnonymousInnerClassDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ArgumentsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ArrayInitializerContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AssertStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.AssignmentExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BlockContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BlockStatementsOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BooleanLiteralAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BreakStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.BuiltInTypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CastExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CastParExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CatchClauseContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CatchTypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassBodyContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassBodyDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifierContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifiersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceModifiersOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassOrInterfaceTypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClassicalForControlContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClosureContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ClosureOrLambdaExpressionContext;
-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;
-import org.apache.groovy.parser.antlr4.GroovyParser.ContinueStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CreatedNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.CreatorContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.DimContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.DoWhileStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.DynamicMemberNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ElementValueArrayInitializerContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ElementValueContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ElementValuePairContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ElementValuePairsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ElementValuesContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EmptyDimsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EmptyDimsOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnhancedArgumentListElementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnhancedArgumentListInParContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnhancedForControlContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnhancedStatementExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnumConstantContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EnumConstantsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.EqualityExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ExclusiveOrExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ExpressionInParContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ExpressionListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ExpressionListElementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FieldDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FinallyBlockContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FloatingPointLiteralAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ForControlContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ForInitContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ForStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ForUpdateContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FormalParameterContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FormalParameterListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.FormalParametersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.GroovyParserRuleContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.GstringContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.GstringPathContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.GstringValueContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.IdentifierContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.IdentifierPrmrAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.IfElseStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ImportDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.InclusiveOrExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.IndexPropertyArgsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.IntegerLiteralAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.KeywordsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LabeledStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LambdaBodyContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LocalVariableDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LogicalAndExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LogicalOrExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.LoopStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MapContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MapEntryContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MapEntryLabelContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MapEntryListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MemberDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MethodBodyContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MethodDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MethodNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ModifierContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ModifiersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ModifiersOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MultipleAssignmentExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.MultiplicativeExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.NamePartContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.NamedPropertyArgsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.NewPrmrAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.NonWildcardTypeArgumentsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.NullLiteralAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PackageDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ParExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PathElementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PathExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PostfixExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PowerExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.PrimitiveTypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.QualifiedClassNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.QualifiedClassNameListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.QualifiedNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.QualifiedNameElementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.QualifiedStandardClassNameContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.RegexExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.RelationalExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ResourceContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ResourceListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ResourcesContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ReturnStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ReturnTypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ScriptStatementsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ShiftExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.StandardLambdaExpressionContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.StandardLambdaParametersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.StatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.StringLiteralContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.SuperPrmrAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.SwitchBlockStatementGroupContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.SwitchLabelContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.SwitchStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.SynchronizedStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ThisFormalParameterContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ThisPrmrAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.ThrowStmtAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TryCatchStatementContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeArgumentsOrDiamondContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeBoundContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeListContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeNamePairContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeNamePairsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeParameterContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.TypeParametersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.UnaryAddExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.UnaryNotExprAltContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclarationContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorIdContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableDeclaratorsContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableInitializerContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableInitializersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableModifierContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableModifiersContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableModifiersOptContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.VariableNamesContext;
-import org.apache.groovy.parser.antlr4.GroovyParser.WhileStmtAltContext;
+import org.apache.groovy.parser.antlr4.GroovyParser.*;
 import org.apache.groovy.parser.antlr4.internal.DescriptiveErrorStrategy;
 import org.apache.groovy.parser.antlr4.internal.atnmanager.AtnManager;
 import org.apache.groovy.parser.antlr4.util.StringUtils;
@@ -322,14 +148,8 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import static groovy.lang.Tuple.tuple;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.ARROW;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.SwitchBlockStatementExpressionGroupContext;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.SwitchExprAltContext;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.SwitchExpressionContext;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.SwitchExpressionLabelContext;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.YieldStatementContext;
-import static org.apache.groovy.parser.antlr4.GroovyLangParser.YieldStmtAltContext;
 import static org.apache.groovy.parser.antlr4.GroovyParser.ADD;
+import static org.apache.groovy.parser.antlr4.GroovyParser.ARROW;
 import static org.apache.groovy.parser.antlr4.GroovyParser.AS;
 import static org.apache.groovy.parser.antlr4.GroovyParser.CASE;
 import static org.apache.groovy.parser.antlr4.GroovyParser.DEC;
@@ -357,10 +177,8 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.STATIC;
 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.assignX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
-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.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
@@ -665,17 +483,14 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     @Override
     public Statement visitLoopStmtAlt(final LoopStmtAltContext ctx) {
-        visitingLoopStatementCount += 1;
         switchExpressionRuleContextStack.push(ctx);
-        Statement result;
+        visitingLoopStatementCount += 1;
         try {
-            result = configureAST((Statement) this.visit(ctx.loopStatement()), ctx);
+            return configureAST((Statement) this.visit(ctx.loopStatement()), ctx);
         } finally {
             switchExpressionRuleContextStack.pop();
+            visitingLoopStatementCount -= 1;
         }
-        visitingLoopStatementCount -= 1;
-
-        return result;
     }
 
     @Override
@@ -748,10 +563,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     @Override
     public Tuple2<Parameter, Expression> visitEnhancedForControl(final EnhancedForControlContext ctx) {
         Parameter parameter = new Parameter(this.visitType(ctx.type()), this.visitVariableDeclaratorId(ctx.variableDeclaratorId()).getName());
+        configureAST(parameter, ctx.variableDeclaratorId());
         ModifierManager modifierManager = new ModifierManager(this, this.visitVariableModifiersOpt(ctx.variableModifiersOpt()));
         modifierManager.processParameter(parameter);
-
-        return tuple(configureAST(parameter, ctx.variableDeclaratorId()), (Expression) this.visit(ctx.expression()));
+        return tuple(parameter, (Expression) this.visit(ctx.expression()));
     }
 
     @Override
@@ -933,10 +748,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     @Override
     public SwitchStatement visitSwitchStatement(final SwitchStatementContext ctx) {
-        visitingSwitchStatementCount += 1;
         switchExpressionRuleContextStack.push(ctx);
-
-        SwitchStatement result;
+        visitingSwitchStatementCount += 1;
         try {
             List<Statement> statementList =
                     ctx.switchBlockStatementGroup().stream()
@@ -949,13 +762,13 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             List<CaseStatement> caseStatementList = new LinkedList<>();
             List<Statement> defaultStatementList = new LinkedList<>();
 
-            statementList.forEach(e -> {
+            for (Statement e : statementList) {
                 if (e instanceof CaseStatement) {
                     caseStatementList.add((CaseStatement) e);
                 } else if (isTrue(e, IS_SWITCH_DEFAULT)) {
                     defaultStatementList.add(e);
                 }
-            });
+            }
 
             int defaultStatementListSize = defaultStatementList.size();
             if (defaultStatementListSize > 1) {
@@ -966,7 +779,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                 throw createParsingFailedException("a default branch must only appear as the last branch of a switch", defaultStatementList.get(0));
             }
 
-            result = configureAST(
+            return configureAST(
                     new SwitchStatement(
                             this.visitExpressionInPar(ctx.expressionInPar()),
                             caseStatementList,
@@ -975,62 +788,42 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                     ctx);
         } finally {
             switchExpressionRuleContextStack.pop();
+            visitingSwitchStatementCount -= 1;
         }
-
-        visitingSwitchStatementCount -= 1;
-
-        return result;
     }
 
     @Override @SuppressWarnings("unchecked")
     public List<Statement> visitSwitchBlockStatementGroup(final SwitchBlockStatementGroupContext ctx) {
-        int labelCnt = ctx.switchLabel().size();
+        int labelCount = ctx.switchLabel().size();
         List<Token> firstLabelHolder = new ArrayList<>(1);
 
         return (List<Statement>) ctx.switchLabel().stream()
                 .map(e -> (Object) this.visitSwitchLabel(e))
                 .reduce(new ArrayList<Statement>(4), (r, e) -> {
+                    Statement statement;
                     List<Statement> statementList = (List<Statement>) r;
                     Tuple2<Token, Expression> tuple = (Tuple2<Token, Expression>) e;
-
-                    boolean isLast = labelCnt - 1 == statementList.size();
-
                     switch (tuple.getV1().getType()) {
-                        case CASE: {
-                            if (!asBoolean(statementList)) {
-                                firstLabelHolder.add(tuple.getV1());
-                            }
-
-                            statementList.add(
-                                    configureAST(
-                                            new CaseStatement(
-                                                    tuple.getV2(),
-
-                                                    // check whether processing the last label. if yes, block statement should be attached.
-                                                    isLast ? this.visitBlockStatements(ctx.blockStatements())
-                                                            : EmptyStatement.INSTANCE
-                                            ),
-                                            firstLabelHolder.get(0)));
-
-                            break;
-                        }
-                        case DEFAULT: {
-
-                            BlockStatement blockStatement = this.visitBlockStatements(ctx.blockStatements());
-                            blockStatement.putNodeMetaData(IS_SWITCH_DEFAULT, true);
-
-                            statementList.add(
-                                    // this.configureAST(blockStatement, tuple.getKey())
-                                    blockStatement
-                            );
-
-                            break;
+                      case CASE:
+                        if (!asBoolean(statementList)) {
+                            firstLabelHolder.add(tuple.getV1());
                         }
+                        statement = new CaseStatement(
+                                tuple.getV2(),
+                                // check whether processing the last label; if yes, block statement should be attached
+                                (statementList.size() == labelCount - 1) ? this.visitBlockStatements(ctx.blockStatements()) : EmptyStatement.INSTANCE
+                        );
+                        statementList.add(configureAST(statement, firstLabelHolder.get(0)));
+                        break;
+
+                      case DEFAULT:
+                        statement = this.visitBlockStatements(ctx.blockStatements());
+                        statement.putNodeMetaData(IS_SWITCH_DEFAULT, Boolean.TRUE);
+                        statementList.add(statement);
+                        break;
                     }
-
                     return statementList;
                 });
-
     }
 
     @Override
@@ -1053,8 +846,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     @Override
     public ReturnStatement visitReturnStmtAlt(final ReturnStmtAltContext ctx) {
-        GroovyParserRuleContext gprc = switchExpressionRuleContextStack.peek();
-        if (gprc instanceof SwitchExpressionContext) {
+        if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
             throw createParsingFailedException("switch expression does not support `return`", ctx);
         }
 
@@ -1082,12 +874,11 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     @Override
     public BreakStatement visitBreakStatement(final BreakStatementContext ctx) {
-        if (0 == visitingLoopStatementCount && 0 == visitingSwitchStatementCount) {
+        if (visitingLoopStatementCount == 0 && visitingSwitchStatementCount == 0) {
             throw createParsingFailedException("break statement is only allowed inside loops or switches", ctx);
         }
 
-        GroovyParserRuleContext gprc = switchExpressionRuleContextStack.peek();
-        if (gprc instanceof SwitchExpressionContext) {
+        if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
             throw createParsingFailedException("switch expression does not support `break`", ctx);
         }
 
@@ -1099,14 +890,14 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
-    public ReturnStatement visitYieldStatement(YieldStatementContext ctx) {
+    public ReturnStatement visitYieldStatement(final YieldStatementContext ctx) {
         ReturnStatement returnStatement = (ReturnStatement) returnS((Expression) this.visit(ctx.expression()));
-        returnStatement.putNodeMetaData(IS_YIELD_STATEMENT, true);
+        returnStatement.putNodeMetaData(IS_YIELD_STATEMENT, Boolean.TRUE);
         return configureAST(returnStatement, ctx);
     }
 
     @Override
-    public ReturnStatement visitYieldStmtAlt(YieldStmtAltContext ctx) {
+    public ReturnStatement visitYieldStmtAlt(final YieldStmtAltContext ctx) {
         return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
     }
 
@@ -1116,8 +907,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             throw createParsingFailedException("continue statement is only allowed inside loops", ctx);
         }
 
-        GroovyParserRuleContext gprc = switchExpressionRuleContextStack.peek();
-        if (gprc instanceof SwitchExpressionContext) {
+        if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
             throw createParsingFailedException("switch expression does not support `continue`", ctx);
         }
 
@@ -1130,7 +920,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
-    public Expression visitSwitchExprAlt(SwitchExprAltContext ctx) {
+    public Expression visitSwitchExprAlt(final SwitchExprAltContext ctx) {
         return configureAST(this.visitSwitchExpression(ctx.switchExpression()), ctx);
     }
 
@@ -1158,7 +948,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
      * @return {@link MethodCallExpression} instance
      */
     @Override
-    public MethodCallExpression visitSwitchExpression(SwitchExpressionContext ctx) {
+    public MethodCallExpression visitSwitchExpression(final SwitchExpressionContext ctx) {
         switchExpressionRuleContextStack.push(ctx);
         try {
             validateSwitchExpressionLabels(ctx);
@@ -1459,31 +1249,29 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             }
         }
 
-        List<ModifierNode> modifierNodeList = ctx.getNodeMetaData(TYPE_DECLARATION_MODIFIERS);
-        Objects.requireNonNull(modifierNodeList, "modifierNodeList should not be null");
-        ModifierManager modifierManager = new ModifierManager(this, modifierNodeList);
+        ModifierManager modifierManager = new ModifierManager(this, ctx.getNodeMetaData(TYPE_DECLARATION_MODIFIERS));
 
-        Optional<ModifierNode> finalModifierNodeOptional = modifierManager.get(FINAL);
-        Optional<ModifierNode> sealedModifierNodeOptional = modifierManager.get(SEALED);
-        Optional<ModifierNode> nonSealedModifierNodeOptional = modifierManager.get(NON_SEALED);
-        boolean isFinal = finalModifierNodeOptional.isPresent();
-        boolean isSealed = sealedModifierNodeOptional.isPresent();
-        boolean isNonSealed = nonSealedModifierNodeOptional.isPresent();
+        Optional<ModifierNode> finalModifier = modifierManager.get(FINAL);
+        Optional<ModifierNode> sealedModifier = modifierManager.get(SEALED);
+        Optional<ModifierNode> nonSealedModifier = modifierManager.get(NON_SEALED);
+        boolean isFinal = finalModifier.isPresent();
+        boolean isSealed = sealedModifier.isPresent();
+        boolean isNonSealed = nonSealedModifier.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 (asBoolean(ctx.EXTENDS())) {
+                throw createParsingFailedException("No extends clause allowed for record declaration", ctx.EXTENDS());
+            }
             if (isSealed) {
-                throw createParsingFailedException("`sealed` is not allowed for record declaration", sealedModifierNodeOptional.get());
+                throw createParsingFailedException("`sealed` is not allowed for record declaration", sealedModifier.get());
             }
             if (isNonSealed) {
-                throw createParsingFailedException("`non-sealed` is not allowed for record declaration", nonSealedModifierNodeOptional.get());
+                throw createParsingFailedException("`non-sealed` is not allowed for record declaration", nonSealedModifier.get());
             }
         } else {
             if (hasRecordHeader) {
@@ -1492,15 +1280,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         }
 
         if (isSealed && isNonSealed) {
-            throw createParsingFailedException("type cannot be defined with both `sealed` and `non-sealed`", nonSealedModifierNodeOptional.get());
+            throw createParsingFailedException("type cannot be defined with both `sealed` and `non-sealed`", nonSealedModifier.get());
         }
 
         if (isFinal && (isSealed || isNonSealed)) {
-            throw createParsingFailedException("type cannot be defined with both " + (isSealed ? "`sealed`" : "`non-sealed`") + " and `final`", finalModifierNodeOptional.get());
+            throw createParsingFailedException("type cannot be defined with both " + (isSealed ? "`sealed`" : "`non-sealed`") + " and `final`", finalModifier.get());
         }
 
         if ((isAnnotation || isEnum) && (isSealed || isNonSealed)) {
-            ModifierNode mn = isSealed ? sealedModifierNodeOptional.get() : nonSealedModifierNodeOptional.get();
+            ModifierNode mn = isSealed ? sealedModifier.get() : nonSealedModifier.get();
             throw createParsingFailedException("modifier `" + mn.getText() + "` is not allowed for " + (isEnum ? "enum" : "annotation definition"), mn);
         }
 
@@ -1562,10 +1350,11 @@ 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(RECORD_TYPE_CLASS));
-        }
         classNode.addAnnotations(modifierManager.getAnnotations());
+        if (isRecord && classNode.getAnnotations().stream().noneMatch(a ->
+                        a.getClassNode().getName().equals(RECORD_TYPE_NAME))) {
+            classNode.addAnnotation(new AnnotationNode(ClassHelper.makeWithoutCaching(RECORD_TYPE_NAME)));
+        }
 
         if (isInterfaceWithDefaultMethods) {
             classNode.putNodeMetaData(IS_INTERFACE_WITH_DEFAULT_METHODS, Boolean.TRUE);
@@ -1597,7 +1386,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             classNode.setInterfaces(this.visitTypeList(ctx.is));
             this.initUsingGenerics(classNode);
             if (isRecord) {
-                transformRecordHeaderToProperties(ctx, classNode);
+                this.transformRecordHeaderToProperties(ctx, classNode);
             }
 
         } else if (isAnnotation) {
@@ -1621,12 +1410,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         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());
-            }
+            classNode.getFields().stream().filter(f -> !isTrue(f, IS_RECORD_GENERATED) && !f.isStatic()).findFirst()
+                    .ifPresent(fn -> this.createParsingFailedException("Instance field is not allowed in `record`", fn));
         }
         classNodeStack.pop();
 
@@ -1808,8 +1593,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     @Override
     public Void visitClassBodyDeclaration(final ClassBodyDeclarationContext ctx) {
         ClassNode classNode = ctx.getNodeMetaData(CLASS_DECLARATION_CLASS_NODE);
-        Objects.requireNonNull(classNode, "classNode should not be null");
-
         if (asBoolean(ctx.memberDeclaration())) {
             ctx.memberDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
             this.visitMemberDeclaration(ctx.memberDeclaration());
@@ -1818,11 +1601,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
             if (asBoolean(ctx.STATIC())) { // e.g. static { }
                 classNode.addStaticInitializerStatements(Collections.singletonList(statement), false);
-            } else { // e.g.  { }
-                classNode.addObjectInitializerStatements(
-                        configureAST(
-                                this.createBlockStatement(statement),
-                                statement));
+            } else { // e.g. { }
+                classNode.addObjectInitializerStatements(configureAST(this.createBlockStatement(statement), statement));
             }
         }
 
@@ -1938,56 +1718,48 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
-    public MethodNode visitCompactConstructorDeclaration(CompactConstructorDeclarationContext ctx) {
+    public MethodNode visitCompactConstructorDeclaration(final CompactConstructorDeclarationContext ctx) {
         ClassNode classNode = ctx.getNodeMetaData(CLASS_DECLARATION_CLASS_NODE);
-        Objects.requireNonNull(classNode, "classNode should not be null");
 
-        if (!AnnotatedNodeUtils.hasAnnotation(classNode, RECORD_TYPE_CLASS)) {
-            createParsingFailedException("Only `record` can have compact constructor", ctx);
+        if (classNode.getAnnotations().stream().noneMatch(a -> a.getClassNode().getName().equals(RECORD_TYPE_NAME))) {
+            createParsingFailedException("Only record can have compact constructor", ctx);
         }
 
-        List<ModifierNode> modifierNodeList = ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS);
-        Objects.requireNonNull(modifierNodeList, "modifierNodeList should not be null");
-        ModifierManager modifierManager = new ModifierManager(this, modifierNodeList);
-
-        if (modifierManager.containsAny(VAR)) {
+        if (new ModifierManager(this, ctx.getNodeMetaData(COMPACT_CONSTRUCTOR_DECLARATION_MODIFIERS)).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());
+            createParsingFailedException("Compact constructor should have the same name as record: " + className, ctx.methodName());
         }
 
         Parameter[] header = classNode.getNodeMetaData(RECORD_HEADER);
-        Objects.requireNonNull(header, "record header should not be null");
-
-        final Parameter[] parameters = cloneParams(header);
         Statement code = this.visitMethodBody(ctx.methodBody());
         code.visit(new CodeVisitorSupport() {
             @Override
-            public void visitPropertyExpression(PropertyExpression expression) {
-                final String propertyName = expression.getPropertyAsString();
-                if (THIS_STR.equals(expression.getObjectExpression().getText()) && Arrays.stream(parameters).anyMatch(p -> p.getName().equals(propertyName))) {
-                    createParsingFailedException("Cannot assign a value to final variable `" + propertyName + "`", expression.getProperty());
+            public void visitPropertyExpression(final PropertyExpression expression) {
+                String receiverText = expression.getObjectExpression().getText();
+                String propertyName = expression.getPropertyAsString();
+                if (THIS_STR.equals(receiverText) && Arrays.stream(header).anyMatch(p -> p.getName().equals(propertyName))) {
+                    createParsingFailedException("Cannot assign a value to final variable '" + propertyName + "'", expression.getProperty());
                 }
                 super.visitPropertyExpression(expression);
             }
         });
 
-        attachTupleConstructorAnnotationToRecord(classNode, parameters, code);
-        return null;
-    }
-
-    private void attachTupleConstructorAnnotationToRecord(ClassNode classNode, Parameter[] parameters, Statement block) {
-        ClassNode tupleConstructorType = ClassHelper.makeCached(TupleConstructor.class);
-        List<AnnotationNode> annos = classNode.getAnnotations(tupleConstructorType);
-        AnnotationNode tupleConstructor = annos.isEmpty() ? new AnnotationNode(tupleConstructorType) : annos.get(0);
-        tupleConstructor.setMember("pre", closureX(block));
+        ClassNode tupleConstructor = ClassHelper.makeCached(TupleConstructor.class);
+        List<AnnotationNode> annos = classNode.getAnnotations(tupleConstructor);
         if (annos.isEmpty()) {
-            classNode.addAnnotation(tupleConstructor);
+            AnnotationNode tcAnnotation = new AnnotationNode(tupleConstructor);
+            tcAnnotation.setMember("pre", closureX(code));
+            classNode.addAnnotation(tcAnnotation);
+        } else {
+            annos.get(0).setMember("pre", closureX(code));
         }
+
+        return null;
     }
 
     @Override
@@ -2388,7 +2160,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         FieldNode fieldNode;
         PropertyNode propertyNode = classNode.getProperty(fieldName);
 
-        if (null != propertyNode && propertyNode.getField().isSynthetic()) {
+        if (propertyNode != null && propertyNode.getField().isSynthetic()) {
             if (propertyNode.hasInitialExpression() && initialValue != null) {
                 throw createParsingFailedException("The split property definition named '" + fieldName + "' must not have an initial value for both the field and the property", ctx);
             }
@@ -3349,7 +3121,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     @Override
     public Expression visitRelationalExprAlt(final RelationalExprAltContext ctx) {
         switch (ctx.op.getType()) {
-        case AS:
+          case AS:
             Expression expr = (Expression) this.visit(ctx.left);
             if (expr instanceof VariableExpression && ((VariableExpression) expr).isSuperExpression()) {
                 this.createParsingFailedException("Cannot cast or coerce `super`", ctx); // GROOVY-9391
@@ -3357,8 +3129,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             CastExpression cast = CastExpression.asExpression(this.visitType(ctx.type()), expr);
             return configureAST(cast, ctx);
 
-        case INSTANCEOF:
-        case NOT_INSTANCEOF:
+          case INSTANCEOF:
+          case NOT_INSTANCEOF:
             ctx.type().putNodeMetaData(IS_INSIDE_INSTANCEOF_EXPR, Boolean.TRUE);
             return configureAST(
                     new BinaryExpression(
@@ -3367,15 +3139,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                             configureAST(new ClassExpression(this.visitType(ctx.type())), ctx.type())),
                     ctx);
 
-        case GT:
-        case GE:
-        case LT:
-        case LE:
-        case IN:
-        case NOT_IN:
+          case GT:
+          case GE:
+          case LT:
+          case LE:
+          case IN:
+          case NOT_IN:
             return this.createBinaryExpression(ctx.left, ctx.op, ctx.right, ctx);
 
-        default:
+          default:
             throw this.createParsingFailedException("Unsupported relational expression: " + ctx.getText(), ctx);
         }
     }
@@ -3573,9 +3345,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                 return configureAST(constructorCallExpression, ctx);
             }
 
-            return configureAST(
-                    new ConstructorCallExpression(classNode, arguments),
-                    ctx);
+            ConstructorCallExpression constructorCallExpression = new ConstructorCallExpression(classNode, arguments);
+            return configureAST(constructorCallExpression, ctx);
         }
 
         if (asBoolean(ctx.dim())) { // create array
@@ -3647,7 +3418,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                                         dimWithExprList.stream().map(Tuple3::getV1),
                                         Arrays.stream(empties)
                                 ).collect(Collectors.toList()));
-
             }
 
             arrayExpression.setType(
@@ -3689,7 +3459,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         InnerClassNode anonymousInnerClass;
         if (ctx.t == 1) {
             anonymousInnerClass = new EnumConstantClassNode(outerClass, innerClassName, superClass.getPlainNodeReference());
-            // and remove the final modifier from classNode to allow the subclass
+            // and remove the final modifier from superClass to allow the sub class
             superClass.setModifiers(superClass.getModifiers() & ~Opcodes.ACC_FINAL);
         } else {
             anonymousInnerClass = new InnerClassNode(outerClass, innerClassName, Opcodes.ACC_PUBLIC, superClass);
@@ -4104,9 +3874,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     @Override
     public ClosureExpression visitClosure(final ClosureContext ctx) {
         switchExpressionRuleContextStack.push(ctx);
+        visitingClosureCount += 1;
         try {
-            visitingClosureCount += 1;
-
             Parameter[] parameters = asBoolean(ctx.formalParameterList())
                     ? this.visitFormalParameterList(ctx.formalParameterList())
                     : null;
@@ -4120,13 +3889,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                 }
             }
 
-            ClosureExpression result = configureAST(new ClosureExpression(parameters, code), ctx);
-
-            visitingClosureCount -= 1;
-
-            return result;
+            return configureAST(new ClosureExpression(parameters, code), ctx);
         } finally {
             switchExpressionRuleContextStack.pop();
+            visitingClosureCount -= 1;
         }
     }
 
@@ -4389,7 +4155,6 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             if (!asBoolean(ctx.type())) {
                 GenericsType genericsType = new GenericsType(baseType);
                 genericsType.setWildcard(true);
-                genericsType.setName(QUESTION_STR);
 
                 return configureAST(genericsType, ctx);
             }
@@ -4641,7 +4406,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     private ClassNode createArrayType(final ClassNode elementType) {
-        if (isPrimitiveVoid(elementType)) {
+        if (ClassHelper.isPrimitiveVoid(elementType)) {
             throw this.createParsingFailedException("void[] is an invalid type", elementType);
         }
         return elementType.makeArray();
@@ -5191,5 +4956,4 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     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 ClassNode RECORD_TYPE_CLASS = ClassHelper.makeWithoutCaching(RECORD_TYPE_NAME);
 }
diff --git a/src/test/org/codehaus/groovy/classgen/RecordTest.groovy b/src/test/org/codehaus/groovy/classgen/RecordTest.groovy
index 7a72f184cf..31c0271918 100644
--- a/src/test/org/codehaus/groovy/classgen/RecordTest.groovy
+++ b/src/test/org/codehaus/groovy/classgen/RecordTest.groovy
@@ -38,122 +38,10 @@ import static org.junit.Assume.assumeTrue
 final class RecordTest {
 
     @Test
-    void testNativeRecordOnJDK16ByDefault() {
-        assumeTrue(isAtLeastJdk('16.0'))
-
-        assertScript '''
-            record Person(String name) {}
-            assert Person.class.superclass == java.lang.Record
-        '''
-    }
-
-    @Test
-    void testRecordLikeOnJDK16withTargetBytecode15() {
+    void testNativeRecordOnJDK16_groovy() {
         assumeTrue(isAtLeastJdk('16.0'))
 
-        def shell = new GroovyShell(new CompilerConfiguration(targetBytecode:'15'))
         assertScript shell, '''
-            record Person(String name) {}
-            assert Person.class.superclass != java.lang.Record
-        '''
-    }
-
-    @Test
-    void testAttemptedNativeRecordWithTargetBytecode15ShouldFail() {
-        assumeTrue(isAtLeastJdk('16.0'))
-
-        def shell = new GroovyShell(new CompilerConfiguration(targetBytecode:'15'))
-        def err = shouldFail shell, '''import groovy.transform.*
-            @RecordType(mode=RecordTypeMode.NATIVE)
-            class Person {
-                String name
-            }
-        '''
-        assert err.message.contains('Expecting JDK16+ but found 15 when attempting to create a native record')
-    }
-
-    @Test
-    void testNativeRecordWithSuperClassShouldFail() {
-        assumeTrue(isAtLeastJdk('16.0'))
-
-        def err = shouldFail '''import groovy.transform.*
-            @RecordType
-            class Person extends ArrayList {
-                String name
-            }
-        '''
-        assert err.message.contains('Invalid superclass for native record found: java.util.ArrayList')
-    }
-
-    @Test
-    void testNoNativeRecordOnJDK16WhenEmulating() {
-        assumeTrue(isAtLeastJdk('16.0'))
-
-        assertScript '''import groovy.transform.*
-            @RecordOptions(mode=RecordTypeMode.EMULATE)
-            record Person(String name) {
-            }
-            assert Person.class.superclass != java.lang.Record
-        '''
-    }
-
-    @Test
-    void testRecordsDefaultParams() {
-        assertScript '''
-            record Bar (String a = 'a', long b, Integer c = 24, short d, String e = 'e') {
-            }
-            short one = 1
-            assert new Bar(3L, one).toString() == 'Bar[a=a, b=3, c=24, d=1, e=e]'
-            assert new Bar('A', 3L, one).toString() == 'Bar[a=A, b=3, c=24, d=1, e=e]'
-            assert new Bar('A', 3L, 42, one).toString() == 'Bar[a=A, b=3, c=42, d=1, e=e]'
-            assert new Bar('A', 3L, 42, one, 'E').toString() == 'Bar[a=A, b=3, c=42, d=1, e=E]'
-        '''
-    }
-
-    @Test
-    void testInnerRecordIsImplicitlyStatic() {
-        assertScript '''
-            class Test {
-                record Point(int i, int j) {
-                }
-            }
-            assert java.lang.reflect.Modifier.isStatic(Test$Point.modifiers)
-        '''
-    }
-
-    @Test
-    void testRecordWithDefaultParams() {
-        assertScript '''
-            record Point(int i = 5, int j = 10) {
-            }
-            assert new Point().toString() == 'Point[i=5, j=10]'
-            assert new Point(50).toString() == 'Point[i=50, j=10]'
-            assert new Point(50, 100).toString() == 'Point[i=50, j=100]'
-            assert new Point([:]).toString() == 'Point[i=5, j=10]'
-            assert new Point(i: 50).toString() == 'Point[i=50, j=10]'
-            assert new Point(j: 100).toString() == 'Point[i=5, j=100]'
-            assert new Point(i: 50, j: 100).toString() == 'Point[i=50, j=100]'
-        '''
-    }
-
-    @Test
-    void testRecordWithDefaultParamsAndMissingRequiredParam() {
-        assertScript '''import static groovy.test.GroovyAssert.shouldFail
-            record Point(int i = 5, int j, int k = 10) {
-            }
-            assert new Point(j: 100).toString() == 'Point[i=5, j=100, k=10]'
-            def err = shouldFail {
-                new Point(i: 50)
-            }
-            assert err.message.contains("Missing required named argument 'j'")
-        '''
-    }
-
-    @Test
-    void testNativeRecordOnJDK16plus() {
-        assumeTrue(isAtLeastJdk('16.0'))
-
-        assertScript '''
             import java.lang.annotation.*
             import java.lang.reflect.RecordComponent
 
@@ -169,39 +57,40 @@ final class RecordTest {
             @Target([ElementType.TYPE_USE])
             @interface NotNull3 {}
 
-            record Person(@NotNull @NotNull2 String name, int age, @NotNull2 @NotNull3 List<String> locations, String[] titles) {}
+            record Person(@NotNull @NotNull2 String name, int age, @NotNull2 @NotNull3 List<String> locations, String[] titles) {
+            }
 
-            RecordComponent[] rcs = Person.class.getRecordComponents()
-            assert 4 == rcs.length
+            RecordComponent[] rcs = Person.getRecordComponents()
+            assert rcs.length == 4
 
-            assert 'name' == rcs[0].name && String.class == rcs[0].type
+            assert rcs[0].name == 'name' && rcs[0].type == String
             Annotation[] annotations = rcs[0].getAnnotations()
-            assert 2 == annotations.length
-            assert NotNull.class == annotations[0].annotationType()
-            assert NotNull2.class == annotations[1].annotationType()
+            assert annotations.length == 2
+            assert annotations[0].annotationType() == NotNull
+            assert annotations[1].annotationType() == NotNull2
             def typeAnnotations = rcs[0].getAnnotatedType().getAnnotations()
-            assert 1 == typeAnnotations.length
-            assert NotNull2.class == typeAnnotations[0].annotationType()
+            assert typeAnnotations.length == 1
+            assert typeAnnotations[0].annotationType() == NotNull2
 
-            assert 'age' == rcs[1].name && int.class == rcs[1].type
+            assert rcs[1].name == 'age' && rcs[1].type == int
 
-            assert 'locations' == rcs[2].name && List.class == rcs[2].type
-            assert 'Ljava/util/List<Ljava/lang/String;>;' == rcs[2].genericSignature
-            assert 'java.util.List<java.lang.String>' == rcs[2].genericType.toString()
+            assert rcs[2].name == 'locations' && rcs[2].type == List
+            assert rcs[2].genericSignature == 'Ljava/util/List<Ljava/lang/String;>;'
+            assert rcs[2].genericType.toString() == 'java.util.List<java.lang.String>'
             def annotations2 = rcs[2].getAnnotations()
-            assert 1 == annotations2.length
-            assert NotNull2.class == annotations2[0].annotationType()
+            assert annotations2.length == 1
+            assert annotations2[0].annotationType() == NotNull2
             def typeAnnotations2 = rcs[2].getAnnotatedType().getAnnotations()
-            assert 2 == typeAnnotations2.length
-            assert NotNull2.class == typeAnnotations2[0].annotationType()
-            assert NotNull3.class == typeAnnotations2[1].annotationType()
+            assert typeAnnotations2.length == 2
+            assert typeAnnotations2[0].annotationType() == NotNull2
+            assert typeAnnotations2[1].annotationType() == NotNull3
 
-            assert 'titles' == rcs[3].name && String[].class == rcs[3].type
+            assert rcs[3].name == 'titles' && rcs[3].type == String[]
         '''
     }
 
     @Test
-    void testNativeRecordOnJDK16plus_java() {
+    void testNativeRecordOnJDK16_java() {
         assumeTrue(isAtLeastJdk('16.0'))
 
         def sourceDir = File.createTempDir()
@@ -235,15 +124,15 @@ final class RecordTest {
             cu.addSources(a)
             cu.compile()
 
-            Class personClazz = loader.loadClass("Person")
-            Class notNullClazz = loader.loadClass("NotNull")
-            Class notNull2Clazz = loader.loadClass("NotNull2")
-            Class notNull3Clazz = loader.loadClass("NotNull3")
+            Class personClazz = loader.loadClass('Person')
+            Class notNullClazz = loader.loadClass('NotNull')
+            Class notNull2Clazz = loader.loadClass('NotNull2')
+            Class notNull3Clazz = loader.loadClass('NotNull3')
 
             def rcs = personClazz.recordComponents
             assert rcs.length == 4
 
-            assert rcs[0].name == 'name' && String.class == rcs[0].type
+            assert rcs[0].name == 'name' && rcs[0].type == String
             def annotations = rcs[0].annotations
             assert annotations.length == 2
             assert annotations[0].annotationType() == notNullClazz
@@ -252,9 +141,9 @@ final class RecordTest {
             assert typeAnnotations.length == 1
             assert notNull2Clazz == typeAnnotations[0].annotationType()
 
-            assert rcs[1].name == 'age'       && rcs[1].type == int.class
-            assert rcs[2].name == 'locations' && rcs[2].type == List.class
-            assert rcs[3].name == 'titles'    && rcs[3].type == String[].class
+            assert rcs[1].name == 'age'       && rcs[1].type == int
+            assert rcs[2].name == 'locations' && rcs[2].type == List
+            assert rcs[3].name == 'titles'    && rcs[3].type == String[]
 
             assert rcs[2].genericSignature == 'Ljava/util/List<Ljava/lang/String;>;'
             assert rcs[2].genericType as String == 'java.util.List<java.lang.String>'
@@ -287,55 +176,177 @@ final class RecordTest {
     private static void checkNativeRecordClassNode(ClassNode personClassNode, ClassNode notNullClassNode, ClassNode notNull2ClassNode, ClassNode notNull3ClassNode) {
         assert personClassNode.isRecord()
         def rcns = personClassNode.getRecordComponents()
-        assert 4 == rcns.size()
-        assert 'name' == rcns[0].name && ClassHelper.STRING_TYPE == rcns[0].type
+        assert rcns.size() == 4
+        assert rcns[0].name == 'name' && rcns[0].type == ClassHelper.STRING_TYPE
         List<AnnotationNode> annotationNodes = rcns[0].getAnnotations()
-        assert 2 == annotationNodes.size()
-        assert notNullClassNode == annotationNodes[0].getClassNode()
-        assert notNull2ClassNode == annotationNodes[1].getClassNode()
+        assert annotationNodes.size() == 2
+        assert annotationNodes[0].getClassNode() == notNullClassNode
+        assert annotationNodes[1].getClassNode() == notNull2ClassNode
         def typeAnnotationNodes = rcns[0].getType().getTypeAnnotations()
-        assert 1 == typeAnnotationNodes.size()
-        assert notNull2ClassNode == typeAnnotationNodes[0].getClassNode()
+        assert typeAnnotationNodes.size() == 1
+        assert typeAnnotationNodes[0].getClassNode() == notNull2ClassNode
 
-        assert 'age' == rcns[1].name && ClassHelper.int_TYPE == rcns[1].type
+        assert rcns[1].name == 'age' && rcns[1].type == ClassHelper.int_TYPE
 
-        assert 'locations' == rcns[2].name && ClassHelper.LIST_TYPE == rcns[2].type
+        assert rcns[2].name == 'locations' && rcns[2].type == ClassHelper.LIST_TYPE
         def genericsTypes = rcns[2].type.genericsTypes
-        assert 1 == genericsTypes.size()
-        assert ClassHelper.STRING_TYPE == genericsTypes[0].type
+        assert genericsTypes.size() == 1
+        assert genericsTypes[0].type == ClassHelper.STRING_TYPE
         def annotationNodes2 = rcns[2].getAnnotations()
-        assert 1 == annotationNodes2.size()
-        assert notNull2ClassNode == annotationNodes2[0].getClassNode()
+        assert annotationNodes2.size() == 1
+        assert annotationNodes2[0].getClassNode() == notNull2ClassNode
         def typeAnnotationNodes2 = rcns[2].getType().getTypeAnnotations()
-        assert 2 == typeAnnotationNodes2.size()
-        assert notNull2ClassNode == typeAnnotationNodes2[0].getClassNode()
-        assert notNull3ClassNode == typeAnnotationNodes2[1].getClassNode()
+        assert typeAnnotationNodes2.size() == 2
+        assert typeAnnotationNodes2[0].getClassNode() == notNull2ClassNode
+        assert typeAnnotationNodes2[1].getClassNode() == notNull3ClassNode
+
+        assert rcns[3].name == 'titles' && rcns[3].type == ClassHelper.STRING_TYPE.makeArray()
+    }
+
+    //--------------------------------------------------------------------------
+
+    private final GroovyShell shell = GroovyShell.withConfig {
+        imports {
+            star 'groovy.transform'
+            staticStar 'java.lang.reflect.Modifier'
+            staticMember 'groovy.test.GroovyAssert', 'shouldFail'
+            staticMember 'org.codehaus.groovy.ast.ClassHelper', 'make'
+        }
+    }
+
+    @Test
+    void testNativeRecordOnJDK16ByDefault() {
+        assumeTrue(isAtLeastJdk('16.0'))
 
-        assert 'titles' == rcns[3].name && ClassHelper.make(String[].class) == rcns[3].type
+        assertScript shell, '''
+            record Person(String name) {}
+            assert Person.superclass == java.lang.Record
+        '''
     }
 
     @Test
-    void testNativeRecordOnJDK16plus2_java() {
+    void testRecordLikeOnJDK16withTargetBytecode15() {
         assumeTrue(isAtLeastJdk('16.0'))
 
-        assertScript '''
-            import org.codehaus.groovy.ast.*
+        shell.targetBytecode = '15'
+        assertScript shell, '''
+            record Person(String name) {}
+            assert Person.superclass != java.lang.Record
+        '''
+    }
+
+    @Test
+    void testAttemptedNativeRecordWithTargetBytecode15ShouldFail() {
+        assumeTrue(isAtLeastJdk('16.0'))
 
-            def cn = ClassHelper.make(jdk.net.UnixDomainPrincipal.class)
+        shell.targetBytecode = '15'
+        def err = shouldFail shell, '''
+            @RecordType(mode=RecordTypeMode.NATIVE)
+            class Person {
+                String name
+            }
+        '''
+        assert err.message.contains('Expecting JDK16+ but found 15 when attempting to create a native record')
+    }
+
+    @Test
+    void testNativeRecordWithSuperClassShouldFail() {
+        assumeTrue(isAtLeastJdk('16.0'))
+
+        def err = shouldFail shell, '''
+            @RecordType
+            class Person extends ArrayList {
+                String name
+            }
+        '''
+        assert err.message.contains('Invalid superclass for native record found: java.util.ArrayList')
+    }
+
+    @Test
+    void testNonNativeRecordOnJDK16WhenEmulating() {
+        assumeTrue(isAtLeastJdk('16.0'))
+
+        assertScript shell, '''
+            @RecordOptions(mode=RecordTypeMode.EMULATE)
+            record Person(String name) {
+            }
+            assert Person.superclass != java.lang.Record
+        '''
+    }
+
+    @Test
+    void testRecordsDefaultParams() {
+        assertScript shell, '''
+            record Bar (String a = 'a', long b, Integer c = 24, short d, String e = 'e') {
+            }
+
+            short one = 1
+            assert new Bar(3L, one).toString() == 'Bar[a=a, b=3, c=24, d=1, e=e]'
+            assert new Bar('A', 3L, one).toString() == 'Bar[a=A, b=3, c=24, d=1, e=e]'
+            assert new Bar('A', 3L, 42, one).toString() == 'Bar[a=A, b=3, c=42, d=1, e=e]'
+            assert new Bar('A', 3L, 42, one, 'E').toString() == 'Bar[a=A, b=3, c=42, d=1, e=E]'
+        '''
+    }
+
+    @Test
+    void testInnerRecordIsImplicitlyStatic() {
+        assertScript shell, '''
+            class Test {
+                record Point(int i, int j) {
+                }
+            }
+            assert isStatic(Test$Point.modifiers)
+        '''
+    }
+
+    @Test
+    void testRecordWithDefaultParams() {
+        assertScript shell, '''
+            record Point(int i = 5, int j = 10) {
+            }
+            assert new Point().toString() == 'Point[i=5, j=10]'
+            assert new Point(50).toString() == 'Point[i=50, j=10]'
+            assert new Point(50, 100).toString() == 'Point[i=50, j=100]'
+            assert new Point([:]).toString() == 'Point[i=5, j=10]'
+            assert new Point(i: 50).toString() == 'Point[i=50, j=10]'
+            assert new Point(j: 100).toString() == 'Point[i=5, j=100]'
+            assert new Point(i: 50, j: 100).toString() == 'Point[i=50, j=100]'
+        '''
+    }
+
+    @Test
+    void testRecordWithDefaultParamsAndMissingRequiredParam() {
+        assertScript shell, '''
+            record Point(int i = 5, int j, int k = 10) {
+            }
+            assert new Point(j: 100).toString() == 'Point[i=5, j=100, k=10]'
+            def err = shouldFail {
+                new Point(i: 50)
+            }
+            assert err.message.contains("Missing required named argument 'j'")
+        '''
+    }
+
+    @Test
+    void testBinaryRecordClassNode() {
+        assumeTrue(isAtLeastJdk('16.0'))
+
+        assertScript shell, '''
+            def cn = make(jdk.net.UnixDomainPrincipal)
             assert cn.isRecord()
             def rcns = cn.getRecordComponents()
-            assert 2 == rcns.size()
-            assert 'user' == rcns[0].name && 'java.nio.file.attribute.UserPrincipal' == rcns[0].type.name
-            assert 'group' == rcns[1].name && 'java.nio.file.attribute.GroupPrincipal' == rcns[1].type.name
+            assert rcns.size() == 2
+            assert rcns[0].name == 'user' && rcns[0].type.name == 'java.nio.file.attribute.UserPrincipal'
+            assert rcns[1].name == 'group' && rcns[1].type.name == 'java.nio.file.attribute.GroupPrincipal'
         '''
     }
 
     @Test
-    void testNativeRecordOnJDK16plus2() {
+    void testManyRecordComponents() {
         assumeTrue(isAtLeastJdk('16.0'))
 
-        assertScript '''
-            @groovy.transform.CompileStatic
+        assertScript shell, '''
+            @CompileStatic
             record Record(String name, int x0, int x1, int x2, int x3, int x4,
                                     int x5, int x6, int x7, int x8, int x9, int x10, int x11, int x12, int x13, int x14,
                                     int x15, int x16, int x17, int x18, int x19, int x20) {
@@ -346,11 +357,10 @@ final class RecordTest {
 
             def r = new Record('someRecord', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
             def expected = 'Record[name=someRecord, x0=0, x1=-1, x2=2, x3=3, x4=4, x5=5, x6=6, x7=7, x8=8, x9=9, x10=10, x11=11, x12=12, x13=13, x14=14, x15=15, x16=16, x17=17, x18=18, x19=19, x20=20]'
-            assert expected == r.toString()
+            assert r.toString() == expected
         '''
 
-        assertScript '''
-            import groovy.transform.*
+        assertScript shell, '''
             @CompileStatic
             @ToString(includeNames=true)
             record Record(String name, int x0, int x1, int x2, int x3, int x4,
@@ -363,14 +373,13 @@ final class RecordTest {
 
             def r = new Record('someRecord', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
             def expected = 'Record(name:someRecord, x0:0, x1:-1, x2:2, x3:3, x4:4, x5:5, x6:6, x7:7, x8:8, x9:9, x10:10, x11:11, x12:12, x13:13, x14:14, x15:15, x16:16, x17:17, x18:18, x19:19, x20:20)'
-            assert expected == r.toString()
+            assert r.toString() == expected
         '''
     }
 
-
     @Test
     void testShallowImmutability() {
-        assertScript '''
+        assertScript shell, '''
             record HasItems(List items) { }
 
             def itemRec = new HasItems(['a', 'b'])
@@ -384,47 +393,50 @@ final class RecordTest {
 
     @Test
     void testCoerce() {
-        assertScript '''
-            @groovy.transform.CompileDynamic
-            record PersonDynamic(String name, int age) {}
-            record PersonStatic(String name, int age) {}
+        assertScript shell, '''
+            @CompileDynamic
+            record PersonDynamic(String name, int age) {
+            }
+            @CompileStatic
+            record PersonStatic(String name, int age) {
+            }
 
-            def testDynamic() {
+            void testDynamic() {
                 PersonDynamic p = ['Daniel', 37]
-                assert 'Daniel' == p.name()
-                assert 37 == p.age()
+                assert p.name() == 'Daniel'
+                assert p.age() == 37
 
                 PersonDynamic p2 = [age: 37, name: 'Daniel']
-                assert 'Daniel' == p2.name()
-                assert 37 == p2.age()
+                assert p2.name() == 'Daniel'
+                assert p2.age() == 37
 
                 PersonStatic p3 = ['Daniel', 37]
-                assert 'Daniel' == p3.name()
-                assert 37 == p3.age()
+                assert p3.name() == 'Daniel'
+                assert p3.age() == 37
 
                 PersonStatic p4 = [age: 37, name: 'Daniel']
-                assert 'Daniel' == p4.name()
-                assert 37 == p4.age()
+                assert p4.name() == 'Daniel'
+                assert p4.age() == 37
             }
             testDynamic()
 
-            @groovy.transform.CompileStatic
-            def testStatic() {
+            @CompileStatic
+            void testStatic() {
                 PersonStatic p = ['Daniel', 37]
-                assert 'Daniel' == p.name()
-                assert 37 == p.age()
+                assert p.name() == 'Daniel'
+                assert p.age() == 37
 
                 PersonStatic p2 = [age: 37, name: 'Daniel']
-                assert 'Daniel' == p2.name()
-                assert 37 == p2.age()
+                assert p2.name() == 'Daniel'
+                assert p2.age() == 37
 
                 PersonDynamic p3 = ['Daniel', 37]
-                assert 'Daniel' == p3.name()
-                assert 37 == p3.age()
+                assert p3.name() == 'Daniel'
+                assert p3.age() == 37
 
                 PersonDynamic p4 = [age: 37, name: 'Daniel']
-                assert 'Daniel' == p4.name()
-                assert 37 == p4.age()
+                assert p4.name() == 'Daniel'
+                assert p4.age() == 37
             }
             testStatic()
         '''
@@ -435,30 +447,30 @@ final class RecordTest {
         // inspired by:
         // https://inside.java/2020/07/20/record-serialization/
 
-        assertScript '''
-        @groovy.transform.ToString(includeNames=true, includeFields=true)
-        class RangeClass implements Serializable {
-            private final int lo
-            private final int hi
-            RangeClass(int lo, int hi) {
-                this.lo = lo
-                this.hi = hi
-                if (lo > hi) throw new IllegalArgumentException("$lo should not be greater than $hi")
-            }
-            // backdoor to emulate hacking of datastream
-            RangeClass(int[] pair) {
-                this.lo = pair[0]
-                this.hi = pair[1]
+        assertScript shell, '''
+            @ToString(includeNames=true, includeFields=true)
+            class RangeClass implements Serializable {
+                private final int lo
+                private final int hi
+                RangeClass(int lo, int hi) {
+                    this.lo = lo
+                    this.hi = hi
+                    if (lo > hi) throw new IllegalArgumentException("$lo should not be greater than $hi")
+                }
+                // backdoor to emulate hacking of datastream
+                RangeClass(int[] pair) {
+                    this.lo = pair[0]
+                    this.hi = pair[1]
+                }
             }
-        }
 
-        var data = File.createTempFile("serial", ".data")
-        var rc = [new RangeClass([5, 10] as int[]), new RangeClass([10, 5] as int[])]
-        data.withObjectOutputStream { out -> rc.each{ out << it } }
-        data.withObjectInputStream(getClass().classLoader) { in ->
-            assert in.readObject().toString() == 'RangeClass(lo:5, hi:10)'
-            assert in.readObject().toString() == 'RangeClass(lo:10, hi:5)'
-        }
+            var data = File.createTempFile("serial", ".data")
+            var rc = [new RangeClass([5, 10] as int[]), new RangeClass([10, 5] as int[])]
+            data.withObjectOutputStream { out -> rc.each{ out << it } }
+            data.withObjectInputStream(getClass().classLoader) { in ->
+                assert in.readObject().toString() == 'RangeClass(lo:5, hi:10)'
+                assert in.readObject().toString() == 'RangeClass(lo:10, hi:5)'
+            }
         '''
     }
 
@@ -466,9 +478,7 @@ final class RecordTest {
     void testNativeRecordSerialization() {
         assumeTrue(isAtLeastJdk('16.0'))
 
-        assertScript '''
-            import static groovy.test.GroovyAssert.shouldFail
-
+        assertScript shell, '''
             record RangeRecord(int lo, int hi) implements Serializable {
                 public RangeRecord {
                     if (lo > hi) throw new IllegalArgumentException("$lo should not be greater than $hi")
@@ -493,22 +503,61 @@ final class RecordTest {
 
     @Test
     void testCustomizedGetter() {
-        assertScript '''
+        assertScript shell, '''
             record Person(String name) {
                 String name() {
                     return "name: $name"
                 }
             }
 
-            assert 'name: Daniel' == new Person('Daniel').name()
+            assert new Person('Daniel').name() == 'name: Daniel'
         '''
     }
 
     @Test
-    void testGenerics() {
-        assertScript '''
-            import groovy.transform.CompileStatic
+    void testTupleConstructor() {
+        for (pair in [['RecordType', 'TupleConstructor'], ['defaults=false', 'defaultsMode=DefaultsMode.OFF']].combinations()) {
+            assertScript shell, """
+                @${pair[0]}(${pair[1]}, namedVariant=false)
+                record Person(String name, Date dob) {
+                    //Person(String,Date)
+                    //Person(String)  no!
+                    //Person(Map)     no!
+                    //Person()        no!
+
+                    public Person { // implies @TupleConstructor(pre={...})
+                        assert name.length() > 1
+                    }
+
+                    Person(Person that) {
+                        this(that.name(), that.dob())
+                    }
+
+                    //getAt(int i)
+                    //toList()
+                    //toMap()
+                }
+
+                assert Person.declaredConstructors.length == 2 // copy and tuple
 
+                def person = new Person('Frank Grimes', new Date())
+                def doppel = new Person(person)
+                shouldFail {
+                    new Person(name:'Frank Grimes', dob:null)
+                }
+                shouldFail {
+                    new Person('Frank Grimes')
+                }
+                shouldFail {
+                    new Person()
+                }
+            """
+        }
+    }
+
+    @Test
+    void testGenerics() {
+        assertScript shell, '''
             @CompileStatic
             record Person<T extends CharSequence>(T name, int age) {
                 Person {
@@ -518,38 +567,37 @@ final class RecordTest {
             }
 
             @CompileStatic
-            def test() {
+            void test() {
                 def p = new Person<String>('Daniel', 37)
-                assert 'daniel' == p.name().toLowerCase()
-                assert 'Person[name=Daniel, age=37]' == p.toString()
+                assert p.name().toLowerCase() == 'daniel'
+                assert p.toString() == 'Person[name=Daniel, age=37]'
 
                 def p2 = new Person<>('Daniel', 37)
-                assert 'daniel' == p2.name().toLowerCase()
-                assert 'Person[name=Daniel, age=37]' == p2.toString()
+                assert p2.name().toLowerCase() == 'daniel'
+                assert p2.toString() == 'Person[name=Daniel, age=37]'
 
-                try {
+                def err = shouldFail(IllegalArgumentException) {
                     new Person<String>('', 1)
-                } catch (IllegalArgumentException e) {
-                    assert 'name can not be empty' == e.message
                 }
+                assert err.message == 'name can not be empty'
 
-                try {
+                err = shouldFail(IllegalArgumentException) {
                     new Person<String>('Unknown', -1)
-                } catch (IllegalArgumentException e) {
-                    assert 'Invalid age: -1' == e.message
                 }
+                assert err.message == 'Invalid age: -1'
             }
 
             test()
         '''
     }
 
-    @Test // GROOVY-10548
+    // GROOVY-10548
+    @Test
     void testProperty() {
-        assertScript '''
+        assertScript shell, '''
             record Person(String name) {
             }
-            @groovy.transform.CompileStatic
+            @CompileStatic
             void test() {
                 def person = new Person('Frank Grimes')
                 assert person.name == 'Frank Grimes'
@@ -558,9 +606,10 @@ final class RecordTest {
         '''
     }
 
-    @Test // GROOVY-10679
+    // GROOVY-10679
+    @Test
     void testAnnotationPropogation() {
-        assertScript '''
+        assertScript shell, '''
             import java.lang.annotation.*
 
             @Target([ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER])