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 02:10:27 UTC

[groovy] branch master updated: GROOVY-10338: Enhance records with additional helper methods

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 d7e725d  GROOVY-10338: Enhance records with additional helper methods
d7e725d is described below

commit d7e725de4a01adb1261b986d454bb50a5fe4ab27
Author: Paul King <pa...@asert.com.au>
AuthorDate: Mon Nov 1 23:22:26 2021 +1000

    GROOVY-10338: Enhance records with additional helper methods
---
 src/main/java/groovy/transform/RecordBase.java     | 93 +++++++++++++++++++-
 .../transform/RecordTypeASTTransformation.java     | 99 ++++++++++++++++++++--
 src/spec/test/RecordSpecificationTest.groovy       | 42 +++++++++
 3 files changed, 228 insertions(+), 6 deletions(-)

diff --git a/src/main/java/groovy/transform/RecordBase.java b/src/main/java/groovy/transform/RecordBase.java
index 4577e45..39ff6e7 100644
--- a/src/main/java/groovy/transform/RecordBase.java
+++ b/src/main/java/groovy/transform/RecordBase.java
@@ -68,4 +68,95 @@ public @interface RecordBase {
      * 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;}
+    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/org/codehaus/groovy/transform/RecordTypeASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
index e3b9f23..11de7bc 100644
--- a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -31,53 +31,65 @@ import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.GenericsType;
 import org.codehaus.groovy.ast.InnerClassNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.PropertyNode;
 import org.codehaus.groovy.ast.RecordComponentNode;
 import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.ClassExpression;
-import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MapEntryExpression;
 import org.codehaus.groovy.ast.expr.PropertyExpression;
 import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.SwitchStatement;
 import org.codehaus.groovy.ast.tools.GenericsUtils;
 import org.codehaus.groovy.classgen.asm.BytecodeHelper;
 import org.codehaus.groovy.control.CompilationUnit;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.CompilerConfiguration;
 import org.codehaus.groovy.control.SourceUnit;
-import org.codehaus.groovy.transform.stc.StaticTypesMarker;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.stream.Collectors;
 
 import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod;
+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.makeWithoutCaching;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.caseS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceProperties;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.mapEntryX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.mapX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.nullX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.switchS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
 import static org.objectweb.asm.Opcodes.ACC_FINAL;
@@ -95,11 +107,18 @@ import static org.objectweb.asm.Opcodes.IRETURN;
 @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
 public class RecordTypeASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
     private CompilationUnit compilationUnit;
+    private static final ClassNode ARRAYLIST_TYPE = makeWithoutCaching(ArrayList.class, false);
+    private static final String COMPONENTS = "components";
     private static final String COPY_WITH = "copyWith";
+    private static final String GET_AT = "getAt";
+    private static final ClassNode ILLEGAL_ARGUMENT = makeWithoutCaching(IllegalArgumentException.class);
+    private static final ClassNode LHMAP_TYPE = makeWithoutCaching(LinkedHashMap.class, false);
     private static final String NAMED_ARGS = "namedArgs";
     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 String TO_LIST = "toList";
+    private static final String TO_MAP = "toMap";
 
     private static final Class<? extends Annotation> MY_CLASS = RecordBase.class;
     public static final ClassNode MY_TYPE = makeWithoutCaching(MY_CLASS, false);
@@ -206,9 +225,79 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
             if (unsupportedTupleAttribute(tupleCons, "callSuper")) return;
         }
 
