Repository: groovy Updated Branches: refs/heads/master 889fb49c9 -> ae51fc62b
GROOVY-8440: Groovy's @Immutable annotation should be re-vamped to be a meta-annotation (closes #653) Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/ae51fc62 Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/ae51fc62 Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/ae51fc62 Branch: refs/heads/master Commit: ae51fc62b050d7408508cf2f919b97ad1d06fba3 Parents: 889fb49 Author: paulk <[email protected]> Authored: Wed Jan 10 23:24:25 2018 +1000 Committer: paulk <[email protected]> Committed: Wed Jan 24 12:57:28 2018 +1000 ---------------------------------------------------------------------- .../groovy/groovy/transform/Immutable.groovy | 181 ++++++++++++++ src/main/groovy/groovy/transform/Immutable.java | 249 ------------------- .../groovy/groovy/transform/ImmutableBase.java | 110 ++++++++ .../groovy/groovy/transform/KnownImmutable.java | 39 +++ .../groovy/groovy/transform/MapConstructor.java | 14 ++ .../groovy/transform/TupleConstructor.java | 10 + .../transform/AbstractASTTransformation.java | 2 +- .../transform/ImmutableASTTransformation.java | 248 +++++++++--------- .../MapConstructorASTTransformation.java | 30 ++- .../TupleConstructorASTTransformation.java | 24 +- .../test/builder/ObjectGraphBuilderTest.groovy | 2 +- ...ithJointCompilationGroovy6836StubTest.groovy | 2 - .../CanonicalComponentsTransformTest.groovy | 20 +- .../transform/ImmutableTransformTest.groovy | 8 +- .../TransformsAndCustomClassLoadersTest.groovy | 15 +- 15 files changed, 552 insertions(+), 402 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/Immutable.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/Immutable.groovy b/src/main/groovy/groovy/transform/Immutable.groovy new file mode 100644 index 0000000..e59342b --- /dev/null +++ b/src/main/groovy/groovy/transform/Immutable.groovy @@ -0,0 +1,181 @@ +/* + * 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 + +/** + * Meta annotation used when defining immutable classes. + * <p> + * It allows you to write classes in this shortened form: + * <pre class="groovyTestCase"> + * {@code @groovy.transform.Immutable} class Customer { + * String first, last + * int age + * Date since + * Collection favItems + * } + * def d = new Date() + * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games']) + * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games']) + * assert c1 == c2 + * </pre> + * The {@code @Immutable} meta-annotation corresponds to adding the following annotations: + * {@code @ToString}, {@code @EqualsAndHashCode}, {@code @ImmutableBase}, {@code @KnownImmutable}, {@code @MapConstructor} and {@code @TupleConstructor}. + * 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 immutable 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>Properties must be of an immutable type or a type with a strategy for handling non-immutable + * characteristics. Specifically, the type must be one of the primitive or wrapper types, Strings, enums, + * other {@code @Immutable} classes or known immutables (e.g. java.awt.Color, java.net.URI, java.util.UUID). + * Also handled are Cloneable classes, collections, maps and arrays, other "effectively immutable" + * classes with special handling (e.g. java.util.Date), and usages of java.util.Optional where the + * contained type is immutable (e.g. Optional<String>). + * <li>Properties automatically have private, final backing fields with getters. + * Attempts to update the property will result in a {@code ReadOnlyPropertyException}. + * <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. + * 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. + * <li>{@code Date}s, {@code Cloneable}s and arrays are defensively copied on the way in (constructor) and out (getters). + * Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes, + * it is up to you to define this method and use deep cloning if appropriate. + * <li>{@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!). + * Attempts to update them will result in an {@code UnsupportedOperationException}. + * <li>Fields that are enums or other {@code @Immutable} classes are allowed but for an + * otherwise possible mutable property type, an error is thrown. + * <li>You don't have to follow Groovy's normal property conventions, e.g. you can create an explicit private field and + * then you can write explicit get and set methods. Such an approach, isn't currently prohibited (to give you some + * wiggle room to get around these conventions) but any fields created in this way are deemed not to be part of the + * significant state of the object and aren't factored into the {@code equals} or {@code hashCode} methods. + * Similarly, you may use static properties (though usually this is discouraged) and these too will be ignored + * as far as significant state is concerned. If you do break standard conventions, you do so at your own risk and + * your objects may no longer be immutable. It is up to you to ensure that your objects remain immutable at least + * to the extent expected in other parts of your program! + * </ul> + * Immutable classes are particularly useful for functional and concurrent styles of programming + * and for use as key values within maps. If you want similar functionality to what this annotation + * provides but don't need immutability then consider using {@code @Canonical}. + * <p> + * Customising behaviour: + * <p> + * You can customise the toString() method provided for you by {@code @Immutable} + * by also adding the {@code @ToString} annotation to your class definition. + * <p> + * Limitations: + * <ul> + * <li> + * As outlined above, Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes, + * it is up to you to define this method and use deep cloning if appropriate. + * </li> + * <li> + * As outlined above, {@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!). + * </li> + * <li> + * Currently {@code BigInteger} and {@code BigDecimal} are deemed immutable but see: + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6348370 + * </li> + * <li> + * {@code java.awt.Color} is treated as "effectively immutable" but is not final so while not normally used with child + * classes, it isn't strictly immutable. Use at your own risk. + * </li> + * <li> + * {@code java.util.Date} is treated as "effectively immutable" but is not final so it isn't strictly immutable. + * Use at your own risk. + * </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> + * <p>More examples:</p> + -------------------------------------------------------------------------------- + * <pre class="groovyTestCase"> + * import groovy.transform.* + * + * @Canonical + * class Building { + * String name + * int floors + * boolean officeSpace + * } + * + * // Constructors are added. + * def officeSpace = new Building('Initech office', 1, true) + * + * // toString() added. + * assert officeSpace.toString() == 'Building(Initech office, 1, true)' + * + * // Default values are used if constructor + * // arguments are not assigned. + * def theOffice = new Building('Wernham Hogg Paper Company') + * assert theOffice.floors == 0 + * theOffice.officeSpace = true + * + * def anotherOfficeSpace = new Building(name: 'Initech office', floors: 1, officeSpace: true) + * + * // equals() method is added. + * assert anotherOfficeSpace == officeSpace + * + * // equals() and hashCode() are added, so duplicate is not in Set. + * def offices = [officeSpace, anotherOfficeSpace, theOffice] as Set + * assert offices.size() == 2 + * assert offices.name.join(',') == 'Initech office,Wernham Hogg Paper Company' + * + * @Canonical + * @ToString(excludes='age') // Customize one of the transformations. + * class Person { + * String name + * int age + * } + * + * def mrhaki = new Person('mrhaki', 37) + * assert mrhaki.toString() == 'Person(mrhaki)' + * </pre> + * + * @see ToString + * @see EqualsAndHashCode + * @see ImmutableBase + * @see KnownImmutable + * @see MapConstructor + * @see TupleConstructor + * @see Canonical + * @since 1.7 + */ +@ToString(cache = true, includeSuperProperties = true) +@EqualsAndHashCode(cache = true) +@ImmutableBase +@TupleConstructor(makeImmutable = true, defaults = false) +@MapConstructor(makeImmutable = true, noArg = true, includeSuperProperties = true) +@KnownImmutable +@AnnotationCollector(mode=AnnotationCollectorMode.PREFER_EXPLICIT_MERGED) +@interface Immutable { } http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/Immutable.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/Immutable.java b/src/main/groovy/groovy/transform/Immutable.java deleted file mode 100644 index 5f6bf90..0000000 --- a/src/main/groovy/groovy/transform/Immutable.java +++ /dev/null @@ -1,249 +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 immutable classes. - * <p> - * It allows you to write classes in this shortened form: - * <pre class="groovyTestCase"> - * {@code @groovy.transform.Immutable} class Customer { - * String first, last - * int age - * Date since - * Collection favItems - * } - * def d = new Date() - * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:d, favItems:['Books', 'Games']) - * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games']) - * assert c1 == c2 - * </pre> - * The {@code @Immutable} annotation instructs the compiler to execute an - * AST transformation which adds the necessary getters, constructors, - * equals, hashCode and other helper methods that are typically written - * when creating immutable 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>Properties must be of an immutable type or a type with a strategy for handling non-immutable - * characteristics. Specifically, the type must be one of the primitive or wrapper types, Strings, enums, - * other {@code @Immutable} classes or known immutables (e.g. java.awt.Color, java.net.URI, java.util.UUID). - * Also handled are Cloneable classes, collections, maps and arrays, other "effectively immutable" - * classes with special handling (e.g. java.util.Date), and usages of java.util.Optional where the - * contained type is immutable (e.g. Optional<String>). - * <li>Properties automatically have private, final backing fields with getters. - * Attempts to update the property will result in a {@code ReadOnlyPropertyException}. - * <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. - * 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. - * <li>{@code Date}s, {@code Cloneable}s and arrays are defensively copied on the way in (constructor) and out (getters). - * Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes, - * it is up to you to define this method and use deep cloning if appropriate. - * <li>{@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!). - * Attempts to update them will result in an {@code UnsupportedOperationException}. - * <li>Fields that are enums or other {@code @Immutable} classes are allowed but for an - * otherwise possible mutable property type, an error is thrown. - * <li>You don't have to follow Groovy's normal property conventions, e.g. you can create an explicit private field and - * then you can write explicit get and set methods. Such an approach, isn't currently prohibited (to give you some - * wiggle room to get around these conventions) but any fields created in this way are deemed not to be part of the - * significant state of the object and aren't factored into the {@code equals} or {@code hashCode} methods. - * Similarly, you may use static properties (though usually this is discouraged) and these too will be ignored - * as far as significant state is concerned. If you do break standard conventions, you do so at your own risk and - * your objects may no longer be immutable. It is up to you to ensure that your objects remain immutable at least - * to the extent expected in other parts of your program! - * </ul> - * Immutable classes are particularly useful for functional and concurrent styles of programming - * and for use as key values within maps. If you want similar functionality to what this annotation - * provides but don't need immutability then consider using {@code @Canonical}. - * <p> - * Customising behaviour: - * <p> - * You can customise the toString() method provided for you by {@code @Immutable} - * by also adding the {@code @ToString} annotation to your class definition. - * <p> - * Limitations: - * <ul> - * <li> - * As outlined above, Arrays and {@code Cloneable} objects use the {@code clone} method. For your own classes, - * it is up to you to define this method and use deep cloning if appropriate. - * </li> - * <li> - * As outlined above, {@code Collection}s and {@code Map}s are wrapped by immutable wrapper classes (but not deeply cloned!). - * </li> - * <li> - * Currently {@code BigInteger} and {@code BigDecimal} are deemed immutable but see: - * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6348370 - * </li> - * <li> - * {@code java.awt.Color} is treated as "effectively immutable" but is not final so while not normally used with child - * classes, it isn't strictly immutable. Use at your own risk. - * </li> - * <li> - * {@code java.util.Date} is treated as "effectively immutable" but is not final so it isn't strictly immutable. - * Use at your own risk. - * </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> - * <p>More examples:</p> - -------------------------------------------------------------------------------- - * <pre class="groovyTestCase"> - * import groovy.transform.* - * - * @Canonical - * class Building { - * String name - * int floors - * boolean officeSpace - * } - * - * // Constructors are added. - * def officeSpace = new Building('Initech office', 1, true) - * - * // toString() added. - * assert officeSpace.toString() == 'Building(Initech office, 1, true)' - * - * // Default values are used if constructor - * // arguments are not assigned. - * def theOffice = new Building('Wernham Hogg Paper Company') - * assert theOffice.floors == 0 - * theOffice.officeSpace = true - * - * def anotherOfficeSpace = new Building(name: 'Initech office', floors: 1, officeSpace: true) - * - * // equals() method is added. - * assert anotherOfficeSpace == officeSpace - * - * // equals() and hashCode() are added, so duplicate is not in Set. - * def offices = [officeSpace, anotherOfficeSpace, theOffice] as Set - * assert offices.size() == 2 - * assert offices.name.join(',') == 'Initech office,Wernham Hogg Paper Company' - * - * @Canonical - * @ToString(excludes='age') // Customize one of the transformations. - * class Person { - * String name - * int age - * } - * - * def mrhaki = new Person('mrhaki', 37) - * assert mrhaki.toString() == 'Person(mrhaki)' - * </pre> - * - * @author Paul King - * @author Andre Steingress - * @see groovy.transform.ToString - * @see groovy.transform.Canonical - * @since 1.7 - */ [email protected] -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) -@GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation") -public @interface Immutable { - /** - * Allows you to provide {@code @Immutable} with a list of classes which - * are deemed immutable. By supplying a class in this list, you are vouching - * for its immutability and {@code @Immutable} will do no further checks. - * Example: - * <pre> - * import groovy.transform.* - * {@code @Immutable}(knownImmutableClasses = [Address]) - * class Person { - * String first, last - * Address address - * } - * - * {@code @TupleConstructor} - * class Address { - * final String street - * } - * </pre> - * - * @since 1.8.7 - */ - Class[] knownImmutableClasses() default {}; - - /** - * Allows you to provide {@code @Immutable} with a list of property names which - * are deemed immutable. By supplying a property's name in this list, you are vouching - * for its immutability and {@code @Immutable} will do no further checks. - * Example: - * <pre> - * {@code @groovy.transform.Immutable}(knownImmutables = ['address']) - * class Person { - * String first, last - * Address address - * } - * ... - * </pre> - * - * @since 2.1.0 - */ - String[] knownImmutables() default {}; - - /** - * If {@code true}, this adds a method {@code copyWith} which takes a Map of - * new property values and returns a new instance of the Immutable class with - * these values set. - * Example: - * <pre class="groovyTestCase"> - * {@code @groovy.transform.Immutable}(copyWith = true) - * class Person { - * String first, last - * } - * - * def tim = new Person( 'tim', 'yates' ) - * def alice = tim.copyWith( first:'alice' ) - * - * assert tim.first == 'tim' - * assert alice.first == 'alice' - * </pre> - * Unknown keys in the map are ignored, and if the values would not change - * the object, then the original object is returned. - * - * If a method called {@code copyWith} that takes a single parameter already - * exists in the class, then this setting is ignored, and no method is generated. - * - * @since 2.2.0 - */ - boolean copyWith() default false; -} http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/ImmutableBase.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/ImmutableBase.java b/src/main/groovy/groovy/transform/ImmutableBase.java new file mode 100644 index 0000000..e6689da --- /dev/null +++ b/src/main/groovy/groovy/transform/ImmutableBase.java @@ -0,0 +1,110 @@ +/* + * 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 immutable classes. + * Checks on the validity of an immutable class and makes some preliminary changes to the class. + * Usually used via the {@code @Immutable} meta annotation. + * + * @see Immutable + * @see MapConstructor + * @see TupleConstructor + * @since 2.5 + */ [email protected] +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation") +public @interface ImmutableBase { + /** + * Allows you to provide {@code @Immutable} with a list of classes which + * are deemed immutable. By supplying a class in this list, you are vouching + * for its immutability and {@code @Immutable} will do no further checks. + * Example: + * <pre> + * import groovy.transform.* + * {@code @Immutable}(knownImmutableClasses = [Address]) + * class Person { + * String first, last + * Address address + * } + * + * {@code @TupleConstructor} + * class Address { + * final String street + * } + * </pre> + * + * @since 1.8.7 + */ + Class[] knownImmutableClasses() default {}; + + /** + * Allows you to provide {@code @Immutable} with a list of property names which + * are deemed immutable. By supplying a property's name in this list, you are vouching + * for its immutability and {@code @Immutable} will do no further checks. + * Example: + * <pre> + * {@code @groovy.transform.Immutable}(knownImmutables = ['address']) + * class Person { + * String first, last + * Address address + * } + * ... + * </pre> + * + * @since 2.1.0 + */ + String[] knownImmutables() default {}; + + /** + * If {@code true}, this adds a method {@code copyWith} which takes a Map of + * new property values and returns a new instance of the Immutable class with + * these values set. + * Example: + * <pre class="groovyTestCase"> + * {@code @groovy.transform.Immutable}(copyWith = true) + * class Person { + * String first, last + * } + * + * def tim = new Person( 'tim', 'yates' ) + * def alice = tim.copyWith( first:'alice' ) + * + * assert tim.first == 'tim' + * assert alice.first == 'alice' + * </pre> + * Unknown keys in the map are ignored, and if the values would not change + * the object, then the original object is returned. + * + * If a method called {@code copyWith} that takes a single parameter already + * exists in the class, then this setting is ignored, and no method is generated. + * + * @since 2.2.0 + */ + boolean copyWith() default false; +} http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/KnownImmutable.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/KnownImmutable.java b/src/main/groovy/groovy/transform/KnownImmutable.java new file mode 100644 index 0000000..c7a349d --- /dev/null +++ b/src/main/groovy/groovy/transform/KnownImmutable.java @@ -0,0 +1,39 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Class annotation used as a marker for immutable classes. + * Not usually used explicitly but rather implicitly via the {@code @Immutable} meta annotation which adds it automatically. + * If you create your own Java or Groovy immutable class manually, you can add this annotation if you don't want to have + * to list your class as one of the known immutable classes. + * + * @see Immutable + * @since 2.5 + */ [email protected] +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface KnownImmutable { +} http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/MapConstructor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/MapConstructor.java b/src/main/groovy/groovy/transform/MapConstructor.java index 64cd1da..ccd127a 100644 --- a/src/main/groovy/groovy/transform/MapConstructor.java +++ b/src/main/groovy/groovy/transform/MapConstructor.java @@ -27,6 +27,8 @@ import java.lang.annotation.Target; /** * Class annotation used to assist in the creation of map constructors in classes. + * If the class is also annotated with {@code @KnownImmutable}, then the generated + * constructor will contain additional code needed for immutable classes. * <p> * It allows you to write classes in this shortened form: * <pre class="groovyTestCase"> @@ -94,6 +96,11 @@ public @interface MapConstructor { boolean includeFields() default false; /** + * Whether immutable pre-cautions (defensive copying, cloning, etc.) should be applied to incoming/outgoing properties. + */ + boolean makeImmutable() default false; + + /** * Include properties in the constructor. */ boolean includeProperties() default true; @@ -104,6 +111,13 @@ public @interface MapConstructor { boolean includeSuperProperties() default false; /** + * Whether to include all properties (as per the JavaBean spec) in the generated constructor. + * When true, Groovy treats any explicitly created setXxx() methods as property setters as per the JavaBean + * specification. + */ + boolean allProperties() default false; + + /** * 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/groovy/blob/ae51fc62/src/main/groovy/groovy/transform/TupleConstructor.java ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/transform/TupleConstructor.java b/src/main/groovy/groovy/transform/TupleConstructor.java index 12e0cb6..bad5518 100644 --- a/src/main/groovy/groovy/transform/TupleConstructor.java +++ b/src/main/groovy/groovy/transform/TupleConstructor.java @@ -27,6 +27,9 @@ import java.lang.annotation.Target; /** * Class annotation used to assist in the creation of tuple constructors in classes. + * If the class is also annotated with {@code @KnownImmutable}, then the generated + * constructor will contain additional code needed for immutable classes. + * * Should be used with care with other annotations which create constructors - see "Known * Limitations" for more details. * <p> @@ -229,6 +232,13 @@ public @interface TupleConstructor { boolean force() default false; /** + * Whether immutable pre-cautions (defensive copying, cloning, etc.) should be applied to incoming/outgoing properties. + * + * @since 2.5.0 + */ + boolean makeImmutable() 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 http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java index 24313f6..e8ca861 100644 --- a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java @@ -250,7 +250,7 @@ public abstract class AbstractASTTransformation implements Opcodes, ASTTransform return true; } - public boolean hasAnnotation(ClassNode cNode, ClassNode annotation) { + public static boolean hasAnnotation(ClassNode cNode, ClassNode annotation) { List annots = cNode.getAnnotations(annotation); return (annots != null && !annots.isEmpty()); } http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java index 97d5033..dcbe6d0 100644 --- a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java @@ -21,7 +21,8 @@ package org.codehaus.groovy.transform; import groovy.lang.MetaClass; import groovy.lang.MissingPropertyException; import groovy.lang.ReadOnlyPropertyException; -import groovy.transform.Immutable; +import groovy.transform.ImmutableBase; +import groovy.transform.KnownImmutable; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; @@ -50,6 +51,7 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.ReflectionMethodInvoker; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; @@ -97,16 +99,9 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX; import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS; import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; -import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createEquals; -import static org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation.createHashCode; -import static org.codehaus.groovy.transform.ToStringASTTransformation.createToString; /** * Handles generation of code for the @Immutable annotation. - * - * @author Paul King - * @author Andre Steingress - * @author Tim Yates */ @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) public class ImmutableASTTransformation extends AbstractASTTransformation { @@ -138,13 +133,16 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { "java.net.URI", "java.util.UUID" ); - private static final Class MY_CLASS = groovy.transform.Immutable.class; + private static final Class MY_CLASS = ImmutableBase.class; + private static final Class<? extends Annotation> KNOWN_IMMUTABLE_CLASS = KnownImmutable.class; + private static final Class<? extends Annotation> IMMUTABLE_BASE_CLASS = ImmutableBase.class; + private static final ClassNode IMMUTABLE_BASE_TYPE = makeWithoutCaching(IMMUTABLE_BASE_CLASS, false); public static final ClassNode MY_TYPE = make(MY_CLASS); static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); - static final String MEMBER_KNOWN_IMMUTABLE_CLASSES = "knownImmutableClasses"; - static final String MEMBER_KNOWN_IMMUTABLES = "knownImmutables"; - static final String MEMBER_ADD_COPY_WITH = "copyWith"; - static final String COPY_WITH_METHOD = "copyWith"; + private static final String MEMBER_KNOWN_IMMUTABLE_CLASSES = "knownImmutableClasses"; + private static final String MEMBER_KNOWN_IMMUTABLES = "knownImmutables"; + private static final String MEMBER_ADD_COPY_WITH = "copyWith"; + private static final String COPY_WITH_METHOD = "copyWith"; private static final ClassNode DATE_TYPE = make(Date.class); private static final ClassNode CLONEABLE_TYPE = make(Cloneable.class); @@ -165,62 +163,56 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { init(nodes, source); AnnotatedNode parent = (AnnotatedNode) nodes[1]; AnnotationNode node = (AnnotationNode) nodes[0]; - // temporarily have weaker check which allows for old Deprecated Annotation -// if (!MY_TYPE.equals(node.getClassNode())) return; - if (!node.getClassNode().getName().endsWith(".Immutable")) return; - List<PropertyNode> newProperties = new ArrayList<PropertyNode>(); + if (!MY_TYPE.equals(node.getClassNode())) return; if (parent instanceof ClassNode) { - final List<String> knownImmutableClasses = getKnownImmutableClasses(node); - final List<String> knownImmutables = getKnownImmutables(node); - - ClassNode cNode = (ClassNode) parent; - String cName = cNode.getName(); - if (!checkNotInterface(cNode, MY_TYPE_NAME)) return; - if (!checkPropertyList(cNode, knownImmutables, "knownImmutables", node, MY_TYPE_NAME, false)) return; - makeClassFinal(cNode); - - final List<PropertyNode> pList = getInstanceProperties(cNode); - for (PropertyNode pNode : pList) { - adjustPropertyForImmutability(pNode, newProperties); - } - for (PropertyNode pNode : newProperties) { - cNode.getProperties().remove(pNode); - addProperty(cNode, pNode); - } - final List<FieldNode> fList = cNode.getFields(); - for (FieldNode fNode : fList) { - ensureNotPublic(cName, fNode); - } - boolean includeSuperProperties = false; - if (hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) { - AnnotationNode tupleCons = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0); - includeSuperProperties = memberHasValue(tupleCons, "includeSuperProperties", true); - if (unsupportedTupleAttribute(tupleCons, "excludes")) return; - if (unsupportedTupleAttribute(tupleCons, "includes")) return; - if (unsupportedTupleAttribute(tupleCons, "includeFields")) return; - if (unsupportedTupleAttribute(tupleCons, "includeProperties")) return; - if (unsupportedTupleAttribute(tupleCons, "includeSuperFields")) return; - if (unsupportedTupleAttribute(tupleCons, "callSuper")) return; - if (unsupportedTupleAttribute(tupleCons, "force")) return; - } - createConstructors(cNode, knownImmutableClasses, knownImmutables, includeSuperProperties); - if (!hasAnnotation(cNode, EqualsAndHashCodeASTTransformation.MY_TYPE)) { - createHashCode(cNode, true, false, false, null, null); - createEquals(cNode, false, false, false, null, null); - } - if (!hasAnnotation(cNode, ToStringASTTransformation.MY_TYPE)) { - createToString(cNode, false, false, null, null, false, true, true, true); - } - if( memberHasValue(node, MEMBER_ADD_COPY_WITH, true) && - !pList.isEmpty() && - !hasDeclaredMethod(cNode, COPY_WITH_METHOD, 1) ) { - createCopyWith( cNode, pList ) ; - } + doMakeImmutable((ClassNode) parent, node); + } + } + + private void doMakeImmutable(ClassNode cNode, AnnotationNode node) { + List<PropertyNode> newProperties = new ArrayList<PropertyNode>(); +// final List<String> knownImmutableClasses = getKnownImmutableClasses(this, node); + final List<String> knownImmutables = getKnownImmutables(this, node); + + String cName = cNode.getName(); + if (!checkNotInterface(cNode, MY_TYPE_NAME)) return; + if (!checkPropertyList(cNode, knownImmutables, "knownImmutables", node, "immutable class", false)) return; + makeClassFinal(this, cNode); + + final List<PropertyNode> pList = getInstanceProperties(cNode); + for (PropertyNode pNode : pList) { + adjustPropertyForImmutability(pNode, newProperties); + } + for (PropertyNode pNode : newProperties) { + cNode.getProperties().remove(pNode); + addProperty(cNode, pNode); + } + final List<FieldNode> fList = cNode.getFields(); + for (FieldNode fNode : fList) { + ensureNotPublic(this, cName, fNode); + } + boolean includeSuperProperties = false; + if (hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) { + AnnotationNode tupleCons = cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0); +// includeSuperProperties = memberHasValue(tupleCons, "includeSuperProperties", true); + if (unsupportedTupleAttribute(tupleCons, "excludes")) return; + if (unsupportedTupleAttribute(tupleCons, "includes")) return; + if (unsupportedTupleAttribute(tupleCons, "includeFields")) return; + if (unsupportedTupleAttribute(tupleCons, "includeProperties")) return; + if (unsupportedTupleAttribute(tupleCons, "includeSuperFields")) return; + if (unsupportedTupleAttribute(tupleCons, "callSuper")) return; + if (unsupportedTupleAttribute(tupleCons, "force")) return; + } + if (!validateConstructors(cNode)) return; +// createConstructors(this, cNode, knownImmutableClasses, knownImmutables, includeSuperProperties); + if (memberHasValue(node, MEMBER_ADD_COPY_WITH, true) && !pList.isEmpty() && + !hasDeclaredMethod(cNode, COPY_WITH_METHOD, 1)) { + createCopyWith(cNode, pList); } } - protected boolean unsupportedTupleAttribute(AnnotationNode anno, String memberName) { + 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 + @@ -240,7 +232,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { break; } } - if (argsParam!=null) { + if (argsParam != null) { final Parameter arg = argsParam; ClassCodeVisitorSupport variableExpressionFix = new ClassCodeVisitorSupport() { @Override @@ -260,14 +252,14 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { } } - private List<String> getKnownImmutableClasses(AnnotationNode node) { + static List<String> getKnownImmutableClasses(AbstractASTTransformation xform, AnnotationNode node) { final List<String> immutableClasses = new ArrayList<String>(); final Expression expression = node.getMember(MEMBER_KNOWN_IMMUTABLE_CLASSES); if (expression == null) return immutableClasses; if (!(expression instanceof ListExpression)) { - addError("Use the Groovy list notation [el1, el2] to specify known immutable classes via \"" + MEMBER_KNOWN_IMMUTABLE_CLASSES + "\"", node); + xform.addError("Use the Groovy list notation [el1, el2] to specify known immutable classes via \"" + MEMBER_KNOWN_IMMUTABLE_CLASSES + "\"", node); return immutableClasses; } @@ -281,14 +273,14 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { return immutableClasses; } - private List<String> getKnownImmutables(AnnotationNode node) { + static List<String> getKnownImmutables(AbstractASTTransformation xform, AnnotationNode node) { final List<String> immutables = new ArrayList<String>(); final Expression expression = node.getMember(MEMBER_KNOWN_IMMUTABLES); if (expression == null) return immutables; if (!(expression instanceof ListExpression)) { - addError("Use the Groovy list notation [el1, el2] to specify known immutable property names via \"" + MEMBER_KNOWN_IMMUTABLES + "\"", node); + xform.addError("Use the Groovy list notation [el1, el2] to specify known immutable property names via \"" + MEMBER_KNOWN_IMMUTABLES + "\"", node); return immutables; } @@ -302,21 +294,35 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { return immutables; } - private void makeClassFinal(ClassNode cNode) { + 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)) { - addError("Error during " + MY_TYPE_NAME + " processing: annotation found on inappropriate class " + cNode.getName(), cNode); + xform.addError("Error during " + MY_TYPE_NAME + " processing: annotation found on inappropriate class " + cNode.getName(), cNode); return; } cNode.setModifiers(modifiers | ACC_FINAL); } } - private void createConstructors(ClassNode cNode, List<String> knownImmutableClasses, List<String> knownImmutables, boolean includeSuperProperties) { - if (!validateConstructors(cNode)) return; + private static void createConstructors(AbstractASTTransformation xform, ClassNode cNode, List<String> knownImmutableClasses, List<String> knownImmutables, boolean includeSuperProperties, boolean allProperties) { + List<PropertyNode> list = getProperties(cNode, includeSuperProperties, allProperties); + boolean specialHashMapCase = isSpecialHashMapCase(list); + if (specialHashMapCase) { + createConstructorMapSpecial(cNode, list); + } else { + createConstructorMap(xform, cNode, list, knownImmutableClasses, knownImmutables); + createConstructorOrdered(cNode, list); + } + } + static boolean isSpecialHashMapCase(List<PropertyNode> list) { + return list.size() == 1 && list.get(0).getField().getType().equals(HASHMAP_TYPE); + } + + static List<PropertyNode> getProperties(ClassNode cNode, boolean includeSuperProperties, boolean allProperties) { List<PropertyNode> list = getInstanceProperties(cNode); + //addPseudoProperties if (includeSuperProperties) { ClassNode next = cNode.getSuperClass(); while (next != null) { @@ -326,16 +332,10 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { next = next.getSuperClass(); } } - boolean specialHashMapCase = list.size() == 1 && list.get(0).getField().getType().equals(HASHMAP_TYPE); - if (specialHashMapCase) { - createConstructorMapSpecial(cNode, list); - } else { - createConstructorMap(cNode, list, knownImmutableClasses, knownImmutables); - createConstructorOrdered(cNode, list); - } + return list; } - private static void createConstructorOrdered(ClassNode cNode, List<PropertyNode> list) { + static void createConstructorOrdered(ClassNode cNode, List<PropertyNode> list) { final MapExpression argMap = new MapExpression(); final Parameter[] orderedParams = new Parameter[list.size()]; int index = 0; @@ -388,17 +388,24 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { return castX(type, smce); } - private static void createConstructorMapSpecial(ClassNode cNode, List<PropertyNode> list) { + static void createConstructorMapSpecial(ClassNode cNode, List<PropertyNode> list) { final BlockStatement body = new BlockStatement(); body.addStatement(createConstructorStatementMapSpecial(list.get(0).getField())); createConstructorMapCommon(cNode, body); } - private void createConstructorMap(ClassNode cNode, List<PropertyNode> list, List<String> knownImmutableClasses, List<String> knownImmutables) { + static void createConstructorMap(AbstractASTTransformation xform, ClassNode cNode, List<PropertyNode> list) { + AnnotationNode annoImmutable = cNode.getAnnotations(IMMUTABLE_BASE_TYPE).get(0); + final List<String> knownImmutableClasses = getKnownImmutableClasses(xform, annoImmutable); + final List<String> knownImmutables = getKnownImmutables(xform, annoImmutable); + createConstructorMap(xform, cNode, list, knownImmutableClasses, knownImmutables); + } + + private static void createConstructorMap(AbstractASTTransformation xform, ClassNode cNode, List<PropertyNode> list, List<String> knownImmutableClasses, List<String> knownImmutables) { final BlockStatement body = new BlockStatement(); body.addStatement(ifS(equalsNullX(varX("args")), assignS(varX("args"), new MapExpression()))); for (PropertyNode pNode : list) { - body.addStatement(createConstructorStatement(cNode, pNode, knownImmutableClasses, knownImmutables)); + body.addStatement(createConstructorStatement(xform, cNode, pNode, knownImmutableClasses, knownImmutables)); } // check for missing properties body.addStatement(stmt(callX(SELF_TYPE, "checkPropNames", args("this", "args")))); @@ -442,7 +449,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { final ClassNode fieldType = fieldExpr.getType(); final Expression initExpr = fNode.getInitialValueExpression(); final Statement assignInit; - if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) { + if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) { assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION); } else { assignInit = assignS(fieldExpr, cloneCollectionExpr(initExpr, fieldType)); @@ -466,11 +473,11 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { ); } - private void ensureNotPublic(String cNode, FieldNode fNode) { + private static void ensureNotPublic(AbstractASTTransformation xform, String cNode, FieldNode fNode) { String fName = fNode.getName(); // TODO: do we need to lock down things like: $ownClass if (fNode.isPublic() && !fName.contains("$") && !(fNode.isStatic() && fNode.isFinal())) { - addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode); + xform.addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode); } } @@ -489,20 +496,19 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { for (ConstructorNode constructorNode : declaredConstructors) { // allow constructors added by other transforms if flagged as safe Object nodeMetaData = constructorNode.getNodeMetaData(IMMUTABLE_SAFE_FLAG); - if (nodeMetaData != null && ((Boolean)nodeMetaData)) { + if (nodeMetaData != null && ((Boolean) nodeMetaData)) { continue; } - // TODO: allow constructors which only call provided constructor? addError("Explicit constructors not allowed for " + MY_TYPE_NAME + " class: " + cNode.getNameWithoutPackage(), constructorNode); return false; } return true; } - private Statement createConstructorStatement(ClassNode cNode, PropertyNode pNode, List<String> knownImmutableClasses, List<String> knownImmutables) { + private static Statement createConstructorStatement(AbstractASTTransformation xform, ClassNode cNode, PropertyNode pNode, List<String> knownImmutableClasses, List<String> knownImmutables) { FieldNode fNode = pNode.getField(); final ClassNode fieldType = fNode.getType(); - Statement statement = null; + Statement statement; if (fieldType.isArray() || isOrImplements(fieldType, CLONEABLE_TYPE)) { statement = createConstructorStatementArrayOrCloneable(fNode); } else if (isKnownImmutableClass(fieldType, knownImmutableClasses) || isKnownImmutable(pNode.getName(), knownImmutables)) { @@ -512,7 +518,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { } else if (isOrImplements(fieldType, COLLECTION_TYPE) || fieldType.isDerivedFrom(COLLECTION_TYPE) || isOrImplements(fieldType, MAP_TYPE) || fieldType.isDerivedFrom(MAP_TYPE)) { statement = createConstructorStatementCollection(fNode); } else if (fieldType.isResolved()) { - addError(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling"), fNode); + xform.addError(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling"), fNode); statement = EmptyStatement.INSTANCE; } else { statement = createConstructorStatementGuarded(cNode, fNode); @@ -524,7 +530,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { final Expression fieldExpr = varX(fNode); Expression initExpr = fNode.getInitialValueExpression(); final Statement assignInit; - if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) { + if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) { assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION); } else { assignInit = assignS(fieldExpr, checkUnresolved(fNode, initExpr)); @@ -543,7 +549,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { ClassNode fieldType = fieldExpr.getType(); Expression initExpr = fNode.getInitialValueExpression(); final Statement assignInit; - if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) { + if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) { assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION); } else { assignInit = assignS(fieldExpr, cloneCollectionExpr(initExpr, fieldType)); @@ -569,7 +575,8 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { if (optionalType.isResolved() && !optionalType.isPlaceholder() && !optionalType.isWildcard()) { String name = optionalType.getType().getName(); if (inImmutableList(name) || knownImmutableClasses.contains(name)) return true; - if (optionalType.getType().isEnum() || !optionalType.getType().getAnnotations(MY_TYPE).isEmpty()) return true; + if (optionalType.getType().isEnum() || !optionalType.getType().getAnnotations(MY_TYPE).isEmpty()) + return true; } } return fieldType.isEnum() || @@ -591,7 +598,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { ClassNode fieldType = fNode.getType(); final Expression array = findArg(fNode.getName()); final Statement assignInit; - if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) { + if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) { assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION); } else { assignInit = assignS(fieldExpr, cloneArrayOrCloneableExpr(initExpr, fieldType)); @@ -603,7 +610,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { final Expression fieldExpr = varX(fNode); Expression initExpr = fNode.getInitialValueExpression(); final Statement assignInit; - if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression)initExpr).isNullExpression())) { + if (initExpr == null || (initExpr instanceof ConstantExpression && ((ConstantExpression) initExpr).isNullExpression())) { assignInit = assignS(fieldExpr, ConstantExpression.EMPTY_EXPRESSION); } else { assignInit = assignS(fieldExpr, cloneDateExpr(initExpr)); @@ -644,11 +651,10 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { } private static String createErrorMessage(String className, String fieldName, String typeName, String mode) { - return MY_TYPE_NAME + " processor doesn't know how to handle field '" + fieldName + "' of type '" + - prettyTypeName(typeName) + "' while " + mode + " class " + className + ".\n" + - MY_TYPE_NAME + " classes only support properties with effectively immutable types including:\n" + + return "Unsupported type (" + prettyTypeName(typeName) + ") found for field '" + fieldName + "' while " + mode + " immutable class " + className + ".\n" + + "Immutable classes only support properties with effectively immutable types including:\n" + "- Strings, primitive types, wrapper types, Class, BigInteger and BigDecimal, enums\n" + - "- other " + MY_TYPE_NAME + " classes and known immutables (java.awt.Color, java.net.URI)\n" + + "- classes annotated with @KnownImmutable and known immutables (java.awt.Color, java.net.URI)\n" + "- Cloneable classes, collections, maps and arrays, and other classes with special handling (java.util.Date)\n" + "Other restrictions apply, please see the groovydoc for " + MY_TYPE_NAME + " for further details"; } @@ -669,7 +675,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { return safeExpression(fieldExpr, expression); } - private static Statement createCheckForProperty( final PropertyNode pNode ) { + private static Statement createCheckForProperty(final PropertyNode pNode) { return block( new VariableScope(), ifElseS( @@ -771,22 +777,37 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { public static Object checkImmutable(String className, String fieldName, Object field) { if (field == null || field instanceof Enum || inImmutableList(field.getClass().getName())) return field; if (field instanceof Collection) return DefaultGroovyMethods.asImmutable((Collection) field); - if (field.getClass().getAnnotation(MY_CLASS) != null) return field; + if (getAnnotationByName(field, "groovy.transform.Immutable") != null) return field; + final String typeName = field.getClass().getName(); throw new RuntimeException(createErrorMessage(className, fieldName, typeName, "constructing")); } + private static Annotation getAnnotationByName(Object field, String name) { + // find longhand since the annotation from earlier versions is now a meta annotation + for (Annotation an : field.getClass().getAnnotations()) { + if (an.getClass().getName().equals(name)) { + return an; + } + } + return null; + } + @SuppressWarnings("Unchecked") public static Object checkImmutable(Class<?> clazz, String fieldName, Object field) { - Immutable immutable = (Immutable) clazz.getAnnotation(MY_CLASS); - List<Class> knownImmutableClasses = new ArrayList<Class>(); - if (immutable != null && immutable.knownImmutableClasses().length > 0) { - knownImmutableClasses = Arrays.asList(immutable.knownImmutableClasses()); + if (field == null || field instanceof Enum || knownImmutable(field.getClass())) { + return field; } - if (field == null || field instanceof Enum || inImmutableList(field.getClass().getName()) || knownImmutableClasses.contains(field.getClass())) - return field; - if (field.getClass().getAnnotation(MY_CLASS) != null) return field; + boolean isImmutable = false; + for (Annotation an : field.getClass().getAnnotations()) { + if (an.getClass().getName().startsWith("groovy.transform.Immutable")) { + isImmutable = true; + break; + } + } + if (isImmutable) return field; + if (field instanceof Collection) { Field declaredField; try { @@ -796,8 +817,7 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { return DefaultGroovyMethods.asImmutable((Collection) field); } // potentially allow Collection coercion for a constructor - if (fieldType.getAnnotation(MY_CLASS) != null) return field; - if (inImmutableList(fieldType.getName()) || knownImmutableClasses.contains(fieldType)) { + if (knownImmutable(fieldType)) { return field; } } catch (NoSuchFieldException ignore) { @@ -808,6 +828,10 @@ public class ImmutableASTTransformation extends AbstractASTTransformation { throw new RuntimeException(createErrorMessage(clazz.getName(), fieldName, typeName, "constructing")); } + private static boolean knownImmutable(Class<?> clazz) { + return inImmutableList(clazz.getName()) || clazz.getAnnotation(KNOWN_IMMUTABLE_CLASS) != null; + } + public static void checkPropNames(Object instance, Map<String, Object> args) { final MetaClass metaClass = InvokerHelper.getMetaClass(instance); for (String k : args.keySet()) { http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java index 77f31dc..b6407d3 100644 --- a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java @@ -28,6 +28,7 @@ import org.codehaus.groovy.ast.ConstructorNode; import org.codehaus.groovy.ast.DynamicVariable; import org.codehaus.groovy.ast.FieldNode; 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.ClosureExpression; import org.codehaus.groovy.ast.expr.Expression; @@ -75,6 +76,8 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation { static final ClassNode MY_TYPE = make(MY_CLASS); static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); private static final ClassNode MAP_TYPE = makeWithoutCaching(Map.class, false); +// private static final ClassNode IMMUTABLE_CLASS_TYPE = makeWithoutCaching(KnownImmutable.class, false); +// private static final ClassNode IMMUTABLE_BASE_TYPE = makeWithoutCaching(ImmutableBase.class, false); // private static final ClassNode CHECK_METHOD_TYPE = make(ImmutableASTTransformation.class); public void visit(ASTNode[] nodes, SourceUnit source) { @@ -90,15 +93,17 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation { boolean includeProperties = !memberHasValue(anno, "includeProperties", false); boolean includeSuperProperties = memberHasValue(anno, "includeSuperProperties", true); boolean useSetters = memberHasValue(anno, "useSetters", true); + boolean allProperties = memberHasValue(anno, "allProperties", true); boolean noArg = memberHasValue(anno, "noArg", true); List<String> excludes = getMemberStringList(anno, "excludes"); List<String> includes = getMemberStringList(anno, "includes"); boolean allNames = memberHasValue(anno, "allNames", true); + boolean makeImmutable = memberHasValue(anno, "makeImmutable", true); if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return; - if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false)) return; - if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false)) return; - // if @Immutable is found, let it pick up options and do work so we'll skip - if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) return; + if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false)) + return; + if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false)) + return; Expression pre = anno.getMember("pre"); if (pre != null && !(pre instanceof ClosureExpression)) { @@ -111,7 +116,8 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation { return; } - createConstructors(cNode, includeFields, includeProperties, includeSuperProperties, useSetters, noArg, allNames, excludes, includes, (ClosureExpression) pre, (ClosureExpression) post, source); + createConstructors(this, cNode, includeFields, includeProperties, includeSuperProperties, useSetters, noArg, allNames, allProperties, makeImmutable, excludes, includes, (ClosureExpression) pre, (ClosureExpression) post, source); + if (pre != null) { anno.setMember("pre", new ClosureExpression(new Parameter[0], EmptyStatement.INSTANCE)); } @@ -121,12 +127,24 @@ public class MapConstructorASTTransformation extends AbstractASTTransformation { } } - public static void createConstructors(ClassNode cNode, boolean includeFields, boolean includeProperties, boolean includeSuperProperties, boolean useSetters, boolean noArg, boolean allNames, List<String> excludes, List<String> includes, ClosureExpression pre, ClosureExpression post, SourceUnit source) { + private static void createConstructors(AbstractASTTransformation xform, ClassNode cNode, boolean includeFields, boolean includeProperties, boolean includeSuperProperties, boolean useSetters, boolean noArg, boolean allNames, boolean allProperties, boolean makeImmutable, List<String> excludes, List<String> includes, ClosureExpression pre, ClosureExpression post, SourceUnit source) { List<ConstructorNode> constructors = cNode.getDeclaredConstructors(); boolean foundEmpty = constructors.size() == 1 && constructors.get(0).getFirstStatement() == null; // HACK: JavaStubGenerator could have snuck in a constructor we don't want if (foundEmpty) constructors.remove(0); + // TODO remove duplicated code from alternative paths below + if (makeImmutable) { + List<PropertyNode> list = ImmutableASTTransformation.getProperties(cNode, includeSuperProperties, allProperties); + boolean specialHashMapCase = ImmutableASTTransformation.isSpecialHashMapCase(list); + if (specialHashMapCase) { + ImmutableASTTransformation.createConstructorMapSpecial(cNode, list); + } else { + ImmutableASTTransformation.createConstructorMap(xform, cNode, list); + } + return; + } + List<FieldNode> superList = new ArrayList<FieldNode>(); if (includeSuperProperties) { superList.addAll(getSuperPropertyFields(cNode.getSuperClass())); http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java index ce2e25d..179ba1a 100644 --- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java @@ -18,6 +18,7 @@ */ package org.codehaus.groovy.transform; +import groovy.transform.KnownImmutable; import groovy.transform.TupleConstructor; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; @@ -61,8 +62,6 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.copyStatementsWithSuper 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.getAllFields; -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.ifElseS; import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS; @@ -84,6 +83,7 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation private static final ClassNode LHMAP_TYPE = makeWithoutCaching(LinkedHashMap.class, false); private static final ClassNode HMAP_TYPE = makeWithoutCaching(HashMap.class, false); private static final ClassNode CHECK_METHOD_TYPE = make(ImmutableASTTransformation.class); + private static final ClassNode IMMUTABLE_CLASS_TYPE = makeWithoutCaching(KnownImmutable.class, false); private static final Map<Class<?>, Expression> primitivesInitialValues; static { @@ -118,14 +118,13 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation boolean defaults = !memberHasValue(anno, "defaults", false); boolean useSetters = memberHasValue(anno, "useSetters", true); boolean allProperties = memberHasValue(anno, "allProperties", true); + boolean makeImmutable = memberHasValue(anno, "makeImmutable", true); List<String> excludes = getMemberStringList(anno, "excludes"); List<String> includes = getMemberStringList(anno, "includes"); boolean allNames = memberHasValue(anno, "allNames", true); if (!checkIncludeExcludeUndefinedAware(anno, excludes, includes, MY_TYPE_NAME)) return; if (!checkPropertyList(cNode, includes, "includes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false, includeSuperFields)) return; if (!checkPropertyList(cNode, excludes, "excludes", anno, MY_TYPE_NAME, includeFields, includeSuperProperties, false, includeSuperFields)) return; - // if @Immutable is found, let it pick up options and do work so we'll skip - if (hasAnnotation(cNode, ImmutableASTTransformation.MY_TYPE)) return; Expression pre = anno.getMember("pre"); if (pre != null && !(pre instanceof ClosureExpression)) { addError("Expected closure value for annotation parameter 'pre'. Found " + pre, cNode); @@ -136,9 +135,20 @@ public class TupleConstructorASTTransformation extends AbstractASTTransformation 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, allProperties, sourceUnit, - (ClosureExpression) pre, (ClosureExpression) post); + + // TODO remove duplication between various paths below + List<PropertyNode> list = ImmutableASTTransformation.getProperties(cNode, includeSuperProperties, allProperties); + boolean specialHashMapCase = ImmutableASTTransformation.isSpecialHashMapCase(list); + if (makeImmutable) { + if (!specialHashMapCase) { + ImmutableASTTransformation.createConstructorOrdered(cNode, list); + } + } else { + createConstructor(this, cNode, includeFields, includeProperties, includeSuperFields, includeSuperProperties, + callSuper, force, excludes, includes, useSetters, defaults, allNames, allProperties, sourceUnit, + (ClosureExpression) pre, (ClosureExpression) post); + } + if (pre != null) { anno.setMember("pre", new ClosureExpression(new Parameter[0], EmptyStatement.INSTANCE)); } http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/spec/test/builder/ObjectGraphBuilderTest.groovy ---------------------------------------------------------------------- diff --git a/src/spec/test/builder/ObjectGraphBuilderTest.groovy b/src/spec/test/builder/ObjectGraphBuilderTest.groovy index a32094f..4f3ccc5 100644 --- a/src/spec/test/builder/ObjectGraphBuilderTest.groovy +++ b/src/spec/test/builder/ObjectGraphBuilderTest.groovy @@ -125,7 +125,7 @@ builder.classNameResolver = "com.acme" // tag::newinstanceresolver[] builder.newInstanceResolver = { Class klazz, Map attributes -> - if (klazz.isAnnotationPresent(Immutable)) { + if (klazz.getConstructor(HashMap)) { def o = klazz.newInstance(attributes) attributes.clear() return o http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/test/org/codehaus/groovy/tools/stubgenerator/ImmutableWithJointCompilationGroovy6836StubTest.groovy ---------------------------------------------------------------------- diff --git a/src/test/org/codehaus/groovy/tools/stubgenerator/ImmutableWithJointCompilationGroovy6836StubTest.groovy b/src/test/org/codehaus/groovy/tools/stubgenerator/ImmutableWithJointCompilationGroovy6836StubTest.groovy index 82ed1d8..6a73e69 100644 --- a/src/test/org/codehaus/groovy/tools/stubgenerator/ImmutableWithJointCompilationGroovy6836StubTest.groovy +++ b/src/test/org/codehaus/groovy/tools/stubgenerator/ImmutableWithJointCompilationGroovy6836StubTest.groovy @@ -20,8 +20,6 @@ package org.codehaus.groovy.tools.stubgenerator /** * Checks that {@code @Immutable} classes work correctly with stubs. - * - * @author Paul King */ class ImmutableWithJointCompilationGroovy6836StubTest extends StringSourcesStubTestCase { http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/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 6457315..f2ffe43 100644 --- a/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy +++ b/src/test/org/codehaus/groovy/transform/CanonicalComponentsTransformTest.groovy @@ -442,13 +442,13 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { // GROOVY-5864 void testExternalizeMethodsWithImmutable() { try { - new GroovyShell().parse """ + new GroovyShell().parse ''' @groovy.transform.ExternalizeMethods @groovy.transform.Immutable class Person { String first } - """ + ''' fail('The compilation should have failed as the final field first (created via @Immutable) is being assigned to (via @ExternalizeMethods).') } catch (MultipleCompilationErrorsException e) { def syntaxError = e.errorCollector.getSyntaxError(0) @@ -459,10 +459,10 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { // GROOVY-5864 void testExternalizeVerifierWithNonExternalizableClass() { try { - new GroovyShell().parse """ + new GroovyShell().parse ''' @groovy.transform.ExternalizeVerifier class Person { } - """ + ''' fail("The compilation should have failed as the class doesn't implement Externalizable") } catch (MultipleCompilationErrorsException e) { def syntaxError = e.errorCollector.getSyntaxError(0) @@ -473,14 +473,14 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { // GROOVY-5864 void testExternalizeVerifierWithFinalField() { try { - new GroovyShell().parse """ + new GroovyShell().parse ''' @groovy.transform.ExternalizeVerifier class Person implements Externalizable { final String first void writeExternal(ObjectOutput out)throws IOException{ } void readExternal(ObjectInput objectInput)throws IOException,ClassNotFoundException{ } } - """ + ''' fail("The compilation should have failed as the final field first (can't be set inside readExternal).") } catch (MultipleCompilationErrorsException e) { def syntaxError = e.errorCollector.getSyntaxError(0) @@ -491,13 +491,13 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { // GROOVY-5864 void testAutoExternalizeWithoutNoArg() { try { - new GroovyShell().parse """ + new GroovyShell().parse ''' @groovy.transform.AutoExternalize class Person { Person(String first) {} String first } - """ + ''' fail("The compilation should have failed as there is no no-arg constructor.") } catch (MultipleCompilationErrorsException e) { def syntaxError = e.errorCollector.getSyntaxError(0) @@ -508,7 +508,7 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { // GROOVY-5864 void testExternalizeVerifierWithNonExternalizableField() { try { - new GroovyShell().parse """ + new GroovyShell().parse ''' class Name {} @groovy.transform.ExternalizeVerifier(checkPropertyTypes=true) @@ -518,7 +518,7 @@ class CanonicalComponentsTransformTest extends GroovyShellTestCase { void writeExternal(ObjectOutput out)throws IOException{ } void readExternal(ObjectInput objectInput)throws IOException,ClassNotFoundException{ } } - """ + ''' fail("The compilation should have failed as the type of Name isn't Externalizable or Serializable.") } catch (MultipleCompilationErrorsException e) { def syntaxError = e.errorCollector.getSyntaxError(0) http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/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 f867c5b..4d47351 100644 --- a/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy +++ b/src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy @@ -607,8 +607,8 @@ class ImmutableTransformTest extends GroovyShellTestCase { new Person(first: 'John', last: 'Doe', address: new Address(street: 'Street')) ''' } - assert msg.contains("doesn't know how to handle field 'address' of type 'Address'") - assert msg.contains("@Immutable classes only support properties with effectively immutable types") + assert msg.contains("Unsupported type (Address) found for field 'address' while constructing immutable class Person") + assert msg.contains("Immutable classes only support properties with effectively immutable types") } // GROOVY-5828 @@ -655,7 +655,7 @@ class ImmutableTransformTest extends GroovyShellTestCase { } ''' } - assert msg.contains("@Immutable processor doesn't know how to handle field 'name' of type 'java.lang.Object or def'") + assert msg.contains("Unsupported type (java.lang.Object or def) found for field 'name' while ") } // GROOVY-6192 @@ -986,7 +986,7 @@ class ImmutableTransformTest extends GroovyShellTestCase { new Person(surName: "Doe") """ } - assert message.contains("Error during @Immutable processing: 'knownImmutables' property 'sirName' does not exist.") + assert message.contains("Error during immutable class processing: 'knownImmutables' property 'sirName' does not exist.") } // GROOVY-7162 http://git-wip-us.apache.org/repos/asf/groovy/blob/ae51fc62/src/test/org/codehaus/groovy/transform/classloading/TransformsAndCustomClassLoadersTest.groovy ---------------------------------------------------------------------- diff --git a/src/test/org/codehaus/groovy/transform/classloading/TransformsAndCustomClassLoadersTest.groovy b/src/test/org/codehaus/groovy/transform/classloading/TransformsAndCustomClassLoadersTest.groovy index 66bf434..456f8ab 100644 --- a/src/test/org/codehaus/groovy/transform/classloading/TransformsAndCustomClassLoadersTest.groovy +++ b/src/test/org/codehaus/groovy/transform/classloading/TransformsAndCustomClassLoadersTest.groovy @@ -40,8 +40,6 @@ import java.lang.annotation.ElementType * Tests whether local and global transforms are successfully detected, loaded, * and run if separate class loaders are used for loading compile dependencies * and AST transforms. - * - * @author Peter Niederwieser */ class TransformsAndCustomClassLoadersTest extends GroovyTestCase { URL[] urls = collectUrls(getClass().classLoader) + addGroovyUrls() @@ -68,8 +66,8 @@ class TransformsAndCustomClassLoadersTest extends GroovyTestCase { } void testBuiltInLocalTransform() { - def clazz = compileAndLoadClass("@groovy.transform.Immutable class Foo { String bar }", dependencyLoader, transformLoader) - checkIsImmutable(clazz) + def clazz = compileAndLoadClass("@groovy.transform.TupleConstructor class Foo { String bar }", dependencyLoader, transformLoader) + checkHasTupleConstructor(clazz) } void testThirdPartyLocalTransform() { @@ -102,12 +100,9 @@ class TransformsAndCustomClassLoadersTest extends GroovyTestCase { return loader.defineClass(classInfo.name, classInfo.bytes) } - private checkIsImmutable(Class clazz) { - try { - def foo = clazz.newInstance(["setting property"] as Object[]) - foo.bar = "updating property" - fail() - } catch (ReadOnlyPropertyException expected) {} + private checkHasTupleConstructor(Class clazz) { + def foo = clazz.newInstance(["some property"] as Object[]) + assert foo.bar == 'some property' } private Set<URL> collectUrls(ClassLoader classLoader) {
