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/22 09:04:46 UTC

incubator-groovy git commit: GROOVY-6319: Canonical annotation should allow property names in toString (Closes #21)

Repository: incubator-groovy
Updated Branches:
  refs/heads/master f568e2392 -> 3844f2835


GROOVY-6319: Canonical annotation should allow property names in toString (Closes #21)


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

Branch: refs/heads/master
Commit: 3844f28357a73fbdd178ebc1193475cbc1ff1437
Parents: f568e23
Author: Paul King <pa...@asert.com.au>
Authored: Wed May 20 21:52:07 2015 +1000
Committer: Paul King <pa...@asert.com.au>
Committed: Fri May 22 17:03:49 2015 +1000

----------------------------------------------------------------------
 src/main/groovy/transform/Canonical.groovy      |  86 ++++++++++++
 src/main/groovy/transform/Canonical.java        | 132 -------------------
 .../transform/BuilderASTTransformation.java     |  15 +--
 .../transform/CanonicalASTTransformation.java   |  79 -----------
 .../EqualsAndHashCodeASTTransformation.java     |   5 -
 .../transform/ToStringASTTransformation.java    |   5 -
 .../TupleConstructorASTTransformation.java      |   5 -
 src/spec/doc/core-metaprogramming.adoc          |  20 +--
 .../test/CodeGenerationASTTransformsTest.groovy |  29 ++--
 .../transform/CanonicalTransformTest.groovy     |  38 ++++--
 .../transform/ImmutableTransformTest.groovy     |  14 --
 11 files changed, 143 insertions(+), 285 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/groovy/transform/Canonical.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/Canonical.groovy b/src/main/groovy/transform/Canonical.groovy
new file mode 100644
index 0000000..637ddd5
--- /dev/null
+++ b/src/main/groovy/transform/Canonical.groovy
@@ -0,0 +1,86 @@
+/*
+ *  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
+/**
+ * The {@code @Canonical} meta-annotation combines the {@code @EqualsAndHashCode},
+ * {@code @ToString} and {@code @TupleConstructor} annotations. It is used to assist in
+ * the creation of mutable classes. It instructs the compiler to execute AST transformations
+ * which add positional constructors, equals, hashCode and a pretty print toString to your class.
+ * <p>
+ * You can write classes in this shortened form:
+ * <pre>
+ * {@code @Canonical} class Customer {
+ *     String first, last
+ *     int age
+ *     Date since
+ *     Collection favItems = ['Food']
+ *     def object 
+ * }
+ * def d = new Date()
+ * def anyObject = new Object()
+ * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games'], object: anyObject)
+ * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games'], anyObject)
+ * assert c1 == c2
+ * </pre>
+ *
+ * You don't need to provide all arguments in constructor calls. If using named parameters, any property names not
+ * referenced will be given their default value (as per Java's default unless an explicit initialization constant is
+ * provided when defining the property). If using a tuple constructor, parameters are supplied in the order in which
+ * the properties are defined. Supplied parameters fill the tuple from the left. Any parameters missing on the right
+ * are given their default value.
+ * <pre>
+ * def c3 = new Customer(last: 'Jones', age: 21)
+ * def c4 = new Customer('Tom', 'Jones')
+ *
+ * assert null == c3.since
+ * assert 0 == c4.age
+ * assert c3.favItems == ['Food'] && c4.favItems == ['Food']
+ * </pre>
+ *
+ * If you don't need all of the functionality of {@code @Canonical}, you can simply directly use one or more of the individual
+ * annotations which {@code @Canonical} aggregates.
+ * In addition, you can use {@code @Canonical} in combination with explicit use one or more of the individual annotations in
+ * cases where you need to further customize the annotation attributes.
+ * Any applicable annotation attributes from {@code @Canonical} missing from the explicit annotation will be merged
+ * but any existing annotation attributes within the explicit annotation take precedence. So, for example in this case here:
+ * <pre>
+ * {@code @Canonical}(includeNames=true, excludes='c')
+ * {@code @}{@link ToString}(excludes='a,b')
+ * class MyClass { ... }
+ * </pre>
+ * The generated {@code toString} will include property names and exclude the {@code a} and {@code b} properties.
+ * <p>
+ * A class created using {@code @Canonical} has the following characteristics:
+ * <ul>
+ * <li>A no-arg constructor is provided which allows you to set properties by name using Groovy's normal bean conventions.
+ * <li>Tuple-style constructors are provided which allow 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.
+ * See the GroovyDoc for the individual annotations for more details.
+ * <p>
+ * If you want similar functionality to what this annotation provides but also require immutability, see the
+ * {@code @}{@link Immutable} annotation.
+ *
+ * @see groovy.transform.EqualsAndHashCode
+ * @see groovy.transform.ToString
+ * @see groovy.transform.TupleConstructor
+ * @see groovy.transform.Immutable
+ * @since 1.8.0
+ */
+@AnnotationCollector(value=[ToString, TupleConstructor, EqualsAndHashCode], mode=AnnotationCollectorMode.PREFER_EXPLICIT_MERGED)
+public @interface Canonical { }

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/groovy/transform/Canonical.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/transform/Canonical.java b/src/main/groovy/transform/Canonical.java
deleted file mode 100644
index 4ab5f83..0000000
--- a/src/main/groovy/transform/Canonical.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- *  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 mutable classes.
- * <p>
- * It allows you to write classes in this shortened form:
- * <pre>
- * {@code @Canonical} class Customer {
- *     String first, last
- *     int age
- *     Date since
- *     Collection favItems = ['Food']
- *     def object 
- * }
- * def d = new Date()
- * def anyObject = new Object()
- * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games'], object: anyObject)
- * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games'], anyObject)
- * assert c1 == c2
- * </pre>
- *
- * You don't need to provide all arguments in constructor calls. If using named parameters, any property names not
- * referenced will be given their default value (as per Java's default unless an explicit initialization constant is
- * provided when defining the property). If using a tuple constructor, parameters are supplied in the order in which
- * the properties are defined. Supplied parameters fill the tuple from the left. Any parameters missing on the right
- * are given their default value.
- * <pre>
- * def c3 = new Customer(last: 'Jones', age: 21)
- * def c4 = new Customer('Tom', 'Jones')
- *
- * assert null == c3.since
- * assert 0 == c4.age
- * assert c3.favItems == ['Food'] && c4.favItems == ['Food']
- * </pre>
- *
- * The {@code @Canonical} annotation instructs the compiler to execute an
- * AST transformation which adds positional constructors,
- * equals, hashCode and a pretty print toString to your class. There are additional
- * annotations if you only need some of the functionality: {@code @EqualsAndHashCode},
- * {@code @ToString} and {@code @TupleConstructor}. In addition, you can add one of
- * the other annotations if you need to further customize the behavior of the
- * AST transformation.
- * <p>
- * A class created in this way has the following characteristics:
- * <ul>
- * <li>A no-arg constructor is provided which allows you to set properties by name using Groovy's normal bean conventions.
- * <li>Tuple-style constructors are provided which allow 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.
- * Though not normally required, you may write your own implementations of these methods. For {@code equals} and {@code hashCode},
- * if you do write your own method, it is up to you to obey the general contract for {@code equals} methods and supply
- * a corresponding matching {@code hashCode} method.
- * If you do provide one of these methods explicitly, the default implementation will be made available in a private
- * "underscore" variant which you can call. E.g., you could provide a (not very elegant) multi-line formatted
- * {@code toString} method for {@code Customer} above as follows:
- * <pre>
- *     String toString() {
- *        _toString().replaceAll(/\(/, '(\n\t').replaceAll(/\)/, '\n)').replaceAll(/, /, '\n\t')
- *    }
- * </pre>
- * If an "underscore" version of the respective method already exists, then no default implementation is provided.
- * </ul>
- * <p>
- * If you want similar functionality to what this annotation provides but also require immutability, see the
- * {@code @}{@link Immutable} annotation.
- * <p>
- * Limitations:
- * <ul>
- * <li>If you explicitly add your own constructors, then the transformation will not add any other constructor to the class</li>
- * <li>Groovy's normal map-style naming conventions will not be available if the first property
- * has type {@code LinkedHashMap} or if there is a single Map, AbstractMap or HashMap property</li>
- * </ul>
- *
- * @author Paulo Poiati
- * @author Paul King
- * @see groovy.transform.EqualsAndHashCode
- * @see groovy.transform.ToString
- * @see groovy.transform.TupleConstructor
- * @see groovy.transform.Immutable
- * @since 1.8.0
- */
-@java.lang.annotation.Documented
-@Retention(RetentionPolicy.SOURCE)
-@Target({ElementType.TYPE})
-@GroovyASTTransformationClass("org.codehaus.groovy.transform.CanonicalASTTransformation")
-public @interface Canonical {
-    /**
-     * List of field and/or property names to exclude.
-     * Must not be used if 'includes' is used. For convenience, a String with comma separated names
-     * can be used in addition to an array (using Groovy's literal list notation) of String values.
-     *
-     * If the {@code @Canonical} behavior is customised by using it in conjunction with one of the more specific
-     * related annotations (i.e. {@code @ToString}, {@code @EqualsAndHashCode} or {@code @TupleConstructor}), then
-     * the value of this attribute can be overridden within the more specific annotation.
-     */
-    String[] excludes() default {};
-
-    /**
-     * List of field and/or property names to include.
-     * Must not be used if 'excludes' is used. For convenience, a String with comma separated names
-     * can be used in addition to an array (using Groovy's literal list notation) of String values.
-     *
-     * If the {@code @Canonical} behavior is customised by using it in conjunction with one of the more specific
-     * related annotations (i.e. {@code @ToString}, {@code @EqualsAndHashCode} or {@code @TupleConstructor}), then
-     * the value of this attribute can be overridden within the more specific annotation.
-     */
-    String[] includes() default {};
-}

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/org/codehaus/groovy/transform/BuilderASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/BuilderASTTransformation.java b/src/main/org/codehaus/groovy/transform/BuilderASTTransformation.java
index d3c5eaf..7481b91 100644
--- a/src/main/org/codehaus/groovy/transform/BuilderASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/BuilderASTTransformation.java
@@ -43,9 +43,6 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.getInstancePropertyFiel
 
 /**
  * Handles generation of code for the {@link Builder} annotation.
- *
- * @author Marcin Grzejszczak
- * @author Paul King
  */
 @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
 public class BuilderASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
@@ -137,15 +134,15 @@ public class BuilderASTTransformation extends AbstractASTTransformation implemen
             List<String> directIncludes = transform.getMemberList(anno, "includes");
             if (directIncludes != null) includes.addAll(directIncludes);
             if (includes.isEmpty() && excludes.isEmpty()) {
-                if (transform.hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
-                    AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
+                if (transform.hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
+                    AnnotationNode tupleConstructor = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0);
                     if (excludes.isEmpty()) {
-                        List<String>  canonicalExcludes = transform.getMemberList(canonical, "excludes");
-                        if (canonicalExcludes != null) excludes.addAll(canonicalExcludes);
+                        List<String>  tupleExcludes = transform.getMemberList(tupleConstructor, "excludes");
+                        if (tupleExcludes != null) excludes.addAll(tupleExcludes);
                     }
                     if (includes.isEmpty()) {
-                        List<String>  canonicalIncludes = transform.getMemberList(canonical, "includes");
-                        if (canonicalIncludes != null) includes.addAll(canonicalIncludes);
+                        List<String>  tupleIncludes = transform.getMemberList(tupleConstructor, "includes");
+                        if (tupleIncludes != null) includes.addAll(tupleIncludes);
                     }
                 }
             }

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java b/src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java
deleted file mode 100644
index c729294..0000000
--- a/src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- *  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.transform.Canonical;
-import org.codehaus.groovy.ast.ASTNode;
-import org.codehaus.groovy.ast.AnnotatedNode;
-import org.codehaus.groovy.ast.AnnotationNode;
-import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.control.CompilePhase;
-import org.codehaus.groovy.control.SourceUnit;
-
-import java.util.List;
-
-import static org.codehaus.groovy.ast.ClassHelper.make;
-import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createEquals;
-import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createHashCode;
-import static org.codehaus.groovy.transform.ToStringASTTransformation.createToString;
-import static org.codehaus.groovy.transform.TupleConstructorASTTransformation.createConstructor;
-
-/**
- * Handles generation of code for the @Canonical annotation.
- *
- * @author Paulo Poiati
- * @author Paul King
- */
-@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
-public class CanonicalASTTransformation extends AbstractASTTransformation {
-
-    static final Class MY_CLASS = Canonical.class;
-    static final ClassNode MY_TYPE = make(MY_CLASS);
-    static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();
-
-    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) {
-            ClassNode cNode = (ClassNode) parent;
-            // TODO remove - let other validation steps pick this up
-            if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) {
-                addError(MY_TYPE_NAME + " class '" + cNode.getName() + "' can't also be " + ImmutableASTTransformation.MY_TYPE_NAME, parent);
-            }
-            if (!checkNotInterface(cNode, MY_TYPE_NAME)) return;
-            List<String> excludes = getMemberList(anno, "excludes");
-            List<String> includes = getMemberList(anno, "includes");
-            if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) return;
-            if (!hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
-                createConstructor(cNode, false, true, false, false, false, false, excludes, includes, false);
-            }
-            if (!hasAnnotation(cNode, EqualsAndHashCodeASTTransformation.MY_TYPE)) {
-                createHashCode(cNode, false, false, false, excludes, includes);
-                createEquals(cNode, false, false, true, excludes, includes);
-            }
-            if (!hasAnnotation(cNode, ToStringASTTransformation.MY_TYPE)) {
-                createToString(cNode, false, false, excludes, includes, false);
-            }
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java b/src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
index f1d0efb..d1a46b5 100644
--- a/src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java
@@ -73,11 +73,6 @@ public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformatio
             boolean includeFields = memberHasValue(anno, "includeFields", true);
             List<String> excludes = getMemberList(anno, "excludes");
             List<String> includes = getMemberList(anno, "includes");
-            if (hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
-                AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
-                if (excludes == null || excludes.isEmpty()) excludes = getMemberList(canonical, "excludes");
-                if (includes == null || includes.isEmpty()) includes = getMemberList(canonical, "includes");
-            }
             if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) return;
             createHashCode(cNode, cacheHashCode, includeFields, callSuper, excludes, includes);
             createEquals(cNode, includeFields, callSuper, useCanEqual, excludes, includes);

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java
----------------------------------------------------------------------
diff --git a/src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java b/src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java
index f915887..e16ee63 100644
--- a/src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java
@@ -81,11 +81,6 @@ public class ToStringASTTransformation extends AbstractASTTransformation {
             boolean includePackage = !memberHasValue(anno, "includePackage", false);
             boolean allProperties = !memberHasValue(anno, "allProperties", false);
 
-            if (hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
-                AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
-                if (excludes == null || excludes.isEmpty()) excludes = getMemberList(canonical, "excludes");
-                if (includes == null || includes.isEmpty()) includes = getMemberList(canonical, "includes");
-            }
             if (!checkIncludeExclude(anno, excludes, includes, MY_TYPE_NAME)) return;
             createToString(cNode, includeSuper, includeFields, excludes, includes, includeNames, ignoreNulls, includePackage, cacheToString, includeSuperProperties, allProperties);
         }

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/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 c94d361..cf9c0ee 100644
--- a/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ b/src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -112,11 +112,6 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation
             boolean useSetters = memberHasValue(anno, "useSetters", true);
             List<String> excludes = getMemberList(anno, "excludes");
             List<String> includes = getMemberList(anno, "includes");
