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/22 08:50:57 UTC

[groovy] 01/01: Support `record` compact constructor

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 3e6c484eacb06b9aa64f2d2064113f6696419b06
Author: Daniel Sun <su...@apache.org>
AuthorDate: Wed Sep 22 16:43:42 2021 +0800

    Support `record` compact constructor
---
 src/antlr/GroovyParser.g4                          |  6 ++
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 79 ++++++++++++++++++++--
 .../core/RecordDeclaration_08x.groovy              | 55 +++++++++++++++
 .../core/RecordDeclaration_09x.groovy              | 56 +++++++++++++++
 .../fail/RecordDeclaration_08x.groovy              | 26 +++++++
 .../groovy/parser/antlr4/GroovyParserTest.groovy   |  2 +
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  1 +
 7 files changed, 220 insertions(+), 5 deletions(-)

diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 49d76c0..1f44e66 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -263,6 +263,8 @@ classBodyDeclaration[int t]
 memberDeclaration[int t]
     :   methodDeclaration[0, $t]
     |   fieldDeclaration
+    |   { 5 == $t }? // Only `record` has compact constructor
+        compactConstructorDeclaration
     |   modifiersOpt classDeclaration
     ;
 
@@ -283,6 +285,10 @@ methodDeclaration[int t, int ct]
         )?
     ;
 
+compactConstructorDeclaration
+    :   modifiersOpt methodName (nls methodBody)?
+    ;
+
 methodName
     :   identifier
     |   stringLiteral
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index 67810ba..385c6a9 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -21,9 +21,11 @@ package org.apache.groovy.parser.antlr4;
 import groovy.lang.Tuple2;
 import groovy.lang.Tuple3;
 import groovy.transform.CompileStatic;
+import groovy.transform.MapConstructor;
 import groovy.transform.NonSealed;
 import groovy.transform.Sealed;
 import groovy.transform.Trait;
+import groovy.transform.TupleConstructor;
 import org.antlr.v4.runtime.ANTLRErrorListener;
 import org.antlr.v4.runtime.CharStream;
 import org.antlr.v4.runtime.CharStreams;
@@ -38,6 +40,7 @@ import org.antlr.v4.runtime.misc.Interval;
 import org.antlr.v4.runtime.misc.ParseCancellationException;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.TerminalNode;
+import org.apache.groovy.internal.util.Function;
 import org.apache.groovy.parser.antlr4.GroovyParser.AdditiveExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.AndExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.AnnotatedQualifiedClassNameContext;
@@ -74,6 +77,7 @@ import org.apache.groovy.parser.antlr4.GroovyParser.ClosureOrLambdaExpressionCon
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandArgumentContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CommandExpressionContext;
+import org.apache.groovy.parser.antlr4.GroovyParser.CompactConstructorDeclarationContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.CompilationUnitContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.ConditionalExprAltContext;
 import org.apache.groovy.parser.antlr4.GroovyParser.ConditionalStatementContext;
@@ -292,7 +296,6 @@ import org.codehaus.groovy.ast.tools.ClosureUtils;
 import org.codehaus.groovy.classgen.Verifier;
 import org.codehaus.groovy.control.CompilationFailedException;
 import org.codehaus.groovy.control.CompilePhase;
-import org.codehaus.groovy.control.CompilerConfiguration;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
@@ -356,9 +359,14 @@ import static org.apache.groovy.parser.antlr4.GroovyParser.SUB;
 import static org.apache.groovy.parser.antlr4.GroovyParser.VAR;
 import static org.apache.groovy.parser.antlr4.util.PositionConfigureUtils.configureAST;
 import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.assignX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.cloneParams;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
