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);