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 4b1ced24e37b701d6c6856988ff07bf435fdfc1f Author: Daniel Sun <sun...@apache.org> AuthorDate: Wed Sep 22 16:43:42 2021 +0800 Support `record` compact constructor --- src/antlr/GroovyParser.g4 | 5 ++ .../apache/groovy/parser/antlr4/AstBuilder.java | 81 ++++++++++++++++++++-- .../core/RecordDeclaration_08x.groovy | 55 +++++++++++++++ .../core/RecordDeclaration_09x.groovy | 56 +++++++++++++++ ...tion_02x.groovy => ClassDeclaration_01x.groovy} | 0 .../fail/ClassDeclaration_02x.groovy | 13 +++- ...ion_02x.groovy => RecordDeclaration_08x.groovy} | 10 ++- .../groovy/parser/antlr4/GroovyParserTest.groovy | 2 + .../groovy/parser/antlr4/SyntaxErrorTest.groovy | 8 ++- 9 files changed, 218 insertions(+), 12 deletions(-) diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4 index 49d76c0..061d26d 100644 --- a/src/antlr/GroovyParser.g4 +++ b/src/antlr/GroovyParser.g4 @@ -264,6 +264,7 @@ memberDeclaration[int t] : methodDeclaration[0, $t] | fieldDeclaration | modifiersOpt classDeclaration + | compactConstructorDeclaration ; /** @@ -283,6 +284,10 @@ methodDeclaration[int t, int ct] )? ; +compactConstructorDeclaration + : modifiers 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..fcb04fc 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,61 @@ 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"); + + if (!classNode.isRecord()) { + createParsingFailedException("Only `record` can have compact constructor", ctx); + } + + ModifierManager modifierManager = new ModifierManager(this, this.visitModifiers(ctx.modifiers())); + + 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 +2112,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 +2369,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 +5227,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/ClassDeclaration_02x.groovy b/src/test-resources/fail/ClassDeclaration_01x.groovy similarity index 100% copy from src/test-resources/fail/ClassDeclaration_02x.groovy copy to src/test-resources/fail/ClassDeclaration_01x.groovy diff --git a/src/test-resources/fail/ClassDeclaration_02x.groovy b/src/test-resources/fail/ClassDeclaration_02x.groovy index 662642d..5e006e3 100644 --- a/src/test-resources/fail/ClassDeclaration_02x.groovy +++ b/src/test-resources/fail/ClassDeclaration_02x.groovy @@ -16,7 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -class Foo {} -new Foo() { - Foo() {} +package fail + +class 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") + } } diff --git a/src/test-resources/fail/ClassDeclaration_02x.groovy b/src/test-resources/fail/RecordDeclaration_08x.groovy similarity index 76% copy from src/test-resources/fail/ClassDeclaration_02x.groovy copy to src/test-resources/fail/RecordDeclaration_08x.groovy index 662642d..57a1f98 100644 --- a/src/test-resources/fail/ClassDeclaration_02x.groovy +++ b/src/test-resources/fail/RecordDeclaration_08x.groovy @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -class Foo {} -new Foo() { - Foo() {} +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..3a660ee 100644 --- a/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy +++ b/src/test/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy @@ -192,10 +192,15 @@ final class SyntaxErrorTest extends GroovyTestCase { } void 'test groovy core - ClassDeclaration 1'() { - TestUtils.doRunAndShouldFail('fail/ClassDeclaration_02x.groovy') + TestUtils.doRunAndShouldFail('fail/ClassDeclaration_01x.groovy') } void 'test groovy core - ClassDeclaration 2'() { + TestUtils.doRunAndShouldFail('fail/ClassDeclaration_02x.groovy') + } + + + void 'test groovy core - ClassDeclaration 3'() { def err = expectParseError '''\ |class C extends Object, Number {} |'''.stripMargin() @@ -435,6 +440,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'() {