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 2016/05/20 10:28:34 UTC

groovy git commit: GROOVY-7837: TupleConstructor should provide pre and post annotation attributes like MapConstructor

Repository: groovy
Updated Branches:
  refs/heads/master 872923f8e -> 7da8b58ca


GROOVY-7837: TupleConstructor should provide pre and post annotation attributes like MapConstructor


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/7da8b58c
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/7da8b58c
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/7da8b58c

Branch: refs/heads/master
Commit: 7da8b58cab2ab070abdd7a8ed7cf6ce4bc989db2
Parents: 872923f
Author: paulk <pa...@asert.com.au>
Authored: Wed May 11 22:04:45 2016 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Fri May 20 20:27:51 2016 +1000

----------------------------------------------------------------------
 src/main/groovy/transform/TupleConstructor.java | 36 +++++++---
 .../codehaus/groovy/ast/tools/GeneralUtils.java | 31 ++++++++
 .../MapConstructorASTTransformation.java        | 74 ++++++--------------
 .../TupleConstructorASTTransformation.java      | 61 ++++++++++++++--
 src/spec/doc/core-metaprogramming.adoc          | 10 +++
 .../test/CodeGenerationASTTransformsTest.groovy | 32 +++++++++
 .../TupleConstructorTransformTest.groovy        | 37 ++++++++++
 7 files changed, 212 insertions(+), 69 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/main/groovy/transform/TupleConstructor.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/TupleConstructor.java b/src/main/groovy/transform/TupleConstructor.java
index 9ea30df..1a3cca5 100644
--- a/src/main/groovy/transform/TupleConstructor.java
+++ b/src/main/groovy/transform/TupleConstructor.java
@@ -213,8 +213,9 @@ public @interface TupleConstructor {
     boolean includeSuperProperties() default false;
 
     /**
-     * Should super properties be called within a call to the parent constructor.
-     * rather than set as properties
+     * Should super properties be called within a call to the parent constructor
+     * rather than set as properties. Typically used in combination with {@code includeSuperProperties}.
+     * Can't be true if using {@code pre} with a {@code super} first statement.
      */
     boolean callSuper() default false;
 
@@ -251,12 +252,27 @@ public @interface TupleConstructor {
      * made null-safe wrt the parameter.
      */
     boolean useSetters() default false;
-
-    /**
-     * Whether to include all fields and/or properties within the constructor, including those with names that are
-     * considered internal.
-     *
-     * @since 2.5.0
-     */
-    boolean allNames() default false;
+
+    /**
+     * Whether to include all fields and/or properties within the constructor, including those with names that are
+     * considered internal.
+     *
+     * @since 2.5.0
+     */
+    boolean allNames() default false;
+
+    /**
+     * A Closure containing statements which will be prepended to the generated constructor. The first statement
+     * within the Closure may be {@code super(someArgs)} in which case the no-arg super constructor won't be called.
+     *
+     * @since 2.5.0
+     */
+    Class pre();
+
+    /**
+     * A Closure containing statements which will be appended to the end of the generated constructor. Useful for validation steps or tweaking the populated fields/properties.
+     *
+     * @since 2.5.0
+     */
+    Class post();
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/main/org/codehaus/groovy/ast/tools/GeneralUtils.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 7fe279b..2d59447 100644
--- a/src/main/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -736,4 +736,35 @@ public class GeneralUtils {
 
         return source;
     }
+
+    public static boolean copyStatementsWithSuperAdjustment(ClosureExpression pre, BlockStatement body) {
+        Statement preCode = pre.getCode();
+        boolean changed = false;
+        if (preCode instanceof BlockStatement) {
+            BlockStatement block = (BlockStatement) preCode;
+            List<Statement> statements = block.getStatements();
+            for (int i = 0; i < statements.size(); i++) {
+                Statement statement = statements.get(i);
+                // adjust the first statement if it's a super call
+                if (i == 0 && statement instanceof ExpressionStatement) {
+                    ExpressionStatement es = (ExpressionStatement) statement;
+                    Expression preExp = es.getExpression();
+                    if (preExp instanceof MethodCallExpression) {
+                        MethodCallExpression mce = (MethodCallExpression) preExp;
+                        String name = mce.getMethodAsString();
+                        if ("super".equals(name)) {
+                            es.setExpression(new ConstructorCallExpression(ClassNode.SUPER, mce.getArguments()));
+                            changed = true;
+                        }
+                    }
+                }
+                body.addStatement(statement);
+            }
+        }
+        return changed;
+    }
+
+    public static String getSetterName(String name) {
+        return "set" + Verifier.capitalize(name);
+    }
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
index ec7865c..69c85f2 100644
--- a/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/MapConstructorASTTransformation.java
@@ -30,15 +30,10 @@ import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.ClosureExpression;
-import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.Expression;
-import org.codehaus.groovy.ast.expr.MethodCallExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.BlockStatement;
 import org.codehaus.groovy.ast.stmt.EmptyStatement;
-import org.codehaus.groovy.ast.stmt.ExpressionStatement;
-import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.classgen.Verifier;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.SourceUnit;
 
@@ -53,9 +48,11 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS;
 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.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.copyStatementsWithSuperAdjustment;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getSuperPropertyFields;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
@@ -139,10 +136,10 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation {
 
         Parameter map = param(MAP_TYPE, "args");
         final BlockStatement body = new BlockStatement();
-        ClassCodeExpressionTransformer transformer = makeTransformer();
+        ClassCodeExpressionTransformer transformer = makeMapTypedArgsTransformer();
         if (pre != null) {
             ClosureExpression transformed = (ClosureExpression) transformer.transform(pre);
-            copyPreStatements(transformed, body);
+            copyStatementsWithSuperAdjustment(transformed, body);
         }
         for (FieldNode fNode : superList) {
             String name = fNode.getName();
@@ -168,56 +165,29 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation {
                 assignS(propX(varX("this"), name), callX(varX(map), "get", nameArg))));
     }
 
-    private static String getSetterName(String name) {
-        return "set" + Verifier.capitalize(name);
-    }
-
-    private static ClassCodeExpressionTransformer makeTransformer() {
+    private static ClassCodeExpressionTransformer makeMapTypedArgsTransformer() {
         return new ClassCodeExpressionTransformer() {
-                @Override
-                public Expression transform(Expression exp) {
-                    if (exp instanceof ClosureExpression) {
-                        ClosureExpression ce = (ClosureExpression) exp;
-                        ce.getCode().visit(this);
-                    } else if (exp instanceof VariableExpression) {
-                        VariableExpression ve = (VariableExpression) exp;
-                        if (ve.getName().equals("args") && ve.getAccessedVariable() instanceof DynamicVariable) {
-                            VariableExpression newVe = new VariableExpression(new Parameter(MAP_TYPE, "args"));
-                            newVe.setSourcePosition(ve);
-                            return newVe;
-                        }
+            @Override
+            public Expression transform(Expression exp) {
+                if (exp instanceof ClosureExpression) {
+                    ClosureExpression ce = (ClosureExpression) exp;
+                    ce.getCode().visit(this);
+                } else if (exp instanceof VariableExpression) {
+                    VariableExpression ve = (VariableExpression) exp;
+                    if (ve.getName().equals("args") && ve.getAccessedVariable() instanceof DynamicVariable) {
+                        VariableExpression newVe = new VariableExpression(new Parameter(MAP_TYPE, "args"));
+                        newVe.setSourcePosition(ve);
+                        return newVe;
                     }
-                    return exp.transformExpression(this);
                 }
+                return exp.transformExpression(this);
+            }
 
-                @Override
-                protected SourceUnit getSourceUnit() {
-                    return null;
-                }
-            };
-    }
-
-    private static void copyPreStatements(ClosureExpression pre, BlockStatement body) {
-        Statement preCode = pre.getCode();
-        if (preCode instanceof BlockStatement) {
-            BlockStatement block = (BlockStatement) preCode;
-            List<Statement> statements = block.getStatements();
-            for (int i = 0; i < statements.size(); i++) {
-                Statement statement = statements.get(i);
-                if (i == 0 && statement instanceof ExpressionStatement) {
-                    ExpressionStatement es = (ExpressionStatement) statement;
-                    Expression preExp = es.getExpression();
-                    if (preExp instanceof MethodCallExpression) {
-                        MethodCallExpression mce = (MethodCallExpression) preExp;
-                        String name = mce.getMethodAsString();
-                        if ("super".equals(name)) {
-                            es.setExpression(new ConstructorCallExpression(ClassNode.SUPER, mce.getArguments()));
-                        }
-                    }
-                }
-                body.addStatement(statement);
+            @Override
+            protected SourceUnit getSourceUnit() {
+                return null;
             }
-        }
+        };
     }
 
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index b31cc8e..f2d0031 100644
--- a/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -28,12 +28,14 @@ import org.codehaus.groovy.ast.ConstructorNode;
 import org.codehaus.groovy.ast.FieldNode;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
 import org.codehaus.groovy.ast.expr.ConstantExpression;
 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.EmptyStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
-import org.codehaus.groovy.classgen.Verifier;
+import org.codehaus.groovy.classgen.VariableScopeVisitor;
 import org.codehaus.groovy.control.CompilePhase;
 import org.codehaus.groovy.control.SourceUnit;
 
@@ -51,10 +53,12 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.copyStatementsWithSuperAdjustment;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.equalsNullX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstanceNonPropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFields;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getSuperNonPropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.getSuperPropertyFields;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS;
@@ -118,7 +122,25 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields)) return;
             // if @Immutable is found, let it pick up options and do work so we'll skip
             if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) return;
-            createConstructor(this, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force, excludes, includes, useSetters, defaults, allNames);
+            Expression pre = anno.getMember("pre");
+            if (pre != null && !(pre instanceof ClosureExpression)) {
+                addError("Expected closure value for annotation parameter 'pre'. Found " + pre, cNode);
+                return;
+            }
+            Expression post = anno.getMember("post");
+            if (post != null && !(post instanceof ClosureExpression)) {
+                addError("Expected closure value for annotation parameter 'post'. Found " + post, cNode);
+                return;
+            }
+            createConstructor(this, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties,
+                    callSuper, force, excludes, includes, useSetters, defaults, allNames, sourceUnit,
+                    (ClosureExpression) pre, (ClosureExpression) post);
+            if (pre != null) {
+                anno.setMember("pre", new ClosureExpression(new Parameter[0], new EmptyStatement()));
+            }
+            if (post != null) {
+                anno.setMember("post", new ClosureExpression(new Parameter[0], new EmptyStatement()));
+            }
         }
     }
 
@@ -131,6 +153,16 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
     }
 
     public static void createConstructor(AbstractASTTransformation xform, ClassNode cNode, boolean includeFields, boolean includeProperties, boolean includeSuperFields, boolean includeSuperProperties, boolean callSuper, boolean force, List<String> excludes, List<String> includes, boolean useSetters, boolean defaults, boolean allNames) {
+        createConstructor(xform, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties,
+                callSuper, force, excludes, includes, useSetters, defaults, false, null, null, null);
+    }
+
+    public static void createConstructor(AbstractASTTransformation xform, ClassNode cNode, boolean includeFields,
+                                         boolean includeProperties, boolean includeSuperFields, boolean
+                                                 includeSuperProperties, boolean callSuper, boolean force,
+                                         List<String> excludes, List<String> includes, boolean useSetters, boolean
+                                                 defaults, boolean allNames, SourceUnit sourceUnit, ClosureExpression
+                                                 pre, ClosureExpression post) {
         // no processing if existing constructors found
         if (!cNode.getDeclaredConstructors().isEmpty() && !force) return;
 
@@ -152,6 +184,15 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
 
         final List<Parameter> params = new ArrayList<Parameter>();
         final List<Expression> superParams = new ArrayList<Expression>();
+        final BlockStatement preBody = new BlockStatement();
+        boolean superInPre = false;
+        if (pre != null) {
+            superInPre = copyStatementsWithSuperAdjustment(pre, preBody);
+            if (superInPre && callSuper) {
+                xform.addError("Error during " + MY_TYPE_NAME + " processing, can't have a super call in 'pre' " +
+                        "closure and also 'callSuper' enabled", cNode);
+            }
+        }
         final BlockStatement body = new BlockStatement();
         for (FieldNode fNode : superList) {
             String name = fNode.getName();
@@ -160,7 +201,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             boolean hasSetter = cNode.getProperty(name) != null && !fNode.isFinal();
             if (callSuper) {
                 superParams.add(varX(name));
-            } else {
+            } else if (!superInPre) {
                 if (useSetters && hasSetter) {
                     body.addStatement(stmt(callThisX(getSetterName(name), varX(name))));
                 } else {
@@ -171,6 +212,9 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         if (callSuper) {
             body.addStatement(stmt(ctorX(ClassNode.SUPER, args(superParams))));
         }
+        if (!preBody.isEmpty()) {
+            body.addStatements(preBody.getStatements());
+        }
         for (FieldNode fNode : list) {
             String name = fNode.getName();
             if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue;
@@ -183,7 +227,14 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
                 body.addStatement(assignS(propX(varX("this"), name), varX(nextParam)));
             }
         }
+        if (post != null) {
+            body.addStatement(post.getCode());
+        }
         cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body));
+        if (sourceUnit != null && !body.isEmpty()) {
+            VariableScopeVisitor scopeVisitor = new VariableScopeVisitor(sourceUnit);
+            scopeVisitor.visitClass(cNode);
+        }
         // add map constructor if needed, don't do it for LinkedHashMap for now (would lead to duplicate signature)
         // or if there is only one Map property (for backwards compatibility)
         if (!params.isEmpty() && defaults) {
@@ -206,10 +257,6 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         }
     }
 
-    private static String getSetterName(String name) {
-        return "set" + Verifier.capitalize(name);
-    }
-
     private static Parameter createParam(FieldNode fNode, String name, boolean defaults, AbstractASTTransformation xform) {
         Parameter param = new Parameter(fNode.getType(), name);
         if (defaults) {

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/spec/doc/core-metaprogramming.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc
index c9ace20..2e0e47b 100644
--- a/src/spec/doc/core-metaprogramming.adoc
+++ b/src/spec/doc/core-metaprogramming.adoc
@@ -980,6 +980,16 @@ include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=
 ----
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_allNames,indent=0]
 ----
+|pre|empty|A closure containing statements to be inserted at the start of the generated constructor(s)|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_pre,indent=0]
+----
+|post|empty|A closure containing statements to be inserted at the end of the generated constructor(s)|
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_post,indent=0]
+----
 |=======================================================================
 
 Setting the `defaults` annotation attribute to `false` and the `force` annotation attribute to `true` allows

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/spec/test/CodeGenerationASTTransformsTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/CodeGenerationASTTransformsTest.groovy b/src/spec/test/CodeGenerationASTTransformsTest.groovy
index 5131f24..0c70516 100644
--- a/src/spec/test/CodeGenerationASTTransformsTest.groovy
+++ b/src/spec/test/CodeGenerationASTTransformsTest.groovy
@@ -721,6 +721,38 @@ def p = new Person('Jack')
 assert p.$firstName == 'Jack'
 // end::tupleconstructor_example_allNames[]
 '''
+
+        assertScript '''
+// tag::tupleconstructor_example_pre[]
+import groovy.transform.TupleConstructor
+
+@TupleConstructor(pre={ first = first?.toLowerCase() })
+class Person {
+    String first
+}
+
+def p = new Person('Jack')
+
+assert p.first == 'jack'
+// end::tupleconstructor_example_pre[]
+'''
+
+        assertScript '''
+// tag::tupleconstructor_example_post[]
+import groovy.transform.TupleConstructor
+import static groovy.test.GroovyAssert.shouldFail
+
+@TupleConstructor(post={ assert first })
+class Person {
+    String first
+}
+
+def jack = new Person('Jack')
+shouldFail {
+  def unknown = new Person()
+}
+// end::tupleconstructor_example_post[]
+'''
     }
 
     void testMapConstructor() {

http://git-wip-us.apache.org/repos/asf/groovy/blob/7da8b58c/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
index e4475ef..af5829b 100644
--- a/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/TupleConstructorTransformTest.groovy
@@ -36,6 +36,43 @@ class TupleConstructorTransformTest extends GroovyShellTestCase {
         """
     }
 
+    void testConstructorWithPostAndFields() {
+        assertScript '''
+            import groovy.transform.*
+
+            @ToString(includeFields=true, includeNames=true)
+            @TupleConstructor(post={ full = "$first $last" })
+            class Person {
+                final String first, last
+                private final String full
+            }
+
+            assert new Person('Dierk', 'Koenig').toString() ==
+                'Person(first:Dierk, last:Koenig, full:Dierk Koenig)'
+        '''
+    }
+
+    void testConstructorWithPreAndPost() {
+        assertScript '''
+            import groovy.transform.*
+
+            @TupleConstructor
+            class Person {
+                String first, last
+            }
+
+            @CompileStatic // optional
+            @ToString(includeSuperProperties=true)
+            @TupleConstructor(includeSuperProperties=true, pre={ super(first, last?.toLowerCase()) }, post = { this.first = this.first?.toUpperCase() })
+            class Author extends Person {
+                String bookName
+            }
+
+            assert new Author('Dierk', 'Koenig', 'ReGinA').toString() == 'Author(ReGinA, DIERK, koenig)'
+            assert new Author().toString() == 'Author(null, null, null)'
+        '''
+    }
+
     void testExistingEmptyConstructorTakesPrecedence_groovy7522() {
         assertScript """
             @groovy.transform.TupleConstructor