-            if (hasAnnotation(cNode, CanonicalASTTransformation.MY_TYPE)) {
-                AnnotationNode canonical = cNode.getAnnotations(CanonicalASTTransformation.MY_TYPE).get(0);
-                if (excludes == null || excludes.isEmpty()) excludes = getMemberList(canonical, "excludes");
-                if (includes == null || includes.isEmpty()) includes = getMemberList(canonical, "includes");
-            }
             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;

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/spec/doc/core-metaprogramming.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-metaprogramming.adoc b/src/spec/doc/core-metaprogramming.adoc
index d05fca8..a6a6af6 100644
--- a/src/spec/doc/core-metaprogramming.adoc
+++ b/src/spec/doc/core-metaprogramming.adoc
@@ -948,7 +948,7 @@ include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=
 [[xform-Canonical]]
 ===== @groovy.transform.Canonical
 
-The `@Canonical` AST transformation combines the effects of the <<x...@ToString>>,
+The `@Canonical` meta-annotation combines the <<x...@ToString>>,
 <<x...@EqualsAndHashCode>> and <<x...@TupleConstructor>>
 annotations:
 
@@ -958,22 +958,24 @@ include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=
 ----
 
 A similar immutable class can be generated using the <<x...@Immutable>> AST transformation instead.
-The `@Canonical` AST transformation supports several configuration options:
+The `@Canonical` meta-annotation supports the configuration options found in the annotations
+it aggregates. See those annotations for more details.
 
