You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2021/09/23 03:37:01 UTC

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

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

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

commit 3b2cb508f7c342d961ded3d3fa4e176635950f89
Author: Daniel Sun <su...@apache.org>
AuthorDate: Tue Sep 21 20:48:51 2021 +0800

    GROOVY-10240: Support record grammar
---
 src/antlr/GroovyLexer.g4                           |  2 +
 src/antlr/GroovyParser.g4                          |  6 +-
 .../apache/groovy/ast/tools/MethodNodeUtils.java   | 15 +++++
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 71 +++++++++++++++++++---
 .../java/org/codehaus/groovy/ast/ClassNode.java    | 11 ++++
 .../org/codehaus/groovy/classgen/Verifier.java     | 23 +++++--
 .../groovy/classgen/asm/WriterController.java      |  7 +++
 .../transform/stc/StaticTypeCheckingVisitor.java   | 16 ++++-
 .../core/RecordDeclaration_01x.groovy              | 25 ++++++++
 .../core/RecordDeclaration_02x.groovy              | 33 ++++++++++
 .../core/RecordDeclaration_03x.groovy              | 31 ++++++++++
 .../core/RecordDeclaration_04x.groovy              | 35 +++++++++++
 .../core/RecordDeclaration_05x.groovy              | 32 ++++++++++
 .../core/RecordDeclaration_06x.groovy              | 38 ++++++++++++
 .../core/RecordDeclaration_07x.groovy              | 30 +++++++++
 .../fail/RecordDeclaration_01x.groovy              | 21 +++++++
 .../fail/RecordDeclaration_02x.groovy              | 21 +++++++
 .../fail/RecordDeclaration_03x.groovy              | 21 +++++++
 .../fail/RecordDeclaration_04x.groovy              | 21 +++++++
 .../fail/RecordDeclaration_05x.groovy              | 21 +++++++
 .../fail/RecordDeclaration_06x.groovy              | 23 +++++++
 .../fail/RecordDeclaration_07x.groovy              | 23 +++++++
 .../org/codehaus/groovy/classgen/RecordTest.groovy | 39 ++++++++++++
 .../groovy/parser/antlr4/GroovyParserTest.groovy   | 10 +++
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    | 10 +++
 .../console/ui/text/SmartDocumentFilter.java       |  3 +-
 26 files changed, 572 insertions(+), 16 deletions(-)

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