@@ -368,6 +376,8 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 import static org.codehaus.groovy.classgen.asm.util.TypeUtil.isPrimitiveType;
 import static org.codehaus.groovy.runtime.DefaultGroovyMethods.asBoolean;
 import static org.codehaus.groovy.runtime.DefaultGroovyMethods.last;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
 
 /**
  * Builds the AST from the parse tree generated by Antlr4.
@@ -1512,8 +1522,8 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
         int modifiers = modifierManager.getClassModifiersOpValue();
 
-        boolean syntheticPublic = ((modifiers & Opcodes.ACC_SYNTHETIC) != 0);
-        modifiers &= ~Opcodes.ACC_SYNTHETIC;
+        boolean syntheticPublic = ((modifiers & ACC_SYNTHETIC) != 0);
+        modifiers &= ~ACC_SYNTHETIC;
 
         ClassNode classNode, outerClass = classNodeStack.peek();
 
@@ -1641,6 +1651,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
 
     private void transformRecordHeaderToProperties(ClassDeclarationContext ctx, ClassNode classNode) {
         Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
+        classNode.putNodeMetaData(RECORD_HEADER, parameters);
         for (int i = 0; i < parameters.length; i++) {
             Parameter parameter = parameters[i];
             ModifierManager parameterModifierManager = parameter.getNodeMetaData(PARAMETER_MODIFIER_MANAGER);
@@ -1835,6 +1846,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         } else if (asBoolean(ctx.fieldDeclaration())) {
             ctx.fieldDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
             this.visitFieldDeclaration(ctx.fieldDeclaration());
+        } else if (asBoolean(ctx.compactConstructorDeclaration())) {
+            ctx.compactConstructorDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
+            this.visitCompactConstructorDeclaration(ctx.compactConstructorDeclaration());
         } else if (asBoolean(ctx.classDeclaration())) {
             ctx.classDeclaration().putNodeMetaData(TYPE_DECLARATION_MODIFIERS, this.visitModifiersOpt(ctx.modifiersOpt()));
             ctx.classDeclaration().putNodeMetaData(CLASS_DECLARATION_CLASS_NODE, classNode);
@@ -1929,6 +1943,59 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
+    public MethodNode visitCompactConstructorDeclaration(CompactConstructorDeclarationContext ctx) {
+        ClassNode classNode = ctx.getNodeMetaData(CLASS_DECLARATION_CLASS_NODE);
+        Objects.requireNonNull(classNode, "classNode should not be null");
+
+        ModifierManager modifierManager =
+                new ModifierManager(this,
+                        asBoolean(ctx.modifiersOpt()) ? this.visitModifiersOpt(ctx.modifiersOpt()) : Collections.emptyList());
+
+        if (modifierManager.containsAny(VAR)) {
+            throw createParsingFailedException("var cannot be used for compact constructor declaration", ctx);
+        }
+
+        String methodName = this.visitMethodName(ctx.methodName());
+        String className = classNode.getNodeMetaData(CLASS_NAME);
+        if (!methodName.equals(className)) {
+            createParsingFailedException("Compact constructor should have the same name with record: " + className, ctx.methodName());
+        }
+        ClassNode returnType = ClassHelper.VOID_TYPE;
+
+        Parameter[] header = classNode.getNodeMetaData(RECORD_HEADER);
+        Objects.requireNonNull(classNode, "record header should not be null");
+        Parameter[] parameters = cloneParams(header);
+        Statement code = this.visitMethodBody(ctx.methodBody());
+        MethodNode methodNode = classNode.addSyntheticMethod(RECORD_COMPACT_CONSTRUCTOR_NAME, ACC_PRIVATE, returnType, parameters, ClassNode.EMPTY_ARRAY, code);
+
+        modifierManager.attachAnnotations(methodNode);
+        attachMapConstructorAnnotationToRecord(classNode, parameters);
+        attachTupleConstructorAnnotationToRecord(classNode, parameters);
+
+        return methodNode;
+    }
+
+    private void attachMapConstructorAnnotationToRecord(ClassNode classNode, Parameter[] parameters) {
+        doAttachConstructorAnnotationToRecord(classNode, MapConstructor.class,
+                parameters, p -> castX(p.getOriginType(), callX(varX("args"), "get", args(constX(p.getName())))));
+    }
+
+    private void attachTupleConstructorAnnotationToRecord(ClassNode classNode, Parameter[] parameters) {
+        doAttachConstructorAnnotationToRecord(classNode, TupleConstructor.class,
+                parameters, p -> castX(p.getOriginType(), varX(p.getName())));
+    }
+
+    private void doAttachConstructorAnnotationToRecord(ClassNode classNode, Class<?> annotationClass, Parameter[] parameters, Function<? super Parameter, ? extends Expression> mapper) {
+        AnnotationNode tupleConstructorAnnotationNode = new AnnotationNode(ClassHelper.makeCached(annotationClass));
+        List<Expression> argExpressionList =
+                Arrays.stream(parameters)
+                        .map(mapper::apply)
+                        .collect(Collectors.toList());
+        tupleConstructorAnnotationNode.setMember("pre", closureX(block(stmt(callX(varX("this"), RECORD_COMPACT_CONSTRUCTOR_NAME, args(argExpressionList))))));
+        classNode.addAnnotation(tupleConstructorAnnotationNode);
+    }
+
+    @Override
     public MethodNode visitMethodDeclaration(final MethodDeclarationContext ctx) {
         ModifierManager modifierManager = createModifierManager(ctx);
 
@@ -2043,7 +2110,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     private MethodNode createScriptMethodNode(final ModifierManager modifierManager, final String methodName, final ClassNode returnType, final Parameter[] parameters, final ClassNode[] exceptions, final Statement code) {
         MethodNode methodNode = new MethodNode(
                 methodName,
-                modifierManager.containsAny(PRIVATE) ? Opcodes.ACC_PRIVATE : Opcodes.ACC_PUBLIC,
+                modifierManager.containsAny(PRIVATE) ? ACC_PRIVATE : Opcodes.ACC_PUBLIC,
                 returnType,
                 parameters,
                 exceptions,
@@ -2300,7 +2367,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             classNode.addProperty(propertyNode);
 
             fieldNode = propertyNode.getField();
-            fieldNode.setModifiers(modifiers & ~Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE);
+            fieldNode.setModifiers(modifiers & ~Opcodes.ACC_PUBLIC | ACC_PRIVATE);
             fieldNode.setSynthetic(!classNode.isInterface());
             modifierManager.attachAnnotations(fieldNode);
             modifierManager.attachAnnotations(propertyNode);
@@ -5158,5 +5225,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     private static final String PARAMETER_MODIFIER_MANAGER = "_PARAMETER_MODIFIER_MANAGER";
     private static final String PARAMETER_CONTEXT = "_PARAMETER_CONTEXT";
     private static final String IS_RECORD_GENERATED = "_IS_RECORD_GENERATED";
+    private static final String RECORD_HEADER = "_RECORD_HEADER";
     private static final String RECORD_TYPE_NAME = "groovy.transform.RecordType";
+    private static final String RECORD_COMPACT_CONSTRUCTOR_NAME = "$compactInit";
 }
diff --git a/src/test-resources/core/RecordDeclaration_08x.groovy b/src/test-resources/core/RecordDeclaration_08x.groovy
new file mode 100644
index 0000000..d1ced5d
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_08x.groovy
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package core
+
+record Person(String name, int age) {
+    public Person {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
+}
+
+assert 'core.Person(name:Daniel, age:37)' == new Person('Daniel', 37).toString()
+try {
+    new Person('Peter', 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person('Devil', 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
+
+try {
+    new Person(name: 'Peter', age: 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person(name: 'Devil', age: 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
diff --git a/src/test-resources/core/RecordDeclaration_09x.groovy b/src/test-resources/core/RecordDeclaration_09x.groovy
new file mode 100644
index 0000000..53027b1
--- /dev/null
+++ b/src/test-resources/core/RecordDeclaration_09x.groovy
@@ -0,0 +1,56 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package core
+
+@groovy.transform.CompileStatic
+record Person(String name, int age) {
+    public Person {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
+}
+
+assert 'core.Person(name:Daniel, age:37)' == new Person('Daniel', 37).toString()
+try {
+    new Person('Peter', 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person('Devil', 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
+
+try {
+    new Person(name: 'Peter', age: 3)
+    assert false, 'should failed because of invalid age'
+} catch (e) {
+    assert 'Invalid age: 3' == e.message
+}
+
+try {
+    new Person(name: 'Devil', age: 100)
+    assert false, 'should failed because of invalid name'
+} catch (e) {
+    assert 'Invalid person: Devil' == e.message
+}
diff --git a/src/test-resources/fail/RecordDeclaration_08x.groovy b/src/test-resources/fail/RecordDeclaration_08x.groovy
new file mode 100644
index 0000000..57a1f98
--- /dev/null
+++ b/src/test-resources/fail/RecordDeclaration_08x.groovy
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package core
+
+record Person(String name, int age) {
+    public Person123 {
+        if (name == 'Devil') throw new IllegalArgumentException("Invalid person: $name")
+        if (age < 18) throw new IllegalArgumentException("Invalid age: $age")
+    }
+}
diff --git a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
index 5b4881a..e1dabc4 100644
--- a/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/GroovyParserTest.groovy
@@ -365,6 +365,8 @@ final class GroovyParserTest extends GroovyTestCase {
         doRunAndTestAntlr4('core/RecordDeclaration_05x.groovy')
         doRunAndTestAntlr4('core/RecordDeclaration_06x.groovy')
         doRunAndTestAntlr4('core/RecordDeclaration_07x.groovy')
+        doRunAndTestAntlr4('core/RecordDeclaration_08x.groovy')
+        doRunAndTestAntlr4('core/RecordDeclaration_09x.groovy')
     }
 
     void "test groovy core - AnnotationDeclaration"() {
diff --git a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index a28ed02..002ea01 100644
--- a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -435,6 +435,7 @@ final class SyntaxErrorTest extends GroovyTestCase {
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_05x.groovy')
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_06x.groovy')
         TestUtils.doRunAndShouldFail('fail/RecordDeclaration_07x.groovy')
+        TestUtils.doRunAndShouldFail('fail/RecordDeclaration_08x.groovy')
     }
 
     void 'test groovy core - Array'() {