You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2021/09/17 06:00:27 UTC

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

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

sunlan pushed a commit to branch GROOVY-10240
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit e8bbd8c12fd0718424c6f278cc03ad6871aaa9aa
Author: Daniel Sun <su...@apache.org>
AuthorDate: Fri Sep 17 14:00:10 2021 +0800

    GROOVY-10240: Support record grammar
---
 src/antlr/GroovyLexer.g4                           |  2 +
 src/antlr/GroovyParser.g4                          |  6 ++-
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 59 +++++++++++++++++++---
 .../core/RecordDeclaration_01x.groovy              | 25 +++++++++
 .../core/RecordDeclaration_02x.groovy              | 33 ++++++++++++
 .../fail/RecordDeclaration_01x.groovy              | 21 ++++++++
 .../fail/RecordDeclaration_02x.groovy              | 21 ++++++++
 .../groovy/parser/antlr4/GroovyParserTest.groovy   |  5 ++
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  5 ++
 .../console/ui/text/SmartDocumentFilter.java       |  3 +-
 10 files changed, 171 insertions(+), 9 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/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 052dcfd..864bb4a 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);
@@ -1453,6 +1455,17 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             }
         }
 
+        boolean isRecord = asBoolean(ctx.RECORD());
+        if (isRecord) {
+            if (asBoolean(ctx.EXTENDS())) {
+                throw createParsingFailedException("No extends clause allowed for record declaration", ctx.EXTENDS());
+            }
+        } else {
+            if (asBoolean(ctx.formalParameters())) {
+                throw createParsingFailedException("Parameter declaration is only allowed for record declaration", ctx.formalParameters());
+            }
+        }
+
         List<ModifierNode> modifierNodeList = ctx.getNodeMetaData(TYPE_DECLARATION_MODIFIERS);
         Objects.requireNonNull(modifierNodeList, "modifierNodeList should not be null");
         ModifierManager modifierManager = new ModifierManager(this, modifierNodeList);
@@ -1533,6 +1546,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) {
@@ -1554,24 +1570,27 @@ 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.setSuperClassForRecordType(classNode);
+            this.transformRecordParametersToProperties(ctx, classNode);
+            this.initUsingGenerics(classNode);
         } else {
             throw createParsingFailedException("Unsupported class declaration: " + ctx.getText(), ctx);
         }
@@ -1598,6 +1617,24 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return classNode;
     }
 
+    private void setSuperClassForRecordType(ClassNode classNode) {
+        Integer bytecodeVersion = CompilerConfiguration.JDK_TO_BYTECODE_VERSION_MAP.get(sourceUnit.getConfiguration().getTargetBytecode());
+        if (null != bytecodeVersion && bytecodeVersion >= Opcodes.V16) {
+            // `java.lang.Record` is added since JDK16
+            classNode.setSuperClass(ClassHelper.makeWithoutCaching(RECORD_CLASS_NAME));
+        }
+    }
+
+    private void transformRecordParametersToProperties(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);
+            declareProperty(parameterCtx, parameterModifierManager, parameter.getType(), classNode, i, parameter, parameter.getName(), parameter.getModifiers(), parameter.getInitialExpression());
+        }
+    }
+
     @SuppressWarnings("unchecked")
     private boolean containsDefaultMethods(final ClassDeclarationContext ctx) {
         List<MethodDeclarationContext> methodDeclarationContextList =
@@ -2220,7 +2257,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 void declareProperty(final GroovyParser.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);
 
@@ -2255,7 +2292,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             if (i == 0) {
                 configureAST(fieldNode, ctx, initialValue);
             } else {
-                configureAST(fieldNode, variableExpression, initialValue);
+                configureAST(fieldNode, startNode, initialValue);
             }
         }
 
@@ -2265,7 +2302,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         if (i == 0) {
             configureAST(propertyNode, ctx, initialValue);
         } else {
-            configureAST(propertyNode, variableExpression, initialValue);
+            configureAST(propertyNode, startNode, initialValue);
         }
     }
 
@@ -4655,8 +4692,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(
@@ -4666,6 +4704,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));
@@ -5098,4 +5139,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 RECORD_TYPE_NAME = "groovy.transform.RecordType";
+    private static final String RECORD_CLASS_NAME = "java.lang.Record";
 }
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..f2542b2
--- /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"
+    }
+}
+
+def apple = new Fruit('Apple', 11.6)
+assert 'Apple eaten' == apple.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/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index b2b3e90..4873d7d 100644
--- a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -357,6 +357,11 @@ 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')
+    }
+
     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 90eab45..2b86736 100644
--- a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -427,6 +427,11 @@ 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')
+    }
+
     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);