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 2020/09/26 04:02:16 UTC

[groovy] branch master updated (0e3fa36 -> 0ed9981)

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git.


    from 0e3fa36  Add a muse configuration (closes #1379)
     new 1e98a89  GROOVY-9753: EqualsAndHashCode should be enhanced to be POJO aware (closes #1374)
     new 45c38f9  GROOVY-9754: Provide a record-like equivalent (closes #1375)
     new cfa5374  GROOVY-9755: ToString AST transformation should be enhanced to be POJO aware (closes #1376)
     new 0ed9981  GROOVY-9756: MapConstructor generated code should be POJO aware (closes #1377)

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 src/main/groovy/groovy/transform/RecordType.groovy |  95 ++++++++++++
 .../java/groovy/transform/EqualsAndHashCode.java   |  13 ++
 .../transform/{Trait.java => RecordBase.java}      |  14 +-
 src/main/java/groovy/transform/ToString.java       |  12 ++
 .../transform/options/DefaultPropertyHandler.java  |   8 +-
 .../options/ImmutablePropertyHandler.java          |   6 +-
 .../groovy/ast/tools/ConstructorNodeUtils.java     |  37 ++++-
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |   5 +
 .../EqualsAndHashCodeASTTransformation.java        |  97 ++++++++++--
 .../transform/RecordTypeASTTransformation.java     | 163 +++++++++++++++++++++
 .../transform/ToStringASTTransformation.java       |  33 ++++-
 .../TupleConstructorASTTransformation.java         |   9 +-
 12 files changed, 455 insertions(+), 37 deletions(-)
 create mode 100644 src/main/groovy/groovy/transform/RecordType.groovy
 copy src/main/java/groovy/transform/{Trait.java => RecordBase.java} (76%)
 create mode 100644 src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java


[groovy] 01/04: GROOVY-9753: EqualsAndHashCode should be enhanced to be POJO aware (closes #1374)

Posted by pa...@apache.org.
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

commit 1e98a892715c24cf570b24c28aa46a8e54b79563
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Sep 22 19:53:54 2020 +1000

    GROOVY-9753: EqualsAndHashCode should be enhanced to be POJO aware (closes #1374)
---
 .../java/groovy/transform/EqualsAndHashCode.java   | 13 +++
 .../EqualsAndHashCodeASTTransformation.java        | 97 ++++++++++++++++++----
 2 files changed, 96 insertions(+), 14 deletions(-)

diff --git a/src/main/java/groovy/transform/EqualsAndHashCode.java b/src/main/java/groovy/transform/EqualsAndHashCode.java
index 20e0783..5a0fe83 100644
--- a/src/main/java/groovy/transform/EqualsAndHashCode.java
+++ b/src/main/java/groovy/transform/EqualsAndHashCode.java
@@ -283,4 +283,17 @@ public @interface EqualsAndHashCode {
      * @since 2.5.0
      */
     boolean allNames() default false;
+
+    /**
+     * Whether to avoid using Groovy runtime methods and instead use methods like {@link java.util.Objects#hash(Object...)}
+     * and {@link java.util.Objects#equals(Object, Object)} for the generated {@code equals} and {@code hashCode} methods.
+     * The generated code is more similar to what is typically used in POJO classes.
+     * The presence of the {@code @POJO} annotation on a class is looked for by default but this annotation attribute
+     * allows the feature to be explicitly configured if desired.
+     *
+     * <em>NOTE:</em> this is an incubating feature and may change in future versions.
+     *
+     * @since 4.0.0
+     */
+    boolean pojo() default false;
 }
diff --git a/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
index 5f4f2d3..b3e34fd 100644
--- a/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.transform;
 
 import groovy.transform.EqualsAndHashCode;
+import groovy.transform.stc.POJO;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.AnnotationNode;
@@ -28,13 +29,13 @@ import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.ast.MethodNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.BinaryExpression;
 import org.codehaus.groovy.ast.expr.CastExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.ast.tools.GenericsUtils;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport;
@@ -43,6 +44,7 @@ import org.codehaus.groovy.util.HashCodeHelper;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod;
@@ -59,6 +61,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getAllProperties;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getterThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getterX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.hasClassX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.hasDeclaredMethod;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.hasEqualFieldX;
@@ -81,6 +84,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.sameX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafe;
+import static org.codehaus.groovy.ast.tools.GenericsUtils.nonGeneric;
 import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
 import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
 import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
@@ -91,6 +95,8 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
     static final ClassNode MY_TYPE = make(MY_CLASS);
     static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
     private static final ClassNode HASHUTIL_TYPE = make(HashCodeHelper.class);
+    private static final ClassNode POJO_TYPE = make(POJO.class);
+    private static final ClassNode OBJECTS_TYPE = make(Objects.class);
     private static final ClassNode OBJECT_TYPE = makeClassSafe(Object.class);
 
     public void visit(ASTNode[] nodes, SourceUnit source) {
@@ -104,6 +110,14 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
             if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
             boolean callSuper = memberHasValue(anno, "callSuper", true);
             boolean cacheHashCode = memberHasValue(anno, "cache", true);
+            // Look for @POJO annotation by default but annotation attribute overrides
+            Object pojoMember = getMemberValue(anno, "pojo");
+            boolean pojo;
+            if (pojoMember == null) {
+                pojo = !cNode.getAnnotations(POJO_TYPE).isEmpty();
+            } else {
+                pojo = (boolean) pojoMember;
+            }
             boolean useCanEqual = !memberHasValue(anno, "useCanEqual", false);
             if (callSuper && cNode.getSuperClass().getName().equals("java.lang.Object")) {
                 addError("Error during " + MY_TYPE_NAME + " processing: callSuper=true but '" + cNode.getName() + "' has no super class.", anno);
@@ -116,8 +130,8 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
             if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return;
             if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields)) return;
             if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields)) return;
-            createHashCode(cNode, cacheHashCode, includeFields, callSuper, excludes, includes, allNames, allProperties);
-            createEquals(cNode, includeFields, callSuper, useCanEqual, excludes, includes, allNames, allProperties);
+            createHashCode(cNode, cacheHashCode, includeFields, callSuper, excludes, includes, allNames, allProperties, pojo);
+            createEquals(cNode, includeFields, callSuper, useCanEqual, excludes, includes, allNames, allProperties, pojo);
         }
     }
 
@@ -130,6 +144,10 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
     }
 
     public static void createHashCode(ClassNode cNode, boolean cacheResult, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties) {
+        createHashCode(cNode, cacheResult, includeFields, callSuper, excludes, includes, allNames, allProperties, false);
+    }
+
+    public static void createHashCode(ClassNode cNode, boolean cacheResult, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties, boolean pojo) {
         // make a public method if none exists otherwise try a private method with leading underscore
         boolean hasExistingHashCode = hasDeclaredMethod(cNode, "hashCode", 0);
         if (hasExistingHashCode && hasDeclaredMethod(cNode, "_hashCode", 0)) return;
@@ -141,11 +159,11 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
             final Expression hash = varX(hashField);
             body.addStatement(ifS(
                     isZeroX(hash),
-                    calculateHashStatements(cNode, hash, includeFields, callSuper, excludes, includes, allNames, allProperties)
+                    calculateHashStatements(cNode, hash, includeFields, callSuper, excludes, includes, allNames, allProperties, pojo)
             ));
             body.addStatement(returnS(hash));
         } else {
-            body.addStatement(calculateHashStatements(cNode, null, includeFields, callSuper, excludes, includes, allNames, allProperties));
+            body.addStatement(calculateHashStatements(cNode, null, includeFields, callSuper, excludes, includes, allNames, allProperties, pojo));
         }
 
         addGeneratedMethod(cNode,
@@ -157,7 +175,14 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
                 body);
     }
 
-    private static Statement calculateHashStatements(ClassNode cNode, Expression hash, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties) {
+    private static Statement calculateHashStatements(ClassNode cNode, Expression hash, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties, boolean pojo) {
+        if (pojo) {
+            return calculateHashStatementsPOJO(cNode, hash, includeFields, callSuper, excludes, includes, allNames, allProperties);
+        }
+        return calculateHashStatementsDefault(cNode, hash, includeFields, callSuper, excludes, includes, allNames, allProperties);
+    }
+
+    private static Statement calculateHashStatementsDefault(ClassNode cNode, Expression hash, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties) {
         final Set<String> names = new HashSet<String>();
         final List<PropertyNode> pList = getAllProperties(names, cNode, true, false, allProperties, false, false, false);
         final List<FieldNode> fList = new ArrayList<FieldNode>();
@@ -202,13 +227,42 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
         return body;
     }
 
+    private static Statement calculateHashStatementsPOJO(ClassNode cNode, Expression hash, boolean includeFields, boolean callSuper, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties) {
+        final Set<String> names = new HashSet<>();
+        final List<PropertyNode> pList = getAllProperties(names, cNode, true, false, allProperties, false, false, false);
+        final List<FieldNode> fList = new ArrayList<>();
+        if (includeFields) {
+            fList.addAll(getInstanceNonPropertyFields(cNode));
+        }
+        final BlockStatement body = new BlockStatement();
+        final ArgumentListExpression args = new ArgumentListExpression();
+        for (PropertyNode pNode : pList) {
+            if (shouldSkipUndefinedAware(pNode.getName(), excludes, includes, allNames)) continue;
+            args.addExpression(getterThisX(cNode, pNode));
+        }
+        for (FieldNode fNode : fList) {
+            if (shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue;
+            args.addExpression(varX(fNode));
+        }
+        if (callSuper) {
+            args.addExpression(varX("super"));
+        }
+        Expression calcHash = callX(OBJECTS_TYPE, "hash", args);
+        if (hash != null) {
+            body.addStatement(assignS(hash, calcHash));
+        } else {
+            body.addStatement(returnS(calcHash));
+        }
+        return body;
+    }
+
     private static void createCanEqual(ClassNode cNode) {
         boolean hasExistingCanEqual = hasDeclaredMethod(cNode, "canEqual", 1);
         if (hasExistingCanEqual && hasDeclaredMethod(cNode, "_canEqual", 1)) return;
 
         final BlockStatement body = new BlockStatement();
         VariableExpression other = varX("other");
-        body.addStatement(returnS(isInstanceOfX(other, GenericsUtils.nonGeneric(cNode))));
+        body.addStatement(returnS(isInstanceOfX(other, nonGeneric(cNode))));
         MethodNode canEqual = addGeneratedMethod(cNode,
                 hasExistingCanEqual ? "_canEqual" : "canEqual",
                 hasExistingCanEqual ? ACC_PRIVATE : ACC_PUBLIC,
@@ -229,6 +283,10 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
     }
 
     public static void createEquals(ClassNode cNode, boolean includeFields, boolean callSuper, boolean useCanEqual, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties) {
+        createEquals(cNode, includeFields, callSuper, useCanEqual, excludes, includes, allNames, allProperties, false);
+    }
+
+    public static void createEquals(ClassNode cNode, boolean includeFields, boolean callSuper, boolean useCanEqual, List<String> excludes, List<String> includes, boolean allNames, boolean allProperties, boolean pojo) {
         if (useCanEqual) createCanEqual(cNode);
         // make a public method if none exists otherwise try a private method with leading underscore
         boolean hasExistingEquals = hasDeclaredMethod(cNode, "equals", 1);
@@ -242,13 +300,17 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
         body.addStatement(ifS(sameX(varX("this"), other), returnS(constX(Boolean.TRUE, true))));
 
         if (useCanEqual) {
-            body.addStatement(ifS(notX(isInstanceOfX(other, GenericsUtils.nonGeneric(cNode))), returnS(constX(Boolean.FALSE,true))));
+            body.addStatement(ifS(notX(isInstanceOfX(other, nonGeneric(cNode))), returnS(constX(Boolean.FALSE,true))));
         } else {
-            body.addStatement(ifS(notX(hasClassX(other, GenericsUtils.nonGeneric(cNode))), returnS(constX(Boolean.FALSE,true))));
+            Expression classesEqual = pojo
+                    ? callX(callThisX("getClass"), "equals", callX(other, "getClass"))
+                    : hasClassX(other, nonGeneric(cNode));
+            body.addStatement(ifS(notX(classesEqual), returnS(constX(Boolean.FALSE,true))));
         }
 
-        VariableExpression otherTyped = localVarX("otherTyped", GenericsUtils.nonGeneric(cNode));
-        CastExpression castExpression = new CastExpression(GenericsUtils.nonGeneric(cNode), other);
+        VariableExpression otherTyped = localVarX("otherTyped", nonGeneric(cNode));
+        ClassNode originType = otherTyped.getOriginType();
+        CastExpression castExpression = new CastExpression(nonGeneric(cNode), other);
         castExpression.setStrict(true);
         body.addStatement(declS(otherTyped, castExpression));
 
@@ -263,15 +325,18 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
             boolean canBeSelf = StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf(
                     pNode.getOriginType(), cNode
             );
+            Expression propsEqual = pojo
+                    ? callX(OBJECTS_TYPE, "equals", args(getterThisX(originType, pNode), getterX(originType, otherTyped, pNode)))
+                    : hasEqualPropertyX(originType, pNode, otherTyped);
             if (!canBeSelf) {
-                body.addStatement(ifS(notX(hasEqualPropertyX(otherTyped.getOriginType(), pNode, otherTyped)), returnS(constX(Boolean.FALSE, true))));
+                body.addStatement(ifS(notX(propsEqual), returnS(constX(Boolean.FALSE, true))));
             } else {
                 body.addStatement(
                         ifS(notX(hasSamePropertyX(pNode, otherTyped)),
                                 ifElseS(differentSelfRecursivePropertyX(pNode, otherTyped),
                                         returnS(constX(Boolean.FALSE, true)),
                                         ifS(notX(bothSelfRecursivePropertyX(pNode, otherTyped)),
-                                                ifS(notX(hasEqualPropertyX(otherTyped.getOriginType(), pNode, otherTyped)), returnS(constX(Boolean.FALSE, true))))
+                                                ifS(notX(propsEqual), returnS(constX(Boolean.FALSE, true))))
                                 )
                         )
                 );
@@ -283,12 +348,16 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
         }
         for (FieldNode fNode : fList) {
             if (shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue;
+            Expression fieldsEqual = pojo
+                    ? callX(OBJECTS_TYPE, "equals", args(varX(fNode), propX(otherTyped, fNode.getName())))
+                    : hasEqualFieldX(fNode, otherTyped);
+
             body.addStatement(
                     ifS(notX(hasSameFieldX(fNode, otherTyped)),
                             ifElseS(differentSelfRecursiveFieldX(fNode, otherTyped),
                                     returnS(constX(Boolean.FALSE,true)),
                                     ifS(notX(bothSelfRecursiveFieldX(fNode, otherTyped)),
-                                            ifS(notX(hasEqualFieldX(fNode, otherTyped)), returnS(constX(Boolean.FALSE,true)))))
+                                            ifS(notX(fieldsEqual), returnS(constX(Boolean.FALSE,true)))))
                     ));
         }
         if (callSuper) {


[groovy] 04/04: GROOVY-9756: MapConstructor generated code should be POJO aware (closes #1377)

Posted by pa...@apache.org.
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

commit 0ed998165a05d9f0ddc4466e1e84bd4ac01e8ca6
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Sep 23 19:14:13 2020 +1000

    GROOVY-9756: MapConstructor generated code should be POJO aware (closes #1377)
---
 .../transform/options/DefaultPropertyHandler.java  |  8 +++--
 .../options/ImmutablePropertyHandler.java          |  6 ++--
 .../groovy/ast/tools/ConstructorNodeUtils.java     | 37 +++++++++++++++++++---
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |  5 +++
 .../TupleConstructorASTTransformation.java         |  9 ++++--
 5 files changed, 55 insertions(+), 10 deletions(-)

diff --git a/src/main/java/groovy/transform/options/DefaultPropertyHandler.java b/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
index 4fb2e04..7d5201b 100644
--- a/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
+++ b/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
@@ -18,6 +18,7 @@
  */
 package groovy.transform.options;
 
+import groovy.transform.stc.POJO;
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassNode;
 import org.codehaus.groovy.ast.FieldNode;
@@ -35,7 +36,8 @@ import org.codehaus.groovy.transform.MapConstructorASTTransformation;
 
 import java.util.List;
 
-import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesExpr;
+import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesS;
+import static org.codehaus.groovy.ast.ClassHelper.make;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
@@ -49,6 +51,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 
 public class DefaultPropertyHandler extends PropertyHandler {
+    private static final ClassNode POJO_TYPE = make(POJO.class);
 
     @Override
     public boolean validateAttributes(AbstractASTTransformation xform, AnnotationNode anno) {
@@ -62,7 +65,8 @@ public class DefaultPropertyHandler extends PropertyHandler {
         if (xform instanceof MapConstructorASTTransformation) {
             VariableExpression namedArgs = varX("args");
             body.addStatement(ifS(equalsNullX(namedArgs), assignS(namedArgs, new MapExpression())));
-            body.addStatement(stmt(checkPropNamesExpr(namedArgs)));
+            boolean pojo = !cNode.getAnnotations(POJO_TYPE).isEmpty();
+            body.addStatement(checkPropNamesS(namedArgs, pojo, props));
         }
         return super.validateProperties(xform, body, cNode, props);
     }
diff --git a/src/main/java/groovy/transform/options/ImmutablePropertyHandler.java b/src/main/java/groovy/transform/options/ImmutablePropertyHandler.java
index e3c74a4..9253c28 100644
--- a/src/main/java/groovy/transform/options/ImmutablePropertyHandler.java
+++ b/src/main/java/groovy/transform/options/ImmutablePropertyHandler.java
@@ -49,7 +49,7 @@ import java.util.Set;
 import java.util.SortedMap;
 import java.util.SortedSet;
 
-import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesExpr;
+import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesS;
 import static org.apache.groovy.ast.tools.ImmutablePropertyUtils.cloneArrayOrCloneableExpr;
 import static org.apache.groovy.ast.tools.ImmutablePropertyUtils.cloneDateExpr;
 import static org.apache.groovy.ast.tools.ImmutablePropertyUtils.derivesFromDate;
@@ -91,6 +91,7 @@ public class ImmutablePropertyHandler extends PropertyHandler {
     private static final ClassNode SET_CLASSNODE = make(Set.class);
     private static final ClassNode MAP_CLASSNODE = make(Map.class);
     private static final ClassNode READONLYEXCEPTION_TYPE = make(ReadOnlyPropertyException.class);
+    private static final ClassNode POJO_TYPE = make(POJO.class);
 
     @Override
     public Statement createPropGetter(PropertyNode pNode) {
@@ -124,7 +125,8 @@ public class ImmutablePropertyHandler extends PropertyHandler {
         if (xform instanceof MapConstructorASTTransformation) {
             VariableExpression namedArgs = varX("args");
             body.addStatement(ifS(equalsNullX(namedArgs), assignS(namedArgs, new MapExpression())));
-            body.addStatement(stmt(checkPropNamesExpr(namedArgs)));
+            boolean pojo = !cNode.getAnnotations(POJO_TYPE).isEmpty();
+            body.addStatement(checkPropNamesS(namedArgs, pojo, props));
         }
         return super.validateProperties(xform, body, cNode, props);
     }
diff --git a/src/main/java/org/apache/groovy/ast/tools/ConstructorNodeUtils.java b/src/main/java/org/apache/groovy/ast/tools/ConstructorNodeUtils.java
index 849ef9d..bdc2f9a 100644
--- a/src/main/java/org/apache/groovy/ast/tools/ConstructorNodeUtils.java
+++ b/src/main/java/org/apache/groovy/ast/tools/ConstructorNodeUtils.java
@@ -18,10 +18,13 @@
  */
 package org.apache.groovy.ast.tools;
 
+import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.PropertyNode;
 import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.ListExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.ExpressionStatement;
@@ -33,6 +36,17 @@ import java.util.List;
 import static org.codehaus.groovy.ast.ClassHelper.make;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+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.declS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.forS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.notX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 
 /**
@@ -40,13 +54,14 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
  */
 public class ConstructorNodeUtils {
     private static final ClassNode IMMUTABLE_TYPE = make(ImmutableASTTransformation.class);
+    private static final ClassNode EXCEPTION = make(IllegalArgumentException.class);
 
     private ConstructorNodeUtils() { }
 
     /**
      * Return the first statement from the constructor code if it is a call to super or this, otherwise null.
      *
-     * @param code
+     * @param code the code to check
      * @return the first statement if a special call or null
      */
     public static ConstructorCallExpression getFirstIfSpecialConstructorCall(Statement code) {
@@ -69,7 +84,21 @@ public class ConstructorNodeUtils {
         return null;
     }
 
-    public static StaticMethodCallExpression checkPropNamesExpr(VariableExpression namedArgs) {
-        return callX(IMMUTABLE_TYPE, "checkPropNames", args(varX("this"), namedArgs));
+    public static Statement checkPropNamesS(VariableExpression namedArgs, boolean pojo, List<PropertyNode> props) {
+        if (!pojo) {
+            return stmt(callX(IMMUTABLE_TYPE, "checkPropNames", args(varX("this"), namedArgs)));
+        }
+        BlockStatement block = new BlockStatement();
+        ListExpression knownPropNames = new ListExpression();
+        for (PropertyNode pNode : props) {
+            knownPropNames.addExpression(constX(pNode.getName()));
+        }
+        VariableExpression validNames = localVarX("validNames", ClassHelper.LIST_TYPE);
+        Parameter name = param(ClassHelper.STRING_TYPE, "arg");
+        Statement loopS = ifS(notX(callX(validNames, "contains", varX(name))),
+                throwS(ctorX(EXCEPTION, plusX(constX("Unknown named argument: "), varX(name)))));
+        block.addStatement(declS(validNames, knownPropNames));
+        block.addStatement(forS(name, callX(namedArgs, "keySet"), loopS));
+        return block;
     }
 }
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 3bfc01a..6abbe61 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -56,6 +56,7 @@ import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.CatchStatement;
 import org.codehaus.groovy.ast.stmt.EmptyStatement;
 import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
 import org.codehaus.groovy.ast.stmt.IfStatement;
 import org.codehaus.groovy.ast.stmt.ReturnStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
@@ -341,6 +342,10 @@ public class GeneralUtils {
         return propX(varX("args"), argName);
     }
 
+    public static ForStatement forS(Parameter variable, Expression collectionExpression, Statement loopS) {
+        return new ForStatement(variable, collectionExpression, loopS);
+    }
+
     public static List<MethodNode> getAllMethods(final ClassNode type) {
         ClassNode node = type;
         List<MethodNode> result = new ArrayList<>();
diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index 636eb2c..dff3a9a 100644
--- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -22,6 +22,7 @@ import groovy.lang.GroovyClassLoader;
 import groovy.transform.CompilationUnitAware;
 import groovy.transform.TupleConstructor;
 import groovy.transform.options.PropertyHandler;
+import groovy.transform.stc.POJO;
 import org.apache.groovy.ast.tools.AnnotatedNodeUtils;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.AnnotatedNode;
@@ -56,7 +57,7 @@ import java.util.Set;
 
 import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
 import static org.apache.groovy.ast.tools.ClassNodeUtils.hasExplicitConstructor;
-import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesExpr;
+import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesS;
 import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility;
 import static org.codehaus.groovy.ast.ClassHelper.make;
 import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
@@ -91,6 +92,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
     static final ClassNode MY_TYPE = make(MY_CLASS);
     static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
     private static final ClassNode LHMAP_TYPE = makeWithoutCaching(LinkedHashMap.class, false);
+    private static final ClassNode POJO_TYPE = make(POJO.class);
     private static final Map<Class<?>, Expression> primitivesInitialValues;
 
     static {
@@ -331,6 +333,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
 
     private static BlockStatement processArgsBlock(ClassNode cNode, VariableExpression namedArgs) {
         BlockStatement block = new BlockStatement();
+        List<PropertyNode> props = new ArrayList<>();
         for (PropertyNode pNode : cNode.getProperties()) {
             if (pNode.isStatic()) continue;
 
@@ -339,8 +342,10 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
                     callX(namedArgs, "containsKey", constX(pNode.getName())),
                     assignS(varX(pNode), propX(namedArgs, pNode.getName())));
             block.addStatement(ifStatement);
+            props.add(pNode);
         }
-        block.addStatement(stmt(checkPropNamesExpr(namedArgs)));
+        boolean pojo = !cNode.getAnnotations(POJO_TYPE).isEmpty();
+        block.addStatement(checkPropNamesS(namedArgs, pojo, props));
         return block;
     }
 


[groovy] 02/04: GROOVY-9754: Provide a record-like equivalent (closes #1375)

Posted by pa...@apache.org.
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

commit 45c38f97031d3d457ee539738f71143ff441b8a7
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Sep 22 21:47:39 2020 +1000

    GROOVY-9754: Provide a record-like equivalent (closes #1375)
---
 src/main/groovy/groovy/transform/RecordType.groovy |  95 ++++++++++++
 src/main/java/groovy/transform/RecordBase.java     |  42 ++++++
 .../transform/RecordTypeASTTransformation.java     | 163 +++++++++++++++++++++
 3 files changed, 300 insertions(+)

diff --git a/src/main/groovy/groovy/transform/RecordType.groovy b/src/main/groovy/groovy/transform/RecordType.groovy
new file mode 100644
index 0000000..a2b7bc5
--- /dev/null
+++ b/src/main/groovy/groovy/transform/RecordType.groovy
@@ -0,0 +1,95 @@
+/*
+ *  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 groovy.transform
+
+import groovy.transform.options.ImmutablePropertyHandler
+import groovy.transform.stc.POJO
+import org.apache.groovy.lang.annotation.Incubating
+
+import java.lang.annotation.ElementType
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import java.lang.annotation.Target
+
+/**
+ * Meta annotation used when defining record-like classes.
+ * <p>
+ * It allows you to write classes in this shortened form:
+ *
+ * <pre class="groovyTestCase">
+ * {@code @groovy.transform.RecordType}
+ * class Cyclist {
+ *     String firstName
+ *     String lastName
+ * }
+ *
+ * def richie = new Cyclist('Richie', 'Porte')
+ * assert richie.toString() =~ /Cyclist.*firstName.*Richie/
+ * </pre>
+ *
+ * The {@code @RecordType} meta-annotation corresponds to adding the following annotations:
+ * {@link RecordBase},
+ * {@link ToString},
+ * {@link EqualsAndHashCode},
+ * {@link ImmutableOptions},
+ * {@link PropertyOptions},
+ * {@link TupleConstructor},
+ * {@link MapConstructor} and
+ * {@link KnownImmutable}.
+ *
+ * Together these annotations instruct the compiler to execute the necessary transformations to add
+ * the necessary getters, constructors, equals, hashCode and other helper methods that are typically
+ * written when creating record-like classes with the defined properties.
+ * <p>
+ * A class created in this way has the following characteristics:
+ * <ul>
+ * <li>The class is automatically made final
+ * <li>The serialVersionUID is by default 0
+ * <li>Properties automatically have private, final backing fields with getters which are the same name as the fields.
+ * <li>A map-based constructor is provided which allows you to set properties by name.
+ * <li>A tuple-style constructor is provided which allows you to set properties in the same order as they are defined.
+ * <li>Default {@code equals}, {@code hashCode} and {@code toString} methods are provided based on the property values.
+ * </ul>
+ * Record-like classes are particularly useful for data structures.
+ *
+ * @see ToString
+ * @see EqualsAndHashCode
+ * @see RecordBase
+ * @see ImmutableOptions
+ * @see PropertyOptions
+ * @see TupleConstructor
+ * @see MapConstructor
+ * @see KnownImmutable*
+ * @since 4.0.0
+ */
+@RecordBase
+@ToString(cache = true, includeNames = true)
+@EqualsAndHashCode(cache = true, useCanEqual = false)
+@ImmutableOptions
+@PropertyOptions(propertyHandler = ImmutablePropertyHandler)
+@TupleConstructor(defaults = false)
+@MapConstructor
+@KnownImmutable
+@POJO
+@AnnotationCollector(mode = AnnotationCollectorMode.PREFER_EXPLICIT_MERGED)
+@Retention(RetentionPolicy.RUNTIME)
+@Target([ElementType.TYPE])
+@Incubating
+@interface RecordType {
+}
diff --git a/src/main/java/groovy/transform/RecordBase.java b/src/main/java/groovy/transform/RecordBase.java
new file mode 100644
index 0000000..1fdfb0a
--- /dev/null
+++ b/src/main/java/groovy/transform/RecordBase.java
@@ -0,0 +1,42 @@
+/*
+ *  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 groovy.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of record-like classes.
+ *
+ * @see ImmutableOptions
+ * @see MapConstructor
+ * @see TupleConstructor
+ * @see PropertyOptions
+ * @since 4.0.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.RecordTypeASTTransformation")
+public @interface RecordBase {
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
new file mode 100644
index 0000000..d9ad857
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -0,0 +1,163 @@
+/*
+ *  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 org.codehaus.groovy.transform;
+
+import groovy.lang.GroovyClassLoader;
+import groovy.transform.CompilationUnitAware;
+import groovy.transform.RecordBase;
+import groovy.transform.options.PropertyHandler;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+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.PropertyNode;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceProperties;
+import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+
+/**
+ * Handles generation of code for the @RecordType annotation.
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class RecordTypeASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
+    private CompilationUnit compilationUnit;
+
+    private static final Class<? extends Annotation> MY_CLASS = RecordBase.class;
+    public static final ClassNode MY_TYPE = makeWithoutCaching(MY_CLASS, false);
+    private static final String MY_TYPE_NAME = MY_TYPE.getNameWithoutPackage();
+
+    @Override
+    public String getAnnotationName() {
+        return MY_TYPE_NAME;
+    }
+
+    public void visit(ASTNode[] nodes, SourceUnit source) {
+        init(nodes, source);
+        AnnotatedNode parent = (AnnotatedNode) nodes[1];
+        AnnotationNode anno = (AnnotationNode) nodes[0];
+        if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+        if (parent instanceof ClassNode) {
+            final GroovyClassLoader classLoader = compilationUnit != null ? compilationUnit.getTransformLoader() : source.getClassLoader();
+            final PropertyHandler handler = PropertyHandler.createPropertyHandler(this, classLoader, (ClassNode) parent);
+            if (handler == null) return;
+            if (!handler.validateAttributes(this, anno)) return;
+            doMakeImmutable((ClassNode) parent, anno, handler);
+        }
+    }
+
+    private void doMakeImmutable(ClassNode cNode, AnnotationNode node, PropertyHandler handler) {
+        List<PropertyNode> newProperties = new ArrayList<PropertyNode>();
+
+        String cName = cNode.getName();
+        if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
+        makeClassFinal(this, cNode);
+
+        final List<PropertyNode> pList = getInstanceProperties(cNode);
+        for (PropertyNode pNode : pList) {
+            adjustPropertyForImmutability(pNode, newProperties, handler);
+        }
+        // replace props with final version of self
+        for (PropertyNode oldProp : newProperties) {
+            cNode.getProperties().remove(oldProp);
+            PropertyNode newProp = new PropertyNode(oldProp.getField(), oldProp.getModifiers() | ACC_FINAL, oldProp.getGetterBlock(), null);
+            newProp.setGetterName(oldProp.getGetterNameOrDefault());
+            cNode.removeField(oldProp.getField().getName());
+            cNode.addProperty(newProp);
+        }
+        final List<FieldNode> fList = cNode.getFields();
+        for (FieldNode fNode : fList) {
+            ensureNotPublic(this, cName, fNode);
+        }
+        // 0L serialVersionUID by default
+        if (cNode.getDeclaredField("serialVersionUID") == null) {
+            cNode.addField("serialVersionUID", ACC_PRIVATE | ACC_STATIC | ACC_FINAL, ClassHelper.long_TYPE, constX(0L));
+        }
+        if (hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
+            AnnotationNode tupleCons = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0);
+            if (unsupportedTupleAttribute(tupleCons, "excludes")) return;
+            if (unsupportedTupleAttribute(tupleCons, "includes")) return;
+            if (unsupportedTupleAttribute(tupleCons, "includeProperties")) return;
+            if (unsupportedTupleAttribute(tupleCons, "includeSuperFields")) return;
+            if (unsupportedTupleAttribute(tupleCons, "callSuper")) return;
+            if (unsupportedTupleAttribute(tupleCons, "force")) return;
+        }
+    }
+
+    private boolean unsupportedTupleAttribute(AnnotationNode anno, String memberName) {
+        if (getMemberValue(anno, memberName) != null) {
+            String tname = TupleConstructorASTTransformation.MY_TYPE_NAME;
+            addError("Error during " + MY_TYPE_NAME + " processing: Annotation attribute '" + memberName +
+                    "' not supported for " + tname + " when used with " + MY_TYPE_NAME, anno);
+            return true;
+        }
+        return false;
+    }
+
+    private static void makeClassFinal(AbstractASTTransformation xform, ClassNode cNode) {
+        int modifiers = cNode.getModifiers();
+        if ((modifiers & ACC_FINAL) == 0) {
+            if ((modifiers & (ACC_ABSTRACT | ACC_SYNTHETIC)) == (ACC_ABSTRACT | ACC_SYNTHETIC)) {
+                xform.addError("Error during " + MY_TYPE_NAME + " processing: annotation found on inappropriate class " + cNode.getName(), cNode);
+                return;
+            }
+            cNode.setModifiers(modifiers | ACC_FINAL);
+        }
+    }
+
+    private static void ensureNotPublic(AbstractASTTransformation xform, String cNode, FieldNode fNode) {
+        String fName = fNode.getName();
+        if (fNode.isPublic() && !fName.contains("$") && !(fNode.isStatic() && fNode.isFinal())) {
+            xform.addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode);
+        }
+    }
+
+    private static void adjustPropertyForImmutability(PropertyNode pNode, List<PropertyNode> newNodes, PropertyHandler handler) {
+        final FieldNode fNode = pNode.getField();
+        fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL | ACC_PRIVATE);
+        Statement getter = handler.createPropGetter(pNode);
+        if (getter != null) {
+            pNode.setGetterBlock(getter);
+            pNode.setGetterName(pNode.getName());
+        }
+        newNodes.add(pNode);
+    }
+
+    @Override
+    public void setCompilationUnit(CompilationUnit unit) {
+        this.compilationUnit = unit;
+    }
+}


[groovy] 03/04: GROOVY-9755: ToString AST transformation should be enhanced to be POJO aware (closes #1376)

Posted by pa...@apache.org.
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

commit cfa5374722db23ae5940685620e0016ef1af953f
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Sep 23 14:06:57 2020 +1000

    GROOVY-9755: ToString AST transformation should be enhanced to be POJO aware (closes #1376)
---
 src/main/java/groovy/transform/ToString.java       | 12 ++++++++
 .../transform/ToStringASTTransformation.java       | 33 ++++++++++++++++------
 2 files changed, 37 insertions(+), 8 deletions(-)

diff --git a/src/main/java/groovy/transform/ToString.java b/src/main/java/groovy/transform/ToString.java
index 7ed6e9b..2899e10 100644
--- a/src/main/java/groovy/transform/ToString.java
+++ b/src/main/java/groovy/transform/ToString.java
@@ -351,4 +351,16 @@ public @interface ToString {
      * @since 2.5.0
      */
     boolean allNames() default false;
+
+    /**
+     * Whether to avoid using Groovy runtime methods when printing the toString for class members.
+     * The generated code is more similar to what is typically used in POJO classes.
+     * The presence of the {@code @POJO} annotation on a class is looked for by default but this annotation attribute
+     * allows the feature to be explicitly configured if desired.
+     *
+     * <em>NOTE:</em> this is an incubating feature and may change in future versions.
+     *
+     * @since 4.0.0
+     */
+    boolean pojo() default false;
 }
diff --git a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java
index 7a05ad7..498b762 100644
--- a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.transform;
 
 import groovy.transform.ToString;
+import groovy.transform.stc.POJO;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.AnnotationNode;
@@ -80,6 +81,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
     static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
     private static final ClassNode STRINGBUILDER_TYPE = make(StringBuilder.class);
     private static final ClassNode INVOKER_TYPE = make(InvokerHelper.class);
+    private static final ClassNode POJO_TYPE = make(POJO.class);
 
     public void visit(ASTNode[] nodes, SourceUnit source) {
         init(nodes, source);
@@ -108,11 +110,19 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
             boolean includePackage = !memberHasValue(anno, "includePackage", false);
             boolean allProperties = !memberHasValue(anno, "allProperties", false);
             boolean allNames = memberHasValue(anno, "allNames", true);
+            // Look for @POJO annotation by default but annotation attribute overrides
+            Object pojoMember = getMemberValue(anno, "pojo");
+            boolean pojo;
+            if (pojoMember == null) {
+                pojo = !cNode.getAnnotations(POJO_TYPE).isEmpty();
+            } else {
+                pojo = (boolean) pojoMember;
+            }
 
             if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return;
             if (!checkPropertyList(cNode, includes != null ? DefaultGroovyMethods.minus(includes, "super") : null, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties)) return;
             if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, allProperties)) return;
-            createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cacheToString, includeSuperProperties, allProperties, allNames, includeSuperFields);
+            createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cacheToString, includeSuperProperties, allProperties, allNames, includeSuperFields, pojo);
         }
     }
 
@@ -141,6 +151,10 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
     }
 
     public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean cache, boolean includeSuperProperties, boolean allProperties, boolean allNames, boolean includeSuperFields) {
+        createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cache, includeSuperProperties, allProperties, allNames, includeSuperFields, false);
+    }
+
+    public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List<String> excludes, List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean cache, boolean includeSuperProperties, boolean allProperties, boolean allNames, boolean includeSuperFields, boolean pojo) {
         // make a public method if none exists otherwise try a private method with leading underscore
         boolean hasExistingToString = hasDeclaredMethod(cNode, "toString", 0);
         if (hasExistingToString && hasDeclaredMethod(cNode, "_toString", 0)) return;
@@ -152,11 +166,11 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
             final Expression savedToString = varX(cacheField);
             body.addStatement(ifS(
                     equalsNullX(savedToString),
-                    assignS(savedToString, calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames))
+                    assignS(savedToString, calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames, pojo))
             ));
             tempToString = savedToString;
         } else {
-            tempToString = calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames);
+            tempToString = calculateToStringStatements(cNode, includeSuper, includeFields, includeSuperFields, excludes, includes, includeNames, ignoreNulls, includePackage, includeSuperProperties, allProperties, body, allNames, pojo);
         }
         body.addStatement(returnS(tempToString));
 
@@ -176,7 +190,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
         boolean canBeSelf;
     }
 
-    private static Expression calculateToStringStatements(ClassNode cNode, boolean includeSuper, boolean includeFields, boolean includeSuperFields, List<String> excludes, final List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean includeSuperProperties, boolean allProperties, BlockStatement body, boolean allNames) {
+    private static Expression calculateToStringStatements(ClassNode cNode, boolean includeSuper, boolean includeFields, boolean includeSuperFields, List<String> excludes, final List<String> includes, boolean includeNames, boolean ignoreNulls, boolean includePackage, boolean includeSuperProperties, boolean allProperties, BlockStatement body, boolean allNames, boolean pojo) {
         // def _result = new StringBuilder()
         final Expression result = localVarX("_result");
         body.addStatement(declS(result, ctorX(STRINGBUILDER_TYPE)));
@@ -225,7 +239,7 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
         }
 
         for (ToStringElement el : elements) {
-            appendValue(body, result, first, el.value, el.name, includeNames, ignoreNulls, el.canBeSelf);
+            appendValue(body, result, first, el.value, el.name, includeNames, ignoreNulls, el.canBeSelf, pojo);
         }
 
         // wrap up
@@ -235,18 +249,21 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
         return toString;
     }
 
-    private static void appendValue(BlockStatement body, Expression result, VariableExpression first, Expression value, String name, boolean includeNames, boolean ignoreNulls, boolean canBeSelf) {
+    private static void appendValue(BlockStatement body, Expression result, VariableExpression first, Expression value, String name, boolean includeNames, boolean ignoreNulls, boolean canBeSelf, boolean pojo) {
         final BlockStatement thenBlock = new BlockStatement();
         final Statement appendValue = ignoreNulls ? ifS(notNullX(value), thenBlock) : thenBlock;
         appendCommaIfNotFirst(thenBlock, result, first);
         appendPrefix(thenBlock, result, name, includeNames);
+        Expression toString = pojo
+                ? callX(value, "toString")
+                : callX(INVOKER_TYPE, "toString", value);
         if (canBeSelf) {
             thenBlock.addStatement(ifElseS(
                     sameX(value, new VariableExpression("this")),
                     appendS(result, constX("(this)")),
-                    appendS(result, callX(INVOKER_TYPE, "toString", value))));
+                    appendS(result, toString)));
         } else {
-            thenBlock.addStatement(appendS(result, callX(INVOKER_TYPE, "toString", value)));
+            thenBlock.addStatement(appendS(result, toString));
 
         }
         body.addStatement(appendValue);