-[cols="1,1,2,3a",options="header"]
-|=======================================================================
-|Attribute|Default value|Description|Example
-|excludes|Empty list|List of properties to exclude from tuple constructor generation|
 [source,groovy]
 ----
 include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=canonical_example_excludes,indent=0]
 ----
-|includes|Empty list|List of fields to include in tuple constructor generation|
+
+The `@Canonical` meta-annotation can be used in conjunction with an explicit use one or more of its
+component annotations, like this:
+
 [source,groovy]
 ----
-include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=canonical_example_includes,indent=0]
+include::{projectdir}/src/spec/test/CodeGenerationASTTransformsTest.groovy[tags=canonical_example_excludes,indent=0]
 ----
-|=======================================================================
+
+Any applicable annotation attributes from `@Caonical` are passed along to the explicit annotation but
+attributes already existing in the explicit annotation take precedence.
 
 [[xform-InheritConstructors]]
 ===== @groovy.transform.InheritConstructors

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/spec/test/CodeGenerationASTTransformsTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/CodeGenerationASTTransformsTest.groovy b/src/spec/test/CodeGenerationASTTransformsTest.groovy
index 5af1bb3..a8d27ef 100644
--- a/src/spec/test/CodeGenerationASTTransformsTest.groovy
+++ b/src/spec/test/CodeGenerationASTTransformsTest.groovy
@@ -612,34 +612,37 @@ class Person {
     String lastName
 }
 def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
