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'() {