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