-assert p1.toString() == 'Person(Jack)' // Effect of @ToString
+assert p1.toString() == 'Person(Jack)' // Effect of @ToString(excludes=['lastName'])
 
-def p2 = new Person('Jack') // Effect of @TupleConstructor
+def p2 = new Person('Jack') // Effect of @TupleConstructor(excludes=['lastName'])
 assert p2.toString() == 'Person(Jack)'
 
-assert p1==p2 // Effect of @EqualsAndHashCode
-assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
+assert p1==p2 // Effect of @EqualsAndHashCode(excludes=['lastName'])
+assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode(excludes=['lastName'])
 // end::canonical_example_excludes[]
 '''
 
         assertScript '''
-// tag::canonical_example_includes[]
-import groovy.transform.Canonical
+// tag::canonical_explicit_tostring[]
+import groovy.transform.*
 
-@Canonical(includes=['firstName'])
+@Canonical(excludes=['lastName'], ignoreNulls=true)
+@ToString(excludes=['firstName'])
 class Person {
     String firstName
     String lastName
 }
 def p1 = new Person(firstName: 'Jack', lastName: 'Nicholson')
-assert p1.toString() == 'Person(Jack)' // Effect of @ToString
+assert p1.toString() == 'Person(Nicholson)'  // Effect of @ToString(excludes=['firstName'], ignoreNulls=true)
 
-def p2 = new Person('Jack') // Effect of @TupleConstructor
-assert p2.toString() == 'Person(Jack)'
+def p2 = new Person('Jack')  // Effect of @TupleConstructor(excludes=['lastName'])
+assert p2.firstName == 'Jack'
+assert p2.lastName == null
+assert p2.toString() == 'Person()'  // Effect of @ToString(excludes=['firstName'], ignoreNulls=true)
 
-assert p1==p2 // Effect of @EqualsAndHashCode
-assert p1.hashCode()==p2.hashCode() // Effect of @EqualsAndHashCode
-// end::canonical_example_includes[]
+assert p1 == p2  // Effect of @EqualsAndHashCode(excludes=['lastName'])
+assert p1.hashCode() == p2.hashCode()  // Effect of @EqualsAndHashCode(excludes=['lastName'])
+// end::canonical_explicit_tostring[]
 '''
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy b/src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy
index 8bcc67e..944b103 100644
--- a/src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy
@@ -38,20 +38,6 @@ class CanonicalTransformTest extends GroovyShellTestCase {
         assertEquals objects[0], objects[1]
     }
 
-    void testCanonicalCantAlsoBeImmutable() {
-        def msg = shouldFail(RuntimeException) {
-            assertScript """
-                import groovy.transform.*
-                @Canonical
-                @Immutable
-                class Foo {
-                    String bar
-                }
-            """
-        }
-        assert msg.contains("@Canonical class 'Foo' can't also be @Immutable")
-    }
-
     void testCanonicalWithDeclaredConstructor() {
         def msg = shouldFail(GroovyRuntimeException) {
             assertScript """
@@ -102,6 +88,30 @@ class CanonicalTransformTest extends GroovyShellTestCase {
         """
     }
 
+    void testCanonicalWithSuper() {
+        // related to Apache PR#9: "Add includeSuper property to @Canonical annotation"
+        assertScript """
+            import groovy.transform.*
+            @Canonical
+            class Foo {
+              int a
+            }
+            @Canonical(callSuper=true, includeSuperProperties=true, includeNames=true)
+            class Bar extends Foo {
+              int b
+            }
+            @Canonical(callSuper=true, includeSuper=true)
+            @TupleConstructor(includeSuperProperties=true)
+            class Baz extends Foo {
+              int b
+            }
+            def (b1, b2, b3) = [new Bar(a:5, b:20), new Bar(10, 20), new Baz(15, 20)]
+            assert [b1, b2, b3].toString() == '[Bar(b:20, a:5), Bar(b:20, a:10), Baz(20, Foo(15))]'
+            assert b1 != b2
+            assert b1.hashCode() != b2.hashCode()
+        """
+    }
+
     void testCanonicalChange() {
         def objects = evaluate("""
               @groovy.transform.Canonical class Foo {

http://git-wip-us.apache.org/repos/asf/incubator-groovy/blob/3844f283/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
index fb95bd7..1d03e2b 100644
--- a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy
@@ -127,20 +127,6 @@ class ImmutableTransformTest extends GroovyShellTestCase {
         assert cls == 'Dolly'
     }
 
-    void testImmutableCantAlsoBeMutable() {
-        def msg = shouldFail(RuntimeException) {
-            assertScript """
-                import groovy.transform.*
-                @Immutable
-                @Canonical
-                class Foo {
-                    String bar
-                }
-            """
-        }
-        assert msg.contains("@Canonical class 'Foo' can't also be @Immutable")
-    }
-
     void testImmutableListProp() {
         def objects = evaluate("""
             import groovy.transform.Immutable