-        if (!pList.isEmpty() && !memberHasValue(node, COPY_WITH, Boolean.FALSE) && !hasDeclaredMethod(cNode, COPY_WITH, 1)) {
+        if (!memberHasValue(node, COPY_WITH, Boolean.FALSE) && !hasDeclaredMethod(cNode, COPY_WITH, 1)) {
             createCopyWith(cNode, pList);
         }
+
+        if (!memberHasValue(node, GET_AT, Boolean.FALSE) && !hasDeclaredMethod(cNode, GET_AT, 1)) {
+            createGetAt(cNode, pList);
+        }
+
+        if (!memberHasValue(node, TO_LIST, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_LIST, 0)) {
+            createToList(cNode, pList);
+        }
+
+        if (!memberHasValue(node, TO_MAP, Boolean.FALSE) && !hasDeclaredMethod(cNode, TO_MAP, 0)) {
+            createToMap(cNode, pList);
+        }
+
+        if (memberHasValue(node, COMPONENTS, Boolean.TRUE) && !hasDeclaredMethod(cNode, COMPONENTS, 0)) {
+            createComponents(cNode, pList);
+        }
+    }
+
+    private void createComponents(ClassNode cNode, List<PropertyNode> pList) {
+        if (pList.size() > 16) { // Groovy currently only goes to Tuple16
+            addError("Record has too many components for a components() method", cNode);
+        }
+        ClassNode tupleClass = getClass(cNode, "groovy.lang.Tuple" + pList.size());
+        if (tupleClass == null) return;
+        List<GenericsType> gtypes = new ArrayList<>();
+        ArgumentListExpression args = new ArgumentListExpression();
+        for (PropertyNode pNode : pList) {
+            args.addExpression(callThisX(pNode.getName()));
+            gtypes.add(new GenericsType(getWrapper(pNode.getType())));
+        }
+        tupleClass.setGenericsTypes(gtypes.toArray(new GenericsType[0]));
+        Statement body = returnS(ctorX(tupleClass, args));
+        addGeneratedMethod(cNode, COMPONENTS, PUBLIC_FINAL, tupleClass, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
+    }
+
+    private ClassNode getClass(ClassNode cNode, String tupleName) {
+        try {
+            return ClassHelper.makeWithoutCaching(Class.forName(tupleName)).getPlainNodeReference();
+        } catch(ClassNotFoundException cnfe) {
+            addError("Unable to find Tuple class '" + tupleName + "'", cNode);
+            return null;
+        }
+    }
+
+    private void createToList(ClassNode cNode, List<PropertyNode> pList) {
+        List<Expression> args = new ArrayList<>();
+        for (PropertyNode pNode : pList) {
+            args.add(callThisX(pNode.getName()));
+        }
+        Statement body = returnS(ctorX(ARRAYLIST_TYPE.getPlainNodeReference(), listX(args)));
+        addGeneratedMethod(cNode, TO_LIST, PUBLIC_FINAL, LIST_TYPE.getPlainNodeReference(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
+    }
+
+    private void createToMap(ClassNode cNode, List<PropertyNode> pList) {
+        List<MapEntryExpression> entries = new ArrayList<>();
+        for (PropertyNode pNode : pList) {
+            String name = pNode.getName();
+            entries.add(mapEntryX(name, callThisX(name)));
+        }
+        Statement body = returnS(ctorX(LHMAP_TYPE.getPlainNodeReference(), args(mapX(entries))));
+        addGeneratedMethod(cNode, TO_MAP, PUBLIC_FINAL, MAP_TYPE.getPlainNodeReference(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body);
+    }
+
+    private void createGetAt(ClassNode cNode, List<PropertyNode> pList) {
+        Expression i = varX("i");
+        SwitchStatement body = switchS(i, throwS(ctorX(ILLEGAL_ARGUMENT, args(plusX(constX("No record component with index: "), i)))));
+        for (int j = 0; j < pList.size(); j++) {
+            body.addCase(caseS(constX(j), returnS(callThisX(pList.get(j).getName()))));
+        }
+        addGeneratedMethod(cNode, GET_AT, PUBLIC_FINAL, ClassHelper.OBJECT_TYPE.getPlainNodeReference(), params(param(int_TYPE, "i")), ClassNode.EMPTY_ARRAY, body);
     }
 
     private void createCopyWith(ClassNode cNode, List<PropertyNode> pList) {
@@ -224,7 +313,7 @@ public class RecordTypeASTTransformation extends AbstractASTTransformation imple
             namedParam.addMember("required", constX(false, true));
             mapParam.addAnnotation(namedParam);
         }
-        Statement body = returnS(nullX() /*ctorX(cNode.getPlainNodeReference(), args)*/);
+        Statement body = returnS(nullX() /*ctorX(cNode.getPlainNodeReference(), args)*/); // TODO FIX
         addGeneratedMethod(cNode, COPY_WITH, PUBLIC_FINAL, cNode.getPlainNodeReference(), params(mapParam), ClassNode.EMPTY_ARRAY, body);
     }
 
diff --git a/src/spec/test/RecordSpecificationTest.groovy b/src/spec/test/RecordSpecificationTest.groovy
index 945154c..ec6917c 100644
--- a/src/spec/test/RecordSpecificationTest.groovy
+++ b/src/spec/test/RecordSpecificationTest.groovy
@@ -139,6 +139,48 @@ assert shop.items() == ['bread', 'milk']
 '''
     }
 
+    void testToList() {
+        assertScript '''
+// tag::record_to_list[]
+record Point(int x, int y, String color) { }
+
+def p = new Point(100, 200, 'green')
+def (x, y, c) = p.toList()
+assert x == 100
+assert y == 200
+assert c == 'green'
+// end::record_to_list[]
+'''
+    }
+
+    void testComponents() {
+        def assertScript = '''
+// tag::record_components[]
+import groovy.transform.*
+
+@RecordBase(componentTuple=true)
+record Point(int x, int y, String color) { }
+
+@CompileStatic
+def method() {
+    def p1 = new Point(100, 200, 'green')
+    def (int x1, int y1, String c1) = p1.components()
+    assert x1 == 100
+    assert y1 == 200
+    assert c1 == 'green'
+
+    def p2 = new Point(10, 20, 'blue')
+    def (x2, y2, c2) = p2.components()
+    assert x2 * 10 == 100
+    assert y2 ** 2 == 400
+    assert c2.toUpperCase() == 'BLUE'
+}
+
+method()
+// end::record_components[]
+'''
+    }
+
     void testRecordCompactConstructor() {
         assertScript '''
 // tag::record_compact_constructor[]