You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2021/11/03 06:17:06 UTC
[groovy] branch master updated: GROOVY-10338: Enhance records with
additional helper methods (additional refactoring)
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 9bb41a4 GROOVY-10338: Enhance records with additional helper methods (additional refactoring)
9bb41a4 is described below
commit 9bb41a408e275536fe2e90bafb3cc92ae580649a
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Nov 3 16:16:59 2021 +1000
GROOVY-10338: Enhance records with additional helper methods (additional refactoring)
---
src/main/groovy/groovy/transform/RecordType.groovy | 1 +
src/main/java/groovy/transform/RecordBase.java | 121 +--------------------
.../{RecordBase.java => RecordOptions.java} | 23 +++-
.../transform/RecordTypeASTTransformation.java | 44 +++++---
src/spec/test/RecordSpecificationTest.groovy | 10 +-
.../org/codehaus/groovy/classgen/RecordTest.groovy | 4 +-
6 files changed, 53 insertions(+), 150 deletions(-)
diff --git a/src/main/groovy/groovy/transform/RecordType.groovy b/src/main/groovy/groovy/transform/RecordType.groovy
index 3941a7b..3e50833 100644
--- a/src/main/groovy/groovy/transform/RecordType.groovy
+++ b/src/main/groovy/groovy/transform/RecordType.groovy
@@ -76,6 +76,7 @@ import java.lang.annotation.Target
* @since 4.0.0
*/
@RecordBase
+@RecordOptions
@TupleConstructor(namedVariant = true, force = true)
@PropertyOptions
@KnownImmutable
diff --git a/src/main/java/groovy/transform/RecordBase.java b/src/main/java/groovy/transform/RecordBase.java
index 39ff6e7..85c7c05 100644
--- a/src/main/java/groovy/transform/RecordBase.java
+++ b/src/main/java/groovy/transform/RecordBase.java
@@ -32,6 +32,7 @@ import java.lang.annotation.Target;
* @see MapConstructor
* @see TupleConstructor
* @see PropertyOptions
+ * @see RecordOptions
* @since 4.0.0
*/
@java.lang.annotation.Documented
@@ -39,124 +40,4 @@ import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@GroovyASTTransformationClass("org.codehaus.groovy.transform.RecordTypeASTTransformation")
public @interface RecordBase {
- /**
- * Mode to use when creating record type classes.
- */
- RecordTypeMode mode() default RecordTypeMode.AUTO;
-
- /**
- * If {@code true}, this adds a method {@code copyWith} which takes a Map of
- * new property values and returns a new instance of the record class with
- * these values set.
- * Example:
- * <!-- TODO pre class="groovyTestCase"-->
- * <pre>
- * {@code @groovy.transform.RecordType}(copyWith = true)
- * class Person {
- * String first, last
- * }
- *
- * def tim = new Person('tim', 'yates')
- * def alice = tim.copyWith(first:'alice')
- *
- * assert tim.toString() == 'Person[first=tim, last=yates]'
- * assert alice.toString() == 'Person[first=alice, last=yates]'
- * </pre>
- * Unknown keys in the map are ignored, and if the values would not change
- * the object, then the original object is returned.
- *
- * If a method called {@code copyWith} that takes a single parameter already
- * exists in the class, then this setting is ignored, and no method is generated.
- */
- boolean copyWith() default false;
-
- /**
- * If {@code true}, this adds a method {@code getAt(int)} which given
- * an integer n, returns the n'th component in the record.
- * Example:
- * <pre class="groovyTestCase">
- * import static groovy.test.GroovyAssert.shouldFail
- *
- * record Point(int x, int y, String color) {}
- *
- * def p = new Point(100, 200, 'green')
- * assert p[0] == 100
- * assert p[1] == 200
- * assert p[2] == 'green'
- * shouldFail(IllegalArgumentException) {
- * p[-1]
- * }
- *
- * // getAt also enables destructuring
- * def (x, y, c) = p
- * assert x == 100
- * assert y == 200
- * assert c == 'green'
- * </pre>
- *
- * If a method {@code getAt(int)} already exists in the class,
- * then this setting is ignored, and no additional method is generated.
- */
- boolean getAt() default true;
-
- /**
- * If {@code true}, this adds a method {@code toList()} to the record
- * which returns the record's components as a list.
- *
- * Example:
- * <pre class="groovyTestCase">
- * record Point(int x, int y, String color) {}
- * def p = new Point(100, 200, 'green')
- * assert p.toList() == [100, 200, 'green']
- * </pre>
- *
- * If a method {@code toList()} already exists in the class,
- * then this setting is ignored, and no additional method is generated.
- */
- boolean toList() default true;
-
- /**
- * If {@code true}, this adds a method {@code toMap()} to the record.
- *
- * Example:
- * <pre class="groovyTestCase">
- * record Point(int x, int y, String color) {}
- * def p = new Point(100, 200, 'green')
- * assert p.toMap() == [x:100, y:200, color:'green']
- * </pre>
- *
- * If a method {@code toMap()} already exists in the class,
- * then this setting is ignored, and no additional method is generated.
- */
- boolean toMap() default true;
-
- /**
- * If {@code true}, this adds a method {@code components()} to the record
- * which returns its components as a typed tuple {@code Tuple0}, {@code Tuple1}...
- *
- * Example:
- * <pre class="groovyTestCase">
- * import groovy.transform.*
- *
- * {@code @RecordBase(components=true)}
- * record Point(int x, int y, String color) {}
- *
- * def (x, y, c) = new Point(100, 200, 'green').components()
- * assert x == 100
- * assert y.intdiv(2) == 100
- * assert c.toUpperCase() == 'GREEN'
- * </pre>
- *
- * The signature of the components method for this example is:
- * <pre>
- * Tuple3<Integer, Integer, String> components()
- * </pre>
- * This is suitable for destructuring in {@code TypeChecked} scenarios.
- *
- * If a method {@code components()} already exists in the class,
- * then this setting is ignored, and no additional method is generated.
- * It is a compile-time error if there are more components in the record
- * than the largest TupleN class in the Groovy codebase.
- */
- boolean components() default false;
}
diff --git a/src/main/java/groovy/transform/RecordBase.java b/src/main/java/groovy/transform/RecordOptions.java
similarity index 89%
copy from src/main/java/groovy/transform/RecordBase.java
copy to src/main/java/groovy/transform/RecordOptions.java
index 39ff6e7..42c106d 100644
--- a/src/main/java/groovy/transform/RecordBase.java
+++ b/src/main/java/groovy/transform/RecordOptions.java
@@ -18,8 +18,6 @@
*/
package groovy.transform;
-import org.codehaus.groovy.transform.GroovyASTTransformationClass;
-
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -32,13 +30,13 @@ import java.lang.annotation.Target;
* @see MapConstructor
* @see TupleConstructor
* @see PropertyOptions
+ * @see RecordBase
* @since 4.0.0
*/
@java.lang.annotation.Documented
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
-@GroovyASTTransformationClass("org.codehaus.groovy.transform.RecordTypeASTTransformation")
-public @interface RecordBase {
+public @interface RecordOptions {
/**
* Mode to use when creating record type classes.
*/
@@ -131,6 +129,21 @@ public @interface RecordBase {
boolean toMap() default true;
/**
+ * If {@code true}, this adds a method {@code size()} to the record which returns the number of components.
+ *
+ * Example:
+ * <pre class="groovyTestCase">
+ * record Point(int x, int y, String color) {}
+ * def p = new Point(100, 200, 'green')
+ * assert p.size() == 3
+ * </pre>
+ *
+ * If a method {@code size()} already exists in the class,
+ * then this setting is ignored, and no additional method is generated.
+ */
+ boolean size() default true;
+
+ /**
* If {@code true}, this adds a method {@code components()} to the record
* which returns its components as a typed tuple {@code Tuple0}, {@code Tuple1}...
*
@@ -138,7 +151,7 @@ public @interface RecordBase {
* <pre class="groovyTestCase">
* import groovy.transform.*
*
- * {@code @RecordBase(components=true)}
+ * {@code @RecordOptions(components=true)}
* record Point(int x, int y, String color) {}
*
* def (x, y, c) = new Point(100, 200, 'green').components()
diff --git a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
index affc586..364be76 100644
--- a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -22,6 +22,7 @@ import groovy.lang.GroovyClassLoader;
import groovy.transform.CompilationUnitAware;
import groovy.transform.NamedParam;
import groovy.transform.RecordBase;
+import groovy.transform.RecordOptions;
import groovy.transform.RecordTypeMode;
import groovy.transform.options.PropertyHandler;
import org.apache.groovy.ast.tools.MethodNodeUtils;
@@ -65,6 +66,7 @@ import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE;
import static org.codehaus.groovy.ast.ClassHelper.getWrapper;
import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
@@ -117,6 +119,8 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
private static final ClassNode NAMED_PARAM_TYPE = makeWithoutCaching(NamedParam.class, false);
private static final int PUBLIC_FINAL = ACC_PUBLIC | ACC_FINAL;
private static final String RECORD_CLASS_NAME = "java.lang.Record";
+ private static final ClassNode RECORD_OPTIONS_TYPE = make(RecordOptions.class);
+ private static final String SIZE = "size";
private static final String TO_LIST = "toList";
private static final String TO_MAP = "toMap";
@@ -141,12 +145,14 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
final PropertyHandler handler = PropertyHandler.createPropertyHandler(this, classLoader, (ClassNode) parent);
if (handler == null) return;
if (!handler.validateAttributes(this, anno)) return;
- doProcessRecordType((ClassNode) parent, anno, handler);
+ doProcessRecordType((ClassNode) parent, handler);
}
}
- private void doProcessRecordType(ClassNode cNode, AnnotationNode node, PropertyHandler handler) {
- RecordTypeMode mode = getMode(node, "mode");
+ private void doProcessRecordType(ClassNode cNode, PropertyHandler handler) {
+ List<AnnotationNode> annotations = cNode.getAnnotations(RECORD_OPTIONS_TYPE);
+ AnnotationNode options = annotations.isEmpty() ? null : annotations.get(0);
+ RecordTypeMode mode = getMode(options, "mode");
boolean isPostJDK16 = false;
String message = "Expecting JDK16+ but unable to determine target bytecode";
if (sourceUnit != null) {
@@ -225,28 +231,28 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
if (unsupportedTupleAttribute(tupleCons, "callSuper")) return;
}
- if (!memberHasValue(node, COPY_WITH, Boolean.FALSE) && !hasDeclaredMethod(cNode, COPY_WITH, 1)) {
+ if ((options == null || !memberHasValue(options, COPY_WITH, Boolean.FALSE)) && !hasDeclaredMethod(cNode, COPY_WITH, 1)) {
createCopyWith(cNode, pList);
}
- if (!memberHasValue(node, GET_AT, Boolean.FALSE) && !hasDeclaredMethod(cNode, GET_AT, 1)) {
+ if ((options == null || !memberHasValue(options, GET_AT, Boolean.FALSE)) && !hasDeclaredMethod(cNode, GET_AT, 1)) {
createGetAt(cNode, pList);
}
- if (!memberHasValue(node, TO_LIST, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_LIST, 0)) {
+ if ((options == null || !memberHasValue(options, TO_LIST, Boolean.FALSE)) && !hasDeclaredMethod(cNode, TO_LIST, 0)) {
createToList(cNode, pList);
}
- if (!memberHasValue(node, TO_MAP, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_MAP, 0)) {
+ if ((options == null || !memberHasValue(options, TO_MAP, Boolean.FALSE)) && !hasDeclaredMethod(cNode, TO_MAP, 0)) {
createToMap(cNode, pList);
}
- if (memberHasValue(node, COMPONENTS, Boolean.TRUE) && !hasDeclaredMethod(cNode, COMPONENTS, 0)) {
+ if (options != null && memberHasValue(options, COMPONENTS, Boolean.TRUE) && !hasDeclaredMethod(cNode, COMPONENTS, 0)) {
createComponents(cNode, pList);
}
- if (!hasDeclaredMethod(cNode, "size", 0)) {
- addGeneratedMethod(cNode, "size", PUBLIC_FINAL, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(constX(pList.size())));
+ if ((options == null || !memberHasValue(options, SIZE, Boolean.FALSE)) && !hasDeclaredMethod(cNode, SIZE, 0)) {
+ addGeneratedMethod(cNode, SIZE, PUBLIC_FINAL, int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, returnS(constX(pList.size())));
}
}
@@ -392,14 +398,16 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
}
private static RecordTypeMode getMode(AnnotationNode node, String name) {
- final Expression member = node.getMember(name);
- if (member instanceof PropertyExpression) {
- PropertyExpression prop = (PropertyExpression) member;
- Expression oe = prop.getObjectExpression();
- if (oe instanceof ClassExpression) {
- ClassExpression ce = (ClassExpression) oe;
- if (ce.getType().getName().equals("groovy.transform.RecordTypeMode")) {
- return RecordTypeMode.valueOf(prop.getPropertyAsString());
+ if (node != null) {
+ final Expression member = node.getMember(name);
+ if (member instanceof PropertyExpression) {
+ PropertyExpression prop = (PropertyExpression) member;
+ Expression oe = prop.getObjectExpression();
+ if (oe instanceof ClassExpression) {
+ ClassExpression ce = (ClassExpression) oe;
+ if (ce.getType().getName().equals("groovy.transform.RecordTypeMode")) {
+ return RecordTypeMode.valueOf(prop.getPropertyAsString());
+ }
}
}
}
diff --git a/src/spec/test/RecordSpecificationTest.groovy b/src/spec/test/RecordSpecificationTest.groovy
index ec6917c..7d29830 100644
--- a/src/spec/test/RecordSpecificationTest.groovy
+++ b/src/spec/test/RecordSpecificationTest.groovy
@@ -98,9 +98,9 @@ assert new Point3D(10, 20, 30).toString() == 'Point3D[coords=10,20,30]'
void testCopyWith() {
assertScript '''
-import groovy.transform.RecordBase
+import groovy.transform.RecordOptions
// tag::record_copywith[]
-@RecordBase(copyWith=true)
+@RecordOptions(copyWith=true)
record Fruit(String name, double price) {}
def apple = new Fruit('Apple', 11.6)
assert 'Apple' == apple.name()
@@ -112,10 +112,10 @@ def orange = apple.copyWith(name: 'Orange')
// end::record_copywith[]
'''
assertScript '''
-import groovy.transform.RecordBase
+import groovy.transform.RecordOptions
import static groovy.test.GroovyAssert.shouldFail
-@RecordBase(copyWith=false)
+@RecordOptions(copyWith=false)
record Fruit(String name, double price) {}
def apple = new Fruit('Apple', 11.6)
shouldFail(MissingMethodException) {
@@ -158,7 +158,7 @@ assert c == 'green'
// tag::record_components[]
import groovy.transform.*
-@RecordBase(componentTuple=true)
+@RecordOptions(componentTuple=true)
record Point(int x, int y, String color) { }
@CompileStatic
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
index 9036d92..96408da 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
@@ -92,7 +92,7 @@ class RecordTest {
assumeTrue(isAtLeastJdk('16.0'))
assertScript '''
import groovy.transform.*
- @RecordBase(mode=RecordTypeMode.EMULATE)
+ @RecordOptions(mode=RecordTypeMode.EMULATE)
record RecordJDK16plus2(String name) {}
assert java.lang.Record != RecordJDK16plus2.class.getSuperclass()
'''
@@ -340,8 +340,8 @@ class RecordTest {
@Test
void testCoerce() {
assertScript '''
+ @groovy.transform.CompileDynamic
record PersonDynamic(String name, int age) {}
- @groovy.transform.CompileStatic
record PersonStatic(String name, int age) {}
def testDynamic() {