You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by pa...@apache.org on 2015/05/18 13:24:59 UTC

incubator-groovy git commit: GROOVY-7427: TupleConstructor should allow a mechanism to switch off automatic parameter defaults

Repository: incubator-groovy
Updated Branches:
  refs/heads/master 6d81d3e2f -> 02f918286


GROOVY-7427: TupleConstructor should allow a mechanism to switch off automatic parameter defaults


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

Branch: refs/heads/master
Commit: 02f918286016051cacf11a91c841960abfa56cbb
Parents: 6d81d3e
Author: Paul King <pa...@asert.com.au>
Authored: Mon May 18 21:24:45 2015 +1000
Committer: Paul King <pa...@asert.com.au>
Committed: Mon May 18 21:24:45 2015 +1000

----------------------------------------------------------------------
 src/main/groovy/transform/TupleConstructor.java | 19 +++++++++++
 .../TupleConstructorASTTransformation.java      | 31 +++++++++++------
 src/spec/doc/core-metaprogramming.adoc          | 36 +++++++++++++++-----
 .../test/CodeGenerationASTTransformsTest.groovy | 29 ++++++++++++++++
 .../CanonicalComponentsTransformTest.groovy     | 16 +++++++++
 5 files changed, 113 insertions(+), 18 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/02f91828/src/main/groovy/transform/TupleConstructor.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/TupleConstructor.java b/src/main/groovy/transform/TupleConstructor.java
index bc084c7..b809792 100644
--- a/src/main/groovy/transform/TupleConstructor.java
+++ b/src/main/groovy/transform/TupleConstructor.java
@@ -116,6 +116,25 @@ public @interface TupleConstructor {
     boolean force() default false;
 
     /**
+     * Used to set whether default value processing is enabled (the default) or disabled.
+     *
+     * By default, every constructor parameter is given a default value. This value will
+     * be Java's default for primitive types (zero or false) and null for Objects, unless
+     * an initial value is given when declaring the property or field. A consequence of
+     * this design is that you can leave off parameters from the right if the default
+     * value will suffice. As far as Java interoperability is concerned, Groovy will
+     * create additional constructors under the covers representing the constructors
+     * with parameters left off, all the way from the constructor with all arguments
+     * to the no-arg constructor.
+     *
+     * However, when set to false, default values are not allowed for properties and fields.
+     * Only the constructor containing all arguments will be provided.
+     * In particular, a no-arg constructor won't be provided and since this is currently
+     * used by Groovy when using named-arguments, the named-argument style won't be available.
+     */
+    boolean defaults() default true;
+
+    /**
      * By default, properties are set directly using their respective field.
      * By setting {@code useSetters=true} then a writable property will be set using its setter.
      * If turning on this flag we recommend that setters that might be called are

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/02f91828/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 23cc2a8..c94d361 100644
--- a/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -67,8 +67,6 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 
 /**
  * Handles generation of code for the @TupleConstructor annotation.
- *
- * @author Paul King
  */
 @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
 public class TupleConstructorASTTransformation extends AbstractASTTransformation {
@@ -110,6 +108,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             boolean includeSuperProperties = memberHasValue(anno, "includeSuperProperties", true);
             boolean callSuper = memberHasValue(anno, "callSuper", true);
             boolean force = memberHasValue(anno, "force", true);
+            boolean defaults = !memberHasValue(anno, "defaults", false);
             boolean useSetters = memberHasValue(anno, "useSetters", true);
             List<String> excludes = getMemberList(anno, "excludes");
             List<String> includes = getMemberList(anno, "includes");
@@ -121,11 +120,15 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) 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(cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force, excludes, includes, useSetters);
+            createConstructor(this, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force, excludes, includes, useSetters, defaults);
         }
     }
 
     public static void createConstructor(ClassNode cNode, boolean includeFields, boolean includeProperties, boolean includeSuperFields, boolean includeSuperProperties, boolean callSuper, boolean force, List<String> excludes, List<String> includes, boolean useSetters) {
+        createConstructor(null, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, callSuper, force, excludes, includes, useSetters, true);
+    }
+
+    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) {
         // no processing if existing constructors found
         List<ConstructorNode> constructors = cNode.getDeclaredConstructors();
         if (constructors.size() > 1 && !force) return;
@@ -156,7 +159,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         for (FieldNode fNode : superList) {
             String name = fNode.getName();
             if (shouldSkip(name, excludes, includes)) continue;
-            params.add(createParam(fNode, name));
+            params.add(createParam(fNode, name, defaults, xform));
             boolean hasSetter = cNode.getProperty(name) != null && !fNode.isFinal();
             if (callSuper) {
                 superParams.add(varX(name));
@@ -174,7 +177,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         for (FieldNode fNode : list) {
             String name = fNode.getName();
             if (shouldSkip(name, excludes, includes)) continue;
-            Parameter nextParam = createParam(fNode, name);
+            Parameter nextParam = createParam(fNode, name, defaults, xform);
             params.add(nextParam);
             boolean hasSetter = cNode.getProperty(name) != null && !fNode.isFinal();
             if (useSetters && hasSetter) {
@@ -186,16 +189,17 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body));
         // 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.size() > 0) {
+        if (params.size() > 0 && defaults) {
             ClassNode firstParam = params.get(0).getType();
             if (params.size() > 1 || firstParam.equals(ClassHelper.OBJECT_TYPE)) {
+                String message = "The class " + cNode.getName() + " was incorrectly initialized via the map constructor with null.";
                 if (firstParam.equals(ClassHelper.MAP_TYPE)) {
-                    addMapConstructors(cNode, true, "The class " + cNode.getName() + " was incorrectly initialized via the map constructor with null.");
+                    addMapConstructors(cNode, true, message);
                 } else {
                     ClassNode candidate = HMAP_TYPE;
                     while (candidate != null) {
                         if (candidate.equals(firstParam)) {
-                            addMapConstructors(cNode, true, "The class " + cNode.getName() + " was incorrectly initialized via the map constructor with null.");
+                            addMapConstructors(cNode, true, message);
                             break;
                         }
                         candidate = candidate.getSuperClass();
@@ -209,9 +213,16 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
         return "set" + Verifier.capitalize(name);
     }
 
-    private static Parameter createParam(FieldNode fNode, String name) {
+    private static Parameter createParam(FieldNode fNode, String name, boolean defaults, AbstractASTTransformation xform) {
         Parameter param = new Parameter(fNode.getType(), name);
-        param.setInitialExpression(providedOrDefaultInitialValue(fNode));
+        if (defaults){
+            param.setInitialExpression(providedOrDefaultInitialValue(fNode));
+        } else {
+            if (fNode.getInitialExpression() != null) {
+                xform.addError("Error during " + MY_TYPE_NAME + " processing, default value processing disabled but default value found for '" + fNode.getName() + "'", fNode);
+            }
+
+        }
         return param;
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/02f91828/src/spec/doc/core-metaprogramming.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc
index 4ad3878..d05fca8 100644
--- a/src/spec/doc/core-metaprogramming.adoc
+++ b/src/spec/doc/core-metaprogramming.adoc
@@ -854,7 +854,11 @@ include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=
 ===== @groovy.transform.TupleConstructor
 
 The `@TupleConstructor` annotation aims at eliminating boilerplate code by generating constructors for you. A tuple
-constructor is created for each property, with default values (using the Java default values). For example, the
+constructor is created having a parameter for each property (and possibly fields). Each parameter has a default value
+(using the initial value of the property if present or otherwise Java's default value according to the properties type).
+In later compilation phases, the Groovy compiler's standard default value processing behavior is then applied.
+The end result is that multiple constructors are placed within the bytecode of your class. This provides a
+well understood semantics and is also useful for Java integration purposes. As an example, the
 following code will generate 3 constructors:
 
 [source,groovy]
@@ -862,12 +866,22 @@ following code will generate 3 constructors:
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_simple,indent=0]
 ----
 
-The first constructor is a no-arg constructor which allows the traditional map-style construction. It is worth noting
-that if the first property (or field) has type LinkedHashMap or if there is a single Map, AbstractMap or HashMap
-property (or field), then the map-style mapping is not available.
+The first constructor is a no-arg constructor which allows the traditional map-style construction so long as
+you don't have final properties. Groovy calls the no-arg constructor and then the relevant setters under the covers.
+It is worth noting that if the first property (or field) has type LinkedHashMap or if there is a single Map,
+AbstractMap or HashMap property (or field), then the map-style named arguments won't be available.
 
 The other constructors are generated by taking the properties in the order they are defined. Groovy will generate as
-many constructors as they are properties (or fields, depending on the options).
+many constructors as there are properties (or fields, depending on the options).
+
+Setting the `defaults` attribute (see the available configuration options table) to `false`, disables the normal default values behavior which means:
+
+* Exactly one constructor will be produced
+* Attempting to use an initial value will produce an error
+* Map-style named arguments won't be available
+
+This attribute is normally only used in situations where another Java framework is
+expecting exactly one constructor, e.g. injection frameworks, JUnit parameterized runners.
 
 The `@TupleConstructor` AST transformation accepts several configuration options:
 
@@ -909,20 +923,26 @@ include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=
 ----
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_callSuper,indent=0]
 ----
-|force|False|By default, the transformation will do nothing if a constructor is already defined. Setting this property to
+|force|False|By default, the transformation will do nothing if a constructor is already defined. Setting this attribute to
 true, the constructor will be generated and it's your responsibility to ensure that no duplicate constructor is defined.|
 [source,groovy]
 ----
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_force,indent=0]
 ----
-|useSetters|False|By default, the transformation will include code which directly sets the backing field for each property
-from its corresponding constructor parameter. Setting this property to true, the constructor will instead call setters if
+|useSetters|False|By default, the transformation will directly set the backing field of each property
+from its corresponding constructor parameter. Setting this attribute to true, the constructor will instead call setters if
 they exist. It's usually deemed bad style from within a constructor to call setters that can be overridden. It's your
 responsibility to avoid such bad style.|
 [source,groovy]
 ----
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_useSetters,indent=0]
 ----
+|defaults|True|Indicates that default value processing is enabled for constructor parameters.
+Set to false to obtain exactly one constructor but with initial value support and named-arguments disabled. |
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=tupleconstructor_example_defaults_false,indent=0]
+----
 |=======================================================================
 
 [[xform-Canonical]]

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/02f91828/src/spec/test/CodeGenerationASTTransformsTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/CodeGenerationASTTransformsTest.groovy b/src/spec/test/CodeGenerationASTTransformsTest.groovy
index 9a75078..5af1bb3 100644
--- a/src/spec/test/CodeGenerationASTTransformsTest.groovy
+++ b/src/spec/test/CodeGenerationASTTransformsTest.groovy
@@ -550,6 +550,35 @@ assert new Foo('cat').toString() == 'Foo(CAT)'
 assert new Foo(bar: 'cat').toString() == 'Foo(CAT)'
 // end::tupleconstructor_example_useSetters[]
 '''
+
+        assertScript '''
+// default/control
+import groovy.transform.*
+
+@ToString
+@TupleConstructor
+class Athlete {
+  String name
+  String sport
+  int age
+}
+
+assert new Athlete('Roger', 'Tennis', 33).toString() == 'Athlete(Roger, Tennis, 33)'
+assert Athlete.constructors.size() == 4
+
+// tag::tupleconstructor_example_defaults_false[]
+@ToString
+@TupleConstructor(defaults=false)
+class Musician {
+  String name
+  String instrument
+  int born
+}
+
+assert new Musician('Jimi', 'Guitar', 1942).toString() == 'Musician(Jimi, Guitar, 1942)'
+assert Musician.constructors.size() == 1
+// end::tupleconstructor_example_defaults_false[]
+'''
     }
 
     void testCanonical() {

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/02f91828/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy b/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy
index 6e6e1b7..2e644d9 100644
--- a/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy
@@ -547,6 +547,22 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase {
         """
     }
 
+    void testTupleConstructorNoDefaultParameterValues_GROOVY7427() {
+        new GroovyShell().evaluate """
+            // checks special Map behavior isn't added if defaults=false
+            import groovy.transform.*
+
+            @ToString
+            @TupleConstructor(defaults=false)
+            class Person {
+              def name
+            }
+
+            assert new Person('John Smith').toString() == 'Person(John Smith)'
+            assert Person.constructors.size() == 1
+        """
+    }
+
     void testNullCloneableField_GROOVY7091() {
         new GroovyShell().evaluate """
             import groovy.transform.AutoClone