Index: src/main/groovy/transform/AutoCloneStyle.java =================================================================== --- src/main/groovy/transform/AutoCloneStyle.java (revision ) +++ src/main/groovy/transform/AutoCloneStyle.java (revision ) @@ -0,0 +1,24 @@ +package groovy.transform; + +/** + * Intended style to use for cloning when using the {@code @}AutoClone annotation. + * + * @author Paul King + * @since 1.8.0 + */ +public enum AutoCloneStyle { + /** + * Uses only cloning. + */ + CLONE, + + /** + * Uses the copy constructor pattern. + */ + COPY_CONSTRUCTOR, + + /** + * Uses serialization to clone. + */ + SERIALIZATION +} Index: src/test/groovy/bugs/Groovy4121Bug.groovy =================================================================== --- src/test/groovy/bugs/Groovy4121Bug.groovy (revision 19655) +++ src/test/groovy/bugs/Groovy4121Bug.groovy (revision ) @@ -21,7 +21,7 @@ void testAssignmentToAFieldMadeFinalByImmutable() { try { new GroovyShell().parse """ - @Immutable + @groovy.transform.Immutable class Account4121 { BigDecimal balance String customer Index: src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy =================================================================== --- src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy (revision ) +++ src/test/org/codehaus/groovy/transform/CanonicalTransformTest.groovy (revision ) @@ -0,0 +1,424 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 + +/** + * @author Paulo Poiati + * @author Paul King + */ +class CanonicalTransformTest extends GroovyShellTestCase { + + void testCanonical() { + def objects = evaluate(""" + import groovy.transform.Canonical + @Canonical class Foo { + String x, y + } + [new Foo(x:'x', y:'y'), + new Foo('x', 'y')] + """) + + assertEquals objects[0].hashCode(), objects[1].hashCode() + 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 """ + @groovy.transform.Canonical class Foo { + def foo, bar, baz + + Foo() {} + + Foo(foo, bar) { + this.foo = foo + this.bar = bar + } + } + + def foo = new Foo('a', 'b') + def foo1 = new Foo() + foo1.foo = 'a' + foo1.bar = 'b' + assert foo == foo1 + + // Fail here + new Foo('a', 'b', 'c') + """ + } + assert msg.contains('Could not find matching constructor') + } + + void testCanonicalNotCopyOrCloneProperty() { + assertScript """ + def date = new Date() + def array = [1, 2, 3] as Integer[] + def map = [foo: 'bar'] + def collection = [4, 5, 6] + + @groovy.transform.Canonical class Foo { + Date date + Integer[] array + Map map + Collection collection + } + + def foo = new Foo(date, array, map, collection) + + assert date.is(foo.date) + assert array.is(foo.array) + assert map.is(foo.map) + assert collection.is(foo.collection) + """ + } + + void testCanonicalChange() { + def objects = evaluate(""" + @groovy.transform.Canonical class Foo { + String x, y + } + [new Foo(x:'x', y:'y'), + new Foo('x', 'y')] + """) + objects[0].x = 'z' + assertFalse objects[0] == objects[1] + assertFalse objects[0].hashCode() == objects[1].hashCode() + } + + void testUntyped() { + def object = evaluate(""" + @groovy.transform.Canonical class Foo { + def x + def y = 10 + } + new Foo() + """) + + assert null == object.x + assert 10 == object.y + + object.x = 20 + assert 20 == object.x + object.x = [1, 2, 3] + assert [1, 2, 3] == object.x + } + + void testMapConstructorOptional() { + assertScript """ + @groovy.transform.Canonical class Foo { + String bar + String baz = 'a' + } + + def foo = new Foo(bar: 'c') + def foo1 = new Foo(baz: 'd') + assert 'a' == foo.baz + assert 'c' == foo.bar + assert 'd' == foo1.baz + assert null == foo1.bar + """ + } + + void testMapConstructorOptionalPrimitive() { + assertScript """ + @groovy.transform.Canonical class Foo { + String a + int b + char c + short d + long e + byte f + double h + float i + boolean j + } + + new Foo("foo") + new Foo("foo", 10) + new Foo("foo", 10, (char) 20) + new Foo("foo", 10, (char) 20, (short) 30) + new Foo("foo", 10, (char) 20, (short) 30, 40L) + new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50) + new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50, 0.0) + new Foo("foo", 10, (char) 20, (short) 30, 40L, (byte) 50, 0.0, 0.0F) + """ + } + + void testOrderedConstructorOptional() { + assertScript """ + @groovy.transform.Canonical class Foo { + String bar + String baz = 'a' + } + + def foo = new Foo() + def foo0 = new Foo('c') + def foo1 = new Foo('c', 'd') + assert null == foo.bar + assert 'a' == foo.baz + assert 'c' == foo0.bar + assert 'a' == foo0.baz + assert 'c' == foo1.bar + assert 'd' == foo1.baz + """ + } + + void testCanonicalListProp() { + def objects = evaluate(""" + @groovy.transform.Canonical class HasList { + String[] letters + List nums + } + def letters = 'A,B,C'.split(',') + def nums = [1, 2] + [new HasList(letters:letters, nums:nums), + new HasList(letters, nums)] + """) + + assertEquals objects[0].hashCode(), objects[1].hashCode() + assertEquals objects[0], objects[1] + assert objects[0].letters.size() == 3 + assert objects[0].nums.size() == 2 + + objects[0].nums = [1, 2, 3] + objects[1].letters = 'D,E'.split(',') + + assert objects[0].nums.size() == 3 + assert objects[1].letters.size() == 2 + assertFalse objects[0] == objects[1] + assert !(objects[0].hashCode() == objects[1].hashCode()) + } + + void testCanonicalChangeArray() { + assertScript """ + @groovy.transform.Canonical class HasListAndMap { + Object[] foo + } + def object = new HasListAndMap() + def object2 = new HasListAndMap(['bar'] as Object[]) + + assert object != object2 + + object.foo = new Object[1] + object.foo[0] = 'bar' + + assert object == object2 + """ + } + + void testCanonicalChangeCollection() { + def object = evaluate(""" + @groovy.transform.Canonical class HasListAndMap { + List nums + Map map + } + new HasListAndMap(nums:[], map:[:]) + """) + + object.nums << 1 + object.nums.add 2 + object.map.foo = "bat" + assertEquals 2, object.nums.size() + assertEquals 1, object.map.size() + object.nums.remove 0 + assertEquals 1, object.nums.size() + } + + void testCanonicalAsMapKey() { + assertScript """ + @groovy.transform.Canonical final class HasString { + String s + } + def k1 = new HasString('xyz') + def k2 = new HasString('xyz') + def map = [(k1):42] + assert map[k2] == 42 + """ + } + + void testCanonicalWithOnlyMap() { + assertScript """ + @groovy.transform.Canonical final class HasMap { + Map map + } + def m = new HasMap([:]) + new HashMap() + """ + } + + void testCanonicalWithInvalidPropertyName() { + def msg = shouldFail(MissingPropertyException) { + assertScript """ + @groovy.transform.Canonical class Simple { } + new Simple(missing:'Name') + """ + } + assert msg.contains('No such property: missing for class: Simple') + } + + void testCanonicalWithHashMap() { + assertScript """ + @groovy.transform.Canonical final class HasHashMap { + HashMap map = [d:4] + } + assert new HasHashMap([a:1]).map == [a:1] + assert new HasHashMap(c:3).map == [c:3] + assert new HasHashMap(null).map == null + assert new HasHashMap().map == [d:4] + assert new HasHashMap([:]).map == [:] + assert new HasHashMap(map:5, c:3).map == [map:5, c:3] + assert new HasHashMap(map:[:]).map == [map:[:]] + """ + } + + void testCanonicalEquals() { + assertScript """ + @groovy.transform.Canonical class This { String value } + @groovy.transform.Canonical class That { String value } + class Other { } + + assert new This('foo') == new This("foo") + assert new This('f${"o"}o') == new This("foo") + + assert new This('foo') != new This("bar") + assert new This('foo') != new That("foo") + assert new This('foo') != new Other() + assert new Other() != new This("foo") + """ + } + + void testExistingToString() { + assertScript """ + import groovy.transform.Canonical + @Canonical class Foo { + String value + } + @Canonical class Bar { + String value + String toString() { 'zzz' + _toString() } + } + @Canonical class Baz { + String value + String toString() { 'zzz' + _toString() } + def _toString() { 'xxx' } + } + def foo = new Foo('abc') + def foo0 = new Foo('abc') + def foo1 = new Foo(value:'abc') + def bar = new Bar('abc') + def baz = new Baz('abc') + assert bar.toString() == 'zzz' + foo.toString().replaceAll('Foo', 'Bar') + assert baz.toString() == 'zzzxxx' + + assert 'Foo(abc)' == foo0.toString() + foo0.value = 'cde' + assert 'Foo(cde)' == foo0.toString() + assert 'Foo(abc)' == foo1.toString() + foo1.value = 'cde' + assert 'Foo(cde)' == foo1.toString() + """ + } + + void testExistingEquals() { + assertScript """ + import groovy.transform.Canonical + @Canonical class Foo { + String value + } + @Canonical class Bar { + String value + // doesn't follow normal conventions - for testing only + boolean equals(other) { value == 'abc' || _equals(other) } + } + @Canonical class Baz { + String value + // doesn't follow normal conventions - for testing only + boolean equals(Baz other) { value == 'abc' || _equals(other) } + def _equals(other) { false } + } + def foo1 = new Foo('abc') + def foo2 = new Foo('abc') + def foo3 = new Foo('def') + assert foo1 == foo2 + assert foo1 != foo3 + + def bar1 = new Bar('abc') + def bar2 = new Bar('abc') + def bar3 = new Bar('def') + def bar4 = new Bar('def') + assert bar1 == bar2 + assert bar1 == bar3 + assert bar3 != bar1 + + def baz1 = new Baz('abc') + def baz2 = new Baz('abc') + def baz3 = new Baz('def') + def baz4 = new Baz('def') + assert baz1 == baz2 + assert baz1 == baz3 + assert baz3 != baz1 + assert baz3 != baz4 + """ + } + + void testExistingHashCode() { + assertScript """ + import groovy.transform.Canonical + @Canonical class Foo { + String value + } + @Canonical class Bar { + String value + // doesn't follow normal conventions - for testing only + int hashCode() { value == 'abc' ? -1 : _hashCode() } + } + @Canonical class Baz { + String value + // doesn't follow normal conventions - for testing only + int hashCode() { value == 'abc' ? -1 : _hashCode() } + def _hashCode() { -100 } + } + def foo1 = new Foo('abc') + def foo2 = new Foo('abc') + assert foo1.hashCode() == foo2.hashCode() + + def bar1 = new Bar('abc') + def bar2 = new Bar('def') + def bar3 = new Bar('def') + assert bar1.hashCode() == -1 + assert bar2.hashCode() == bar3.hashCode() + + def baz1 = new Baz('abc') + def baz2 = new Baz('def') + assert baz1.hashCode() == -1 + assert baz2.hashCode() == -100 + """ + } +} Index: src/main/groovy/transform/ToString.java =================================================================== --- src/main/groovy/transform/ToString.java (revision ) +++ src/main/groovy/transform/ToString.java (revision ) @@ -0,0 +1,105 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 {@code toString()} methods in classes. + * The {@code @ToString} annotation instructs the compiler to execute an + * AST transformation which adds the necessary toString() method. + *

+ * It allows you to write classes in this shortened form: + *

+ * {@code @ToString}
+ * class Customer {
+ *     String first, last
+ *     int age
+ *     Date since = new Date()
+ *     Collection favItems
+ *     private answer = 42
+ * }
+ * println new Customer(first:'Tom', last:'Jones', age:21, favItems:['Books', 'Games'])
+ * 
+ * Which will have this output: + *
+ * Customer(Tom, Jones, 21, Wed Jul 14 23:57:14 EST 2010, [Books, Games])
+ * 
+ * There are numerous options to customize the format of the generated output. + * E.g. if you change the first annotation to: + *
+ * {@code @ToString(includeNames=true)}
+ * 
+ * Then the output will be: + *
+ * Customer(first:Tom, last:Jones, age:21, since:Wed Jul 14 23:57:50 EST 2010, favItems:[Books, Games])
+ * 
+ * Or if you change the first annotation to: + *
+ * {@code @ToString(includeNames=true,includeFields=true,excludes="since,favItems")}
+ * 
+ * Then the output will be: + *
+ * Customer(first:Tom, last:Jones, age:21, answer:42)
+ * 
+ * If you have this example: + *
+ * import groovy.transform.ToString
+ * {@code @ToString} class NamedThing {
+ *     String name
+ * }
+ * {@code @ToString}(includeNames=true,includeSuper=true)
+ * class AgedThing extends NamedThing {
+ *     int age
+ * }
+ * println new AgedThing(name:'Lassie', age:5)
+ * 
+ * Then the output will be: + *
+ * AgedThing(age:5, super:NamedThing(Lassie))
+ * 
+ * + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ToStringASTTransformation") +public @interface ToString { + /** + * Comma separated list of field and property names to exclude from generated toString + */ + String excludes() default ""; + + /** + * Whether to include super in generated toString + */ + boolean includeSuper() default false; + + /** + * Whether to include names of properties/fields in generated toString + */ + boolean includeNames() default false; + + /** + * Include fields as well as properties in generated toString + */ + boolean includeFields() default false; +} Index: src/main/groovy/transform/TupleConstructor.java =================================================================== --- src/main/groovy/transform/TupleConstructor.java (revision ) +++ src/main/groovy/transform/TupleConstructor.java (revision ) @@ -0,0 +1,69 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 tuple constructors in classes. + *

+ * It allows you to write classes in this shortened form: + *

+ * {@code @TupleConstructor} class Customer {
+ *     String first, last
+ *     int age
+ *     Date since
+ *     Collection favItems
+ * }
+ * def c1 = new Customer(first:'Tom', last:'Jones', age:21, since:new Date(), favItems:['Books', 'Games'])
+ * def c2 = new Customer('Tom', 'Jones', 21, new Date(), ['Books', 'Games'])
+ * def c3 = new Customer('Tom', 'Jones')
+ * 
+ * The {@code @TupleConstructor} annotation instructs the compiler to execute an + * AST transformation which adds the necessary toString() method. + *

+ * A tuple constructor is created with a parameter for each property (and optionally field). + * A default value is provided (using Java's default values) for all parameters in the constructor. + * Groovy then allows any number of parameters to be left off the end of the parameter list. + * + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.TupleConstructorASTTransformation") +public @interface TupleConstructor { + /** + * Comma separated list of field and property names to exclude from the constructor + */ + String excludes() default ""; + + /** + * Include fields as well as properties in the constructor + */ + boolean includeFields() default false; + + /** + * By default, this annotation becomes a no-op if you provide your own constructor. + * By setting {@code force=true} then the tuple constructor(s) will be added regardless of + * whether existing constructors exist. It is up to you to avoid creating duplicate constructors. + */ + boolean force() default false; +} Index: src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/AbstractASTTransformation.java (revision ) @@ -0,0 +1,80 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 org.codehaus.groovy.GroovyBugError; +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.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.syntax.SyntaxException; +import org.objectweb.asm.Opcodes; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class AbstractASTTransformation implements Opcodes, ASTTransformation { + protected static final ClassNode SELF_TYPE = new ClassNode(ImmutableASTTransformation.class); + protected static final ClassNode HASHMAP_TYPE = new ClassNode(HashMap.class); + protected static final ClassNode MAP_TYPE = new ClassNode(Map.class); + private SourceUnit sourceUnit; + + protected void init(ASTNode[] nodes, SourceUnit sourceUnit) { + if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { + throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes)); + } + this.sourceUnit = sourceUnit; + } + + protected boolean memberHasValue(AnnotationNode node, String name, Object value) { + final Expression member = node.getMember(name); + return member != null && member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(value); + } + + protected Object getMemberValue(AnnotationNode node, String name) { + final Expression member = node.getMember(name); + if (member != null && member instanceof ConstantExpression) return ((ConstantExpression) member).getValue(); + return null; + } + + protected void addError(String msg, ASTNode expr) { + int line = expr.getLineNumber(); + int col = expr.getColumnNumber(); + sourceUnit.getErrorCollector().addErrorAndContinue( + new SyntaxErrorMessage(new SyntaxException(msg + '\n', line, col), sourceUnit) + ); + } + + protected void checkNotInterface(ClassNode cNode, String annotationName) { + if (cNode.isInterface()) { + addError("Error processing interface '" + cNode.getName() + "'. " + + annotationName + " not allowed for interfaces.", cNode); + } + } + + protected boolean hasAnnotation(ClassNode cNode, ClassNode annotation) { + List annots = cNode.getAnnotations(annotation); + return (annots != null && annots.size() > 0); + } +} Index: src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/ToStringASTTransformation.java (revision ) @@ -0,0 +1,156 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.ToString; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.BooleanExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.ConstructorCallExpression; +import org.codehaus.groovy.ast.expr.DeclarationExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.runtime.InvokerHelper; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; + +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.hasDeclaredMethod; + +/** + * Handles generation of code for the @ToString annotation. + * + * @author Paul King + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class ToStringASTTransformation extends AbstractASTTransformation { + + static final Class MY_CLASS = ToString.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static final ClassNode STRINGBUFFER_TYPE = new ClassNode(StringBuffer.class); + private static final ClassNode INVOKER_TYPE = new ClassNode(InvokerHelper.class); + private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); + + 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; + checkNotInterface(cNode, MY_TYPE_NAME); + boolean includeSuper = memberHasValue(anno, "includeSuper", true); + if (includeSuper && cNode.getSuperClass().getName().equals("java.lang.Object")) { + addError("Error during " + MY_TYPE_NAME + " processing: includeSuper=true but '" + cNode.getName() + "' has no super class.", anno); + } + boolean includeNames = memberHasValue(anno, "includeNames", true); + boolean includeFields = memberHasValue(anno, "includeFields", true); + String rawExcludes = (String)getMemberValue(anno, "excludes"); + List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", "); + toStringInit(cNode, new ConstantExpression(includeNames)); + createToString(cNode, includeSuper, includeFields, excludes); + } + } + + public static void createToString(ClassNode cNode, boolean includeSuper, boolean includeFields, List excludes) { + // make a public method if none exists otherwise try a private method with leading underscore + boolean hasExistingToString = hasDeclaredMethod(cNode, "toString", 0); + if (hasExistingToString && hasDeclaredMethod(cNode, "_toString", 0)) return; + + final BlockStatement body = new BlockStatement(); + // def _result = new StringBuffer() + final Expression result = new VariableExpression("_result"); + final Expression init = new ConstructorCallExpression(STRINGBUFFER_TYPE, MethodCallExpression.NO_ARGUMENTS); + body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); + + body.addStatement(append(result, new ConstantExpression(cNode.getName()))); + body.addStatement(append(result, new ConstantExpression("("))); + boolean first = true; + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + for (FieldNode fNode : list) { + if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue; + first = appendPrefix(cNode, body, result, first, fNode.getName()); + body.addStatement(append(result, new StaticMethodCallExpression(INVOKER_TYPE, "toString", new FieldExpression(fNode)))); + } + if (includeSuper) { + appendPrefix(cNode, body, result, first, "super"); + // not through MOP to avoid infinite recursion + body.addStatement(append(result, new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "toString", MethodCallExpression.NO_ARGUMENTS))); + } + body.addStatement(append(result, new ConstantExpression(")"))); + body.addStatement(new ReturnStatement(new MethodCallExpression(result, "toString", MethodCallExpression.NO_ARGUMENTS))); + cNode.addMethod(new MethodNode(hasExistingToString ? "_toString" : "toString", hasExistingToString ? ACC_PRIVATE : ACC_PUBLIC, + ClassHelper.STRING_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); + } + + private static boolean appendPrefix(ClassNode cNode, BlockStatement body, Expression result, boolean first, String name) { + if (first) { + first = false; + } else { + body.addStatement(append(result, new ConstantExpression(", "))); + } + body.addStatement(new IfStatement( + new BooleanExpression(new FieldExpression(cNode.getField("$print$names"))), + toStringPropertyName(result, name), + new EmptyStatement() + )); + return first; + } + + private static Statement toStringPropertyName(Expression result, String fName) { + final BlockStatement body = new BlockStatement(); + body.addStatement(append(result, new ConstantExpression(fName))); + body.addStatement(append(result, new ConstantExpression(":"))); + return body; + } + + private static ExpressionStatement append(Expression result, Expression expr) { + return new ExpressionStatement(new MethodCallExpression(result, "append", expr)); + } + + public static void toStringInit(ClassNode cNode, ConstantExpression fieldValue) { + cNode.addField("$print$names", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.boolean_TYPE, fieldValue); + } + +} Index: src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/AutoExternalizeASTTransformation.java (revision ) @@ -0,0 +1,123 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.AutoExternalize; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields; + +/** + * Handles generation of code for the @AutoExternalize annotation. + * + * @author Paul King + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class AutoExternalizeASTTransformation extends AbstractASTTransformation { + static final Class MY_CLASS = AutoExternalize.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static final ClassNode EXTERNALIZABLE_TYPE = ClassHelper.make(Externalizable.class); + private static final ClassNode OBJECTOUTPUT_TYPE = ClassHelper.make(ObjectOutput.class); + private static final ClassNode OBJECTINPUT_TYPE = ClassHelper.make(ObjectInput.class); + private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); + + 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; + checkNotInterface(cNode, MY_TYPE_NAME); + cNode.addInterface(EXTERNALIZABLE_TYPE); + boolean includeFields = memberHasValue(anno, "includeFields", true); + String rawExcludes = (String) getMemberValue(anno, "excludes"); + List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", "); + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + createWriteExternal(cNode, excludes, list); + createReadExternal(cNode, excludes, list); + } + } + + private void createWriteExternal(ClassNode cNode, List excludes, List list) { + final BlockStatement body = new BlockStatement(); + VariableExpression out = new VariableExpression("out", OBJECTOUTPUT_TYPE); + for (FieldNode fNode : list) { + if (excludes.contains(fNode.getName())) continue; + if ((fNode.getModifiers() & ACC_TRANSIENT) != 0) continue; + body.addStatement(new ExpressionStatement(new MethodCallExpression(out, "write" + suffixForField(fNode), new FieldExpression(fNode)))); + } + ClassNode[] exceptions = {ClassHelper.make(IOException.class)}; + cNode.addMethod("writeExternal", ACC_PUBLIC, ClassHelper.VOID_TYPE, new Parameter[]{new Parameter(OBJECTOUTPUT_TYPE, "out")}, exceptions, body); + } + + private void createReadExternal(ClassNode cNode, List excludes, List list) { + final BlockStatement body = new BlockStatement(); + final Expression oin = new VariableExpression("oin", OBJECTINPUT_TYPE); + for (FieldNode fNode : list) { + if (excludes.contains(fNode.getName())) continue; + if ((fNode.getModifiers() & ACC_TRANSIENT) != 0) continue; + Expression readObject = new MethodCallExpression(oin, "read" + suffixForField(fNode), MethodCallExpression.NO_ARGUMENTS); + body.addStatement(new ExpressionStatement(new BinaryExpression(new FieldExpression(fNode), ASSIGN, readObject))); + } + cNode.addMethod("readExternal", ACC_PUBLIC, ClassHelper.VOID_TYPE, new Parameter[]{new Parameter(OBJECTINPUT_TYPE, "oin")}, ClassNode.EMPTY_ARRAY, body); + } + + private String suffixForField(FieldNode fNode) { + // use primitives for efficiently + if (fNode.getType() == ClassHelper.int_TYPE) return "Int"; + if (fNode.getType() == ClassHelper.boolean_TYPE) return "Boolean"; +// currently char isn't found due to a bug, so go with Object +// if (fNode.getType() == ClassHelper.char_TYPE) return "Char"; + if (fNode.getType() == ClassHelper.long_TYPE) return "Long"; + if (fNode.getType() == ClassHelper.short_TYPE) return "Short"; + if (fNode.getType() == ClassHelper.byte_TYPE) return "Byte"; + if (fNode.getType() == ClassHelper.float_TYPE) return "Float"; + if (fNode.getType() == ClassHelper.double_TYPE) return "Double"; + return "Object"; + } +} Index: src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/CanonicalASTTransformation.java (revision ) @@ -0,0 +1,69 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.ast.expr.ConstantExpression; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.ArrayList; + +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.ToStringASTTransformation.toStringInit; +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 = new ClassNode(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 node = (AnnotationNode) nodes[0]; + if (!MY_TYPE.equals(node.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); + } + checkNotInterface(cNode, MY_TYPE_NAME); + createConstructor(cNode, false, false, new ArrayList()); + createHashCode(cNode, false, false, false, new ArrayList()); + createEquals(cNode, false, false, new ArrayList()); + toStringInit(cNode, ConstantExpression.FALSE); + createToString(cNode, false, false, new ArrayList()); + } + } + +} Index: src/main/groovy/transform/EqualsAndHashCode.java =================================================================== --- src/main/groovy/transform/EqualsAndHashCode.java (revision ) +++ src/main/groovy/transform/EqualsAndHashCode.java (revision ) @@ -0,0 +1,72 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 creating appropriate {@code equals()} and {@code hashCode()} methods. + *

+ * It allows you to write classes in this shortened form: + *

+ * import groovy.transform.EqualsAndHashCode
+ * {@code @EqualsAndHashCode}
+ * class Person {
+ *     String first, last
+ *     int age
+ * }
+ * def p1 = new Person(first:'John', last:'Smith', age:21)
+ * def p2 = new Person(first:'John', last:'Smith', age:21)
+ * assert p1 == p2
+ * def map = [:]
+ * map[p1] = 45
+ * assert map[p2] == 45
+ * 
+ * The {@code @EqualsAndHashCode} annotation instructs the compiler to execute an + * AST transformation which adds the necessary equals and hashCode methods to the class. + *

+ * The {@code hashCode()} method is calculated using Groovy's {@code HashCodeHelper} class + * which implements an algorithm similar to the outlined in the book Effective Java. + *

+ * The {@code equals()} method compares the values of the individual properties of the class. + * + * @see org.codehaus.groovy.util.HashCodeHelper + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.EqualsAndHashCodeASTTransformation") +public @interface EqualsAndHashCode { + /** + * Comma separated list of field and property names to exclude from equals and hashCode calculations + */ + String excludes() default ""; + + /** + * Whether to include super in equals and hashCode calculations + */ + boolean callSuper() default false; + + /** + * Include fields as well as properties in equals and hashCode calculations + */ + boolean includeFields() default false; +} \ No newline at end of file Index: src/main/groovy/lang/Immutable.java =================================================================== --- src/main/groovy/lang/Immutable.java (revision 19660) +++ src/main/groovy/lang/Immutable.java (revision ) @@ -24,6 +24,8 @@ import java.lang.annotation.Target; /** + * Note: This class is Deprecated, please use groovy.transform.Immutable. + *

* Class annotation used to assist in the creation of immutable classes. *

* It allows you to write classes in this shortened form: @@ -39,7 +41,7 @@ * def c2 = new Customer('Tom', 'Jones', 21, d, ['Books', 'Games']) * assert c1 == c2 * - * The @Immutable annotation instructs the compiler to execute an + * 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. @@ -49,7 +51,7 @@ *

  • The class is automatically made final. *
  • 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 @Immutable classes or known immutables (e.g. java.awt.Color, java.net.URI). Also handled are + * other {@code @Immutable} classes or known immutables (e.g. java.awt.Color, java.net.URI). Also handled are * Cloneable classes, collections, maps and arrays, and other "effectively immutable" classes with * special handling (e.g. java.util.Date). *
  • Properties automatically have private, final backing fields with getters. @@ -113,9 +115,11 @@ * * * @author Paul King + * @deprecated use groovy.transform.Immutable */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation") +@Deprecated public @interface Immutable { } Index: src/main/groovy/transform/Immutable.java =================================================================== --- src/main/groovy/transform/Immutable.java (revision ) +++ src/main/groovy/transform/Immutable.java (revision ) @@ -0,0 +1,121 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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. + *

    + * It allows you to write classes in this shortened form: + *

    + * {@code @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
    + * 
    + * 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. + *

    + * A class created in this way has the following characteristics: + *

    + *

    + * Immutable classes are particularly useful for functional and concurrent styles of programming + * and for use as key values within maps. + *

    + * Limitations: + *

    + * + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.ImmutableASTTransformation") +public @interface Immutable { +} Index: src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy =================================================================== --- src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy (revision 20003) +++ src/test/org/codehaus/groovy/transform/ImmutableTransformTest.groovy (revision ) @@ -1,5 +1,5 @@ /* - * Copyright 2008-2009 the original author or authors. + * Copyright 2008-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,31 +22,47 @@ void testImmutable() { def objects = evaluate(""" + import groovy.transform.Immutable - enum Coin { HEAD, TAIL } - @Immutable class Bar { - String x, y - Coin c - Collection nums - } - [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]), - new Bar('x', 'y', Coin.HEAD, [1,2])] + enum Coin { HEAD, TAIL } + @Immutable class Bar { + String x, y + Coin c + Collection nums + } + [new Bar(x:'x', y:'y', c:Coin.HEAD, nums:[1,2]), + new Bar('x', 'y', Coin.HEAD, [1,2])] """) assertEquals objects[0].hashCode(), objects[1].hashCode() assertEquals objects[0], objects[1] assertTrue objects[0].nums.class.name.contains("Unmodifiable") } - + + 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 - @Immutable class HasList { - String[] letters - List nums - } - def letters = 'A,B,C'.split(',') - def nums = [1, 2] - [new HasList(letters:letters, nums:nums), - new HasList(letters, nums)] + @Immutable class HasList { + String[] letters + List nums + } + def letters = 'A,B,C'.split(',') + def nums = [1, 2] + [new HasList(letters:letters, nums:nums), + new HasList(letters, nums)] """) assertEquals objects[0].hashCode(), objects[1].hashCode() @@ -57,6 +73,7 @@ void testImmutableAsMapKey() { assertScript """ + import groovy.transform.Immutable @Immutable final class HasString { String s } @@ -69,6 +86,7 @@ void testImmutableWithOnlyMap() { assertScript """ + import groovy.transform.Immutable @Immutable final class HasMap { Map map } @@ -79,6 +97,7 @@ void testImmutableWithInvalidPropertyName() { def msg = shouldFail(MissingPropertyException) { assertScript """ + import groovy.transform.Immutable @Immutable class Simple { } new Simple(missing:'Name') """ @@ -88,6 +107,7 @@ void testImmutableWithHashMap() { assertScript """ + import groovy.transform.Immutable @Immutable final class HasHashMap { HashMap map = [d:4] } @@ -105,6 +125,7 @@ void testImmutableEquals() { assertScript """ + import groovy.transform.Immutable @Immutable class This { String value } @Immutable class That { String value } class Other { } @@ -121,6 +142,7 @@ void testExistingToString() { assertScript """ + import groovy.transform.Immutable @Immutable class Foo { String value } @@ -143,6 +165,7 @@ void testExistingEquals() { assertScript """ + import groovy.transform.Immutable @Immutable class Foo { String value } @@ -184,6 +207,7 @@ void testExistingHashCode() { assertScript """ + import groovy.transform.Immutable @Immutable class Foo { String value } @@ -218,6 +242,7 @@ void testStaticsAllowed_ThoughUsuallyBadDesign() { // design here is questionable as getDescription() method is not idempotent assertScript ''' + import groovy.transform.Immutable @Immutable class Person { String first, last static species = 'Human' @@ -243,4 +268,4 @@ assert spock.description == 'Leonard Nimoy is a Vulcan' ''' } -} \ No newline at end of file +} Index: src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java (revision ) @@ -0,0 +1,118 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.TupleConstructor; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.assignStatement; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields; + +/** + * Handles generation of code for the @TupleConstructor annotation. + * + * @author Paul King + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class TupleConstructorASTTransformation extends AbstractASTTransformation { + + static final Class MY_CLASS = TupleConstructor.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static Map, Expression> primitivesInitialValues; + + static { + final ConstantExpression zero = new ConstantExpression(0); + final ConstantExpression zeroDecimal = new ConstantExpression(.0); + primitivesInitialValues = new HashMap, Expression>(); + primitivesInitialValues.put(int.class, zero); + primitivesInitialValues.put(long.class, zero); + primitivesInitialValues.put(short.class, zero); + primitivesInitialValues.put(byte.class, zero); + primitivesInitialValues.put(char.class, zero); + primitivesInitialValues.put(float.class, zeroDecimal); + primitivesInitialValues.put(double.class, zeroDecimal); + primitivesInitialValues.put(boolean.class, ConstantExpression.FALSE); + } + + 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; + checkNotInterface(cNode, MY_TYPE_NAME); + boolean includeFields = memberHasValue(anno, "includeFields", true); + boolean force = memberHasValue(anno, "force", true); + String rawExcludes = (String)getMemberValue(anno, "excludes"); + List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", "); + createConstructor(cNode, includeFields, force, excludes); + } + } + + public static void createConstructor(ClassNode cNode, boolean includeFields, boolean force, List excludes) { + // no processing if existing constructors found + if (cNode.getDeclaredConstructors().size() != 0 && !force) return; + + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + final List params = new ArrayList(); + final BlockStatement body = new BlockStatement(); + for (FieldNode fNode : list) { + if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue; + Parameter param = new Parameter(fNode.getType(), fNode.getName()); + param.setInitialExpression(providedOrDefaultInitialValue(fNode)); + params.add(param); + body.addStatement(assignStatement(new FieldExpression(fNode), new VariableExpression(param.getName()))); + } + cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, params.toArray(new Parameter[params.size()]), ClassNode.EMPTY_ARRAY, body)); + } + + private static Expression providedOrDefaultInitialValue(FieldNode fNode) { + Expression initialExp = fNode.getInitialExpression() != null ? fNode.getInitialExpression() : ConstantExpression.NULL; + final ClassNode paramType = fNode.getType(); + if (ClassHelper.isPrimitiveType(paramType) && initialExp.equals(ConstantExpression.NULL)) { + initialExp = primitivesInitialValues.get(paramType.getTypeClass()); + } + return initialExp; + } + +} Index: src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java (revision 20420) +++ src/main/org/codehaus/groovy/transform/ImmutableASTTransformation.java (revision ) @@ -15,30 +15,47 @@ */ package org.codehaus.groovy.transform; -import groovy.lang.Immutable; import groovy.lang.MetaClass; import groovy.lang.MissingPropertyException; -import org.codehaus.groovy.ast.*; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.expr.*; -import org.codehaus.groovy.ast.stmt.*; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.InvokerHelper; -import org.codehaus.groovy.syntax.Token; -import org.codehaus.groovy.syntax.Types; -import org.codehaus.groovy.util.HashCodeHelper; -import org.objectweb.asm.Opcodes; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.*; +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 */ @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) -public class ImmutableASTTransformation implements ASTTransformation, Opcodes { +public class ImmutableASTTransformation extends AbstractASTTransformation { /* Currently leaving BigInteger and BigDecimal in list but see: @@ -62,49 +79,32 @@ "java.awt.Color", "java.net.URI" ); - private static final Class MY_CLASS = Immutable.class; - private static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); - private static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); - private static final ClassNode OBJECT_TYPE = new ClassNode(Object.class); - private static final ClassNode HASHMAP_TYPE = new ClassNode(HashMap.class); - private static final ClassNode MAP_TYPE = new ClassNode(Map.class); + private static final Class MY_CLASS = groovy.transform.Immutable.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); private static final ClassNode DATE_TYPE = new ClassNode(Date.class); private static final ClassNode CLONEABLE_TYPE = new ClassNode(Cloneable.class); private static final ClassNode COLLECTION_TYPE = new ClassNode(Collection.class); - private static final ClassNode HASHUTIL_TYPE = new ClassNode(HashCodeHelper.class); - private static final ClassNode STRINGBUFFER_TYPE = new ClassNode(StringBuffer.class); private static final ClassNode DGM_TYPE = new ClassNode(DefaultGroovyMethods.class); - private static final ClassNode INVOKER_TYPE = new ClassNode(InvokerHelper.class); - private static final ClassNode SELF_TYPE = new ClassNode(ImmutableASTTransformation.class); - private static final Token COMPARE_EQUAL = Token.newSymbol(Types.COMPARE_EQUAL, -1, -1); - private static final Token COMPARE_NOT_EQUAL = Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1); - private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); public void visit(ASTNode[] nodes, SourceUnit source) { - if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { - throw new RuntimeException("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes)); - } - + init(nodes, source); AnnotatedNode parent = (AnnotatedNode) nodes[1]; AnnotationNode node = (AnnotationNode) nodes[0]; if (!MY_TYPE.equals(node.getClassNode())) return; - List newNodes = new ArrayList(); + List newProperties = new ArrayList(); if (parent instanceof ClassNode) { ClassNode cNode = (ClassNode) parent; String cName = cNode.getName(); - if (cNode.isInterface()) { - throw new RuntimeException("Error processing interface '" + cName + "'. " + MY_TYPE_NAME + " not allowed for interfaces."); - } - if ((cNode.getModifiers() & ACC_FINAL) == 0) { - cNode.setModifiers(cNode.getModifiers() | ACC_FINAL); - } + checkNotInterface(cNode, MY_TYPE_NAME); + makeClassFinal(cNode); final List pList = getInstanceProperties(cNode); for (PropertyNode pNode : pList) { - adjustPropertyForImmutability(pNode, newNodes); + adjustPropertyForImmutability(pNode, newProperties); } - for (PropertyNode pNode : newNodes) { + for (PropertyNode pNode : newProperties) { cNode.getProperties().remove(pNode); addProperty(cNode, pNode); } @@ -112,199 +112,24 @@ for (FieldNode fNode : fList) { ensureNotPublic(cName, fNode); } - createConstructor(cNode); - createHashCode(cNode); - createEquals(cNode); - createToString(cNode); + createConstructors(cNode); + createHashCode(cNode, true, false, false, new ArrayList()); + createEquals(cNode, false, false, new ArrayList()); + createToString(cNode, false, false, new ArrayList()); } } - private boolean hasDeclaredMethod(ClassNode cNode, String name, int argsCount) { - List ms = cNode.getDeclaredMethods(name); - for(MethodNode m : ms) { - Parameter[] paras = m.getParameters(); - if(paras != null && paras.length == argsCount) { - return true; + private void makeClassFinal(ClassNode cNode) { + if ((cNode.getModifiers() & ACC_FINAL) == 0) { + cNode.setModifiers(cNode.getModifiers() | ACC_FINAL); - } - } + } + } - return false; - } - private void ensureNotPublic(String cNode, FieldNode fNode) { - String fName = fNode.getName(); - // TODO: do we need to lock down things like: $ownClass - if (fNode.isPublic() && !fName.contains("$")) { - throw new RuntimeException("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'."); - } - } - - private void createHashCode(ClassNode cNode) { - // make a public method if none exists otherwise try a private method with leading underscore - boolean hasExistingHashCode = hasDeclaredMethod(cNode, "hashCode", 0); - if (hasExistingHashCode && hasDeclaredMethod(cNode, "_hashCode", 0)) return; - - final FieldNode hashField = cNode.addField("$hash$code", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.int_TYPE, null); - final BlockStatement body = new BlockStatement(); - final Expression hash = new FieldExpression(hashField); - final List list = getInstanceProperties(cNode); - - body.addStatement(new IfStatement( - isZeroExpr(hash), - calculateHashStatements(hash, list), - new EmptyStatement() - )); - - body.addStatement(new ReturnStatement(hash)); - - cNode.addMethod(new MethodNode(hasExistingHashCode ? "_hashCode" : "hashCode", hasExistingHashCode ? ACC_PRIVATE : ACC_PUBLIC, - ClassHelper.int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); - } - - private void createToString(ClassNode cNode) { - // make a public method if none exists otherwise try a private method with leading underscore - boolean hasExistingToString = hasDeclaredMethod(cNode, "toString", 0); - if (hasExistingToString && hasDeclaredMethod(cNode, "_toString", 0)) return; - - final BlockStatement body = new BlockStatement(); - final List list = getInstanceProperties(cNode); - // def _result = new StringBuffer() - final Expression result = new VariableExpression("_result"); - final Expression init = new ConstructorCallExpression(STRINGBUFFER_TYPE, MethodCallExpression.NO_ARGUMENTS); - body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); - - body.addStatement(append(result, new ConstantExpression(cNode.getName()))); - body.addStatement(append(result, new ConstantExpression("("))); - boolean first = true; - for (PropertyNode pNode : list) { - if (first) { - first = false; - } else { - body.addStatement(append(result, new ConstantExpression(", "))); - } - body.addStatement(new IfStatement( - new BooleanExpression(new FieldExpression(cNode.getField("$map$constructor"))), - toStringPropertyName(result, pNode.getName()), - new EmptyStatement() - )); - final FieldExpression fieldExpr = new FieldExpression(pNode.getField()); - body.addStatement(append(result, new StaticMethodCallExpression(INVOKER_TYPE, "toString", fieldExpr))); - } - body.addStatement(append(result, new ConstantExpression(")"))); - body.addStatement(new ReturnStatement(new MethodCallExpression(result, "toString", MethodCallExpression.NO_ARGUMENTS))); - cNode.addMethod(new MethodNode(hasExistingToString ? "_toString" : "toString", hasExistingToString ? ACC_PRIVATE : ACC_PUBLIC, - ClassHelper.STRING_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); - } - - private Statement toStringPropertyName(Expression result, String fName) { - final BlockStatement body = new BlockStatement(); - body.addStatement(append(result, new ConstantExpression(fName))); - body.addStatement(append(result, new ConstantExpression(":"))); - return body; - } - - private ExpressionStatement append(Expression result, Expression expr) { - return new ExpressionStatement(new MethodCallExpression(result, "append", expr)); - } - - private Statement calculateHashStatements(Expression hash, List list) { - final BlockStatement body = new BlockStatement(); - // def _result = HashCodeHelper.initHash() - final Expression result = new VariableExpression("_result"); - final Expression init = new StaticMethodCallExpression(HASHUTIL_TYPE, "initHash", MethodCallExpression.NO_ARGUMENTS); - body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); - - // fields - for (PropertyNode pNode : list) { - // _result = HashCodeHelper.updateHash(_result, field) - final Expression fieldExpr = new FieldExpression(pNode.getField()); - final Expression args = new TupleExpression(result, fieldExpr); - final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args); - body.addStatement(assignStatement(result, current)); - } - // $hash$code = _result - body.addStatement(assignStatement(hash, result)); - return body; - } - - private void createEquals(ClassNode cNode) { - // make a public method if none exists otherwise try a private method with leading underscore - boolean hasExistingEquals = hasDeclaredMethod(cNode, "equals", 1); - if (hasExistingEquals && hasDeclaredMethod(cNode, "_equals", 1)) return; - - final BlockStatement body = new BlockStatement(); - Expression other = new VariableExpression("other"); - - // some short circuit cases for efficiency - body.addStatement(returnFalseIfNull(other)); - body.addStatement(returnFalseIfWrongType(cNode, other)); - body.addStatement(returnTrueIfIdentical(VariableExpression.THIS_EXPRESSION, other)); - - body.addStatement(new ExpressionStatement(new BinaryExpression(other, ASSIGN, new CastExpression(cNode, other)))); - - final List list = getInstanceProperties(cNode); - // fields - for (PropertyNode pNode : list) { - body.addStatement(returnFalseIfPropertyNotEqual(pNode, other)); - } - - // default - body.addStatement(new ReturnStatement(ConstantExpression.TRUE)); - - Parameter[] params = {new Parameter(OBJECT_TYPE, "other")}; - cNode.addMethod(new MethodNode(hasExistingEquals ? "_equals" : "equals", hasExistingEquals ? ACC_PRIVATE : ACC_PUBLIC, - ClassHelper.boolean_TYPE, params, ClassNode.EMPTY_ARRAY, body)); - } - - private Statement returnFalseIfWrongType(ClassNode cNode, Expression other) { - return new IfStatement( - notEqualClasses(cNode, other), - new ReturnStatement(ConstantExpression.FALSE), - new EmptyStatement() - ); - } - - private IfStatement returnFalseIfNull(Expression other) { - return new IfStatement( - equalsNullExpr(other), - new ReturnStatement(ConstantExpression.FALSE), - new EmptyStatement() - ); - } - - private IfStatement returnTrueIfIdentical(Expression self, Expression other) { - return new IfStatement( - identicalExpr(self, other), - new ReturnStatement(ConstantExpression.TRUE), - new EmptyStatement() - ); - } - - private Statement returnFalseIfPropertyNotEqual(PropertyNode pNode, Expression other) { - return new IfStatement( - notEqualsExpr(pNode, other), - new ReturnStatement(ConstantExpression.FALSE), - new EmptyStatement() - ); - } - - private void addProperty(ClassNode cNode, PropertyNode pNode) { - final FieldNode fn = pNode.getField(); - cNode.getFields().remove(fn); - cNode.addProperty(pNode.getName(), pNode.getModifiers() | ACC_FINAL, pNode.getType(), - pNode.getInitialExpression(), pNode.getGetterBlock(), pNode.getSetterBlock()); - final FieldNode newfn = cNode.getField(fn.getName()); - cNode.getFields().remove(newfn); - cNode.addField(fn); - } - - private void createConstructor(ClassNode cNode) { + private void createConstructors(ClassNode cNode) { // pretty toString will remember how the user declared the params and print accordingly - final FieldNode constructorField = cNode.addField("$map$constructor", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.boolean_TYPE, null); + final FieldNode constructorField = cNode.addField("$print$names", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.boolean_TYPE, null); final FieldExpression constructorStyle = new FieldExpression(constructorField); - if (cNode.getDeclaredConstructors().size() != 0) { - // TODO: allow constructors which call provided constructor? - throw new RuntimeException("Explicit constructors not allowed for " + MY_TYPE_NAME + " class: " + cNode.getNameWithoutPackage()); - } + if (!validateConstructors(cNode)) return; List list = getInstanceProperties(cNode); boolean specialHashMapCase = list.size() == 1 && list.get(0).getField().getType().equals(HASHMAP_TYPE); @@ -316,16 +141,36 @@ } } - private List getInstanceProperties(ClassNode cNode) { - final List result = new ArrayList(); - for (PropertyNode pNode : cNode.getProperties()) { - if (!pNode.isStatic()) { - result.add(pNode); + private void createConstructorOrdered(ClassNode cNode, FieldExpression constructorStyle, List list) { + final MapExpression argMap = new MapExpression(); + final Parameter[] orderedParams = new Parameter[list.size()]; + int index = 0; + for (PropertyNode pNode : list) { + Parameter param = new Parameter(pNode.getField().getType(), pNode.getField().getName()); + orderedParams[index++] = param; + argMap.addMapEntryExpression(new ConstantExpression(pNode.getName()), new VariableExpression(pNode.getName())); - } + } + final BlockStatement orderedBody = new BlockStatement(); + orderedBody.addStatement(new ExpressionStatement( + new ConstructorCallExpression(ClassNode.THIS, new ArgumentListExpression(new CastExpression(HASHMAP_TYPE, argMap))) + )); + orderedBody.addStatement(assignStatement(constructorStyle, ConstantExpression.FALSE)); + cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, orderedParams, ClassNode.EMPTY_ARRAY, orderedBody)); - } + } - return result; + + private Statement createGetterBodyDefault(FieldNode fNode) { + final Expression fieldExpr = new FieldExpression(fNode); + return new ExpressionStatement(fieldExpr); } + private Expression cloneCollectionExpr(Expression fieldExpr) { + return new StaticMethodCallExpression(DGM_TYPE, "asImmutable", fieldExpr); + } + + private Expression cloneArrayOrCloneableExpr(Expression fieldExpr) { + return new MethodCallExpression(fieldExpr, "clone", MethodCallExpression.NO_ARGUMENTS); + } + private void createConstructorMapSpecial(ClassNode cNode, FieldExpression constructorStyle, List list) { final BlockStatement body = new BlockStatement(); body.addStatement(createConstructorStatementMapSpecial(list.get(0).getField())); @@ -358,26 +203,62 @@ body))); } - private void createConstructorOrdered(ClassNode cNode, FieldExpression constructorStyle, List list) { - final MapExpression argMap = new MapExpression(); - final Parameter[] orderedParams = new Parameter[list.size()]; - int index = 0; - for (PropertyNode pNode : list) { - orderedParams[index++] = new Parameter(pNode.getField().getType(), pNode.getField().getName()); - argMap.addMapEntryExpression(new ConstantExpression(pNode.getName()), new VariableExpression(pNode.getName())); + private Statement createConstructorStatementMapSpecial(FieldNode fNode) { + final FieldExpression fieldExpr = new FieldExpression(fNode); + Expression initExpr = fNode.getInitialValueExpression(); + if (initExpr == null) initExpr = ConstantExpression.NULL; + Expression namedArgs = findArg(fNode.getName()); + Expression baseArgs = new VariableExpression("args"); + return new IfStatement( + equalsNullExpr(baseArgs), + new IfStatement( + equalsNullExpr(initExpr), + new EmptyStatement(), + assignStatement(fieldExpr, cloneCollectionExpr(initExpr))), + new IfStatement( + equalsNullExpr(namedArgs), + new IfStatement( + isTrueExpr(new MethodCallExpression(baseArgs, "containsKey", new ConstantExpression(fNode.getName()))), + assignStatement(fieldExpr, namedArgs), + assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))), + new IfStatement( + isOneExpr(new MethodCallExpression(baseArgs, "size", MethodCallExpression.NO_ARGUMENTS)), + assignStatement(fieldExpr, cloneCollectionExpr(namedArgs)), + assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))) + ) + ); - } + } - final BlockStatement orderedBody = new BlockStatement(); - orderedBody.addStatement(new ExpressionStatement( - new ConstructorCallExpression(ClassNode.THIS, new ArgumentListExpression(new CastExpression(HASHMAP_TYPE, argMap))) - )); - orderedBody.addStatement(assignStatement(constructorStyle, ConstantExpression.FALSE)); - cNode.addConstructor(new ConstructorNode(ACC_PUBLIC, orderedParams, ClassNode.EMPTY_ARRAY, orderedBody)); + + private void ensureNotPublic(String cNode, FieldNode fNode) { + String fName = fNode.getName(); + // TODO: do we need to lock down things like: $ownClass + if (fNode.isPublic() && !fName.contains("$")) { + addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode); - } + } + } + private void addProperty(ClassNode cNode, PropertyNode pNode) { + final FieldNode fn = pNode.getField(); + cNode.getFields().remove(fn); + cNode.addProperty(pNode.getName(), pNode.getModifiers() | ACC_FINAL, pNode.getType(), + pNode.getInitialExpression(), pNode.getGetterBlock(), pNode.getSetterBlock()); + final FieldNode newfn = cNode.getField(fn.getName()); + cNode.getFields().remove(newfn); + cNode.addField(fn); + } + + private boolean validateConstructors(ClassNode cNode) { + if (cNode.getDeclaredConstructors().size() != 0) { + // TODO: allow constructors which only call provided constructor? + addError("Explicit constructors not allowed for " + ImmutableASTTransformation.MY_TYPE_NAME + " class: " + cNode.getNameWithoutPackage(), cNode.getDeclaredConstructors().get(0)); + } + return true; + } + private Statement createConstructorStatement(ClassNode cNode, PropertyNode pNode) { FieldNode fNode = pNode.getField(); final ClassNode fieldType = fNode.getType(); - final Statement statement; + Statement statement = null; if (fieldType.isArray() || fieldType.implementsInterface(CLONEABLE_TYPE)) { statement = createConstructorStatementArrayOrCloneable(fNode); } else if (fieldType.isDerivedFrom(DATE_TYPE)) { @@ -387,17 +268,13 @@ } else if (isKnownImmutable(fieldType)) { statement = createConstructorStatementDefault(fNode); } else if (fieldType.isResolved()) { - throw new RuntimeException(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling")); + addError(createErrorMessage(cNode.getName(), fNode.getName(), fieldType.getName(), "compiling"), fNode); } else { statement = createConstructorStatementGuarded(cNode, fNode); } return statement; } - private boolean isOrImplements(ClassNode fieldType, ClassNode interfaceType) { - return fieldType.equals(interfaceType) || fieldType.implementsInterface(interfaceType); - } - private Statement createConstructorStatementGuarded(ClassNode cNode, FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); @@ -431,32 +308,6 @@ assignStatement(fieldExpr, cloneCollectionExpr(collection))); } - private Statement createConstructorStatementMapSpecial(FieldNode fNode) { - final FieldExpression fieldExpr = new FieldExpression(fNode); - Expression initExpr = fNode.getInitialValueExpression(); - if (initExpr == null) initExpr = ConstantExpression.NULL; - Expression namedArgs = findArg(fNode.getName()); - Expression baseArgs = new VariableExpression("args"); - return new IfStatement( - equalsNullExpr(baseArgs), - new IfStatement( - equalsNullExpr(initExpr), - new EmptyStatement(), - assignStatement(fieldExpr, cloneCollectionExpr(initExpr))), - new IfStatement( - equalsNullExpr(namedArgs), - new IfStatement( - isTrueExpr(new MethodCallExpression(baseArgs, "containsKey", new ConstantExpression(fNode.getName()))), - assignStatement(fieldExpr, namedArgs), - assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))), - new IfStatement( - isOneExpr(new MethodCallExpression(baseArgs, "size", MethodCallExpression.NO_ARGUMENTS)), - assignStatement(fieldExpr, cloneCollectionExpr(namedArgs)), - assignStatement(fieldExpr, cloneCollectionExpr(baseArgs))) - ) - ); - } - private boolean isKnownImmutable(ClassNode fieldType) { if (!fieldType.isResolved()) return false; return fieldType.isEnum() || @@ -468,20 +319,6 @@ return immutableList.contains(typeName); } - private Statement createConstructorStatementDefault(FieldNode fNode) { - final FieldExpression fieldExpr = new FieldExpression(fNode); - Expression initExpr = fNode.getInitialValueExpression(); - if (initExpr == null) initExpr = ConstantExpression.NULL; - Expression value = findArg(fNode.getName()); - return new IfStatement( - equalsNullExpr(value), - new IfStatement( - equalsNullExpr(initExpr), - new EmptyStatement(), - assignStatement(fieldExpr, initExpr)), - assignStatement(fieldExpr, value)); - } - private Statement createConstructorStatementArrayOrCloneable(FieldNode fNode) { final FieldExpression fieldExpr = new FieldExpression(fNode); Expression initExpr = fNode.getInitialValueExpression(); @@ -515,49 +352,6 @@ new MethodCallExpression(origDate, "getTime", MethodCallExpression.NO_ARGUMENTS)); } - private Statement assignStatement(Expression fieldExpr, Expression value) { - return new ExpressionStatement(assignExpr(fieldExpr, value)); - } - - private Expression assignExpr(Expression fieldExpr, Expression value) { - return new BinaryExpression(fieldExpr, ASSIGN, value); - } - - private BooleanExpression equalsNullExpr(Expression argExpr) { - return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.NULL)); - } - - private BooleanExpression isTrueExpr(Expression argExpr) { - return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.TRUE)); - } - - private BooleanExpression isZeroExpr(Expression expr) { - return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(0))); - } - - private BooleanExpression isOneExpr(Expression expr) { - return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(1))); - } - - private BooleanExpression notEqualsExpr(PropertyNode pNode, Expression other) { - final Expression fieldExpr = new FieldExpression(pNode.getField()); - final Expression otherExpr = new PropertyExpression(other, pNode.getField().getName()); - return new BooleanExpression(new BinaryExpression(fieldExpr, COMPARE_NOT_EQUAL, otherExpr)); - } - - private BooleanExpression identicalExpr(Expression self, Expression other) { - return new BooleanExpression(new MethodCallExpression(self, "is", new ArgumentListExpression(other))); - } - - private BooleanExpression notEqualClasses(ClassNode cNode, Expression other) { - return new BooleanExpression(new BinaryExpression(new ClassExpression(cNode), COMPARE_NOT_EQUAL, - new MethodCallExpression(other, "getClass", MethodCallExpression.NO_ARGUMENTS))); - } - - private Expression findArg(String fName) { - return new PropertyExpression(new VariableExpression("args"), fName); - } - private void adjustPropertyForImmutability(PropertyNode pNode, List newNodes) { final FieldNode fNode = pNode.getField(); fNode.setModifiers((pNode.getModifiers() & (~ACC_PUBLIC)) | ACC_FINAL | ACC_PRIVATE); @@ -585,16 +379,10 @@ return body; } - private Statement createGetterBodyDefault(FieldNode fNode) { - final Expression fieldExpr = new FieldExpression(fNode); - return new ExpressionStatement(fieldExpr); - } - 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 currently only support properties with known immutable types " + - "or types where special handling achieves immutable behavior, including:\n" + + MY_TYPE_NAME + " classes only support properties with effectively immutable types including:\n" + "- Strings, primitive types, wrapper types, BigInteger and BigDecimal, enums\n" + "- other " + MY_TYPE_NAME + " classes 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" + @@ -611,27 +399,12 @@ return safeExpression(fieldExpr, expression); } - private Expression cloneArrayOrCloneableExpr(Expression fieldExpr) { - return new MethodCallExpression(fieldExpr, "clone", MethodCallExpression.NO_ARGUMENTS); - } - - private Expression cloneCollectionExpr(Expression fieldExpr) { - return new StaticMethodCallExpression(DGM_TYPE, "asImmutable", fieldExpr); - } - private Statement createGetterBodyDate(FieldNode fNode) { final Expression fieldExpr = new FieldExpression(fNode); final Expression expression = cloneDateExpr(fieldExpr); return safeExpression(fieldExpr, expression); } - private Statement safeExpression(Expression fieldExpr, Expression expression) { - return new IfStatement( - equalsNullExpr(fieldExpr), - new ExpressionStatement(fieldExpr), - new ExpressionStatement(expression)); - } - 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); Index: src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java =================================================================== --- src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java (revision ) +++ src/main/org/codehaus/groovy/transform/AbstractASTTransformUtil.java (revision ) @@ -0,0 +1,197 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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 org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; +import org.objectweb.asm.Opcodes; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractASTTransformUtil implements Opcodes { + private static final Token COMPARE_EQUAL = Token.newSymbol(Types.COMPARE_EQUAL, -1, -1); + private static final Token COMPARE_NOT_EQUAL = Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1); + private static final Token INSTANCEOF = Token.newSymbol(Types.KEYWORD_INSTANCEOF, -1, -1); + private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); + + public static boolean hasDeclaredMethod(ClassNode cNode, String name, int argsCount) { + List ms = cNode.getDeclaredMethods(name); + for (MethodNode m : ms) { + Parameter[] paras = m.getParameters(); + if (paras != null && paras.length == argsCount) { + return true; + } + } + return false; + } + + public static Statement returnFalseIfWrongType(ClassNode cNode, Expression other) { + return new IfStatement( + notEqualClasses(cNode, other), + new ReturnStatement(ConstantExpression.FALSE), + new EmptyStatement() + ); + } + + public static IfStatement returnFalseIfNull(Expression other) { + return new IfStatement( + equalsNullExpr(other), + new ReturnStatement(ConstantExpression.FALSE), + new EmptyStatement() + ); + } + + public static IfStatement returnTrueIfIdentical(Expression self, Expression other) { + return new IfStatement( + identicalExpr(self, other), + new ReturnStatement(ConstantExpression.TRUE), + new EmptyStatement() + ); + } + + public static Statement returnFalseIfPropertyNotEqual(FieldNode fNode, Expression other) { + return new IfStatement( + notEqualsExpr(fNode, other), + new ReturnStatement(ConstantExpression.FALSE), + new EmptyStatement() + ); + } + + public static List getInstanceProperties(ClassNode cNode) { + final List result = new ArrayList(); + for (PropertyNode pNode : cNode.getProperties()) { + if (!pNode.isStatic()) { + result.add(pNode); + } + } + return result; + } + + public static List getInstancePropertyFields(ClassNode cNode) { + final List result = new ArrayList(); + for (PropertyNode pNode : cNode.getProperties()) { + if (!pNode.isStatic()) { + result.add(pNode.getField()); + } + } + return result; + } + + public static List getInstanceNonPropertyFields(ClassNode cNode) { + final List result = new ArrayList(); + for (FieldNode fNode : cNode.getFields()) { + if (!fNode.isStatic() && cNode.getProperty(fNode.getName()) == null) { + result.add(fNode); + } + } + return result; + } + + public static List getInstanceFields(ClassNode cNode) { + final List result = new ArrayList(); + for (FieldNode fNode : cNode.getFields()) { + if (!fNode.isStatic()) { + result.add(fNode); + } + } + return result; + } + + public static Statement assignStatement(Expression fieldExpr, Expression value) { + return new ExpressionStatement(assignExpr(fieldExpr, value)); + } + + private static Expression assignExpr(Expression expression, Expression value) { + return new BinaryExpression(expression, ASSIGN, value); + } + + public static BooleanExpression isInstanceOf(Expression objectExpression, ClassNode cNode) { + return new BooleanExpression(new BinaryExpression(objectExpression, INSTANCEOF, new ClassExpression(cNode))); + } + + public static BooleanExpression equalsNullExpr(Expression argExpr) { + return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.NULL)); + } + + public static BooleanExpression isZeroExpr(Expression expr) { + return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(0))); + } + + private static BooleanExpression notEqualsExpr(FieldNode fNode, Expression other) { + final Expression fieldExpr = new FieldExpression(fNode); + final Expression otherExpr = new PropertyExpression(other, fNode.getName()); + return new BooleanExpression(new BinaryExpression(fieldExpr, COMPARE_NOT_EQUAL, otherExpr)); + } + + private static BooleanExpression identicalExpr(Expression self, Expression other) { + return new BooleanExpression(new MethodCallExpression(self, "is", new ArgumentListExpression(other))); + } + + private static BooleanExpression notEqualClasses(ClassNode cNode, Expression other) { + return new BooleanExpression(new BinaryExpression(new ClassExpression(cNode), COMPARE_NOT_EQUAL, + new MethodCallExpression(other, "getClass", MethodCallExpression.NO_ARGUMENTS))); + } + + public static boolean isOrImplements(ClassNode fieldType, ClassNode interfaceType) { + return fieldType.equals(interfaceType) || fieldType.implementsInterface(interfaceType); + } + + public static BooleanExpression isTrueExpr(Expression argExpr) { + return new BooleanExpression(new BinaryExpression(argExpr, COMPARE_EQUAL, ConstantExpression.TRUE)); + } + + public static BooleanExpression isOneExpr(Expression expr) { + return new BooleanExpression(new BinaryExpression(expr, COMPARE_EQUAL, new ConstantExpression(1))); + } + + public static Statement safeExpression(Expression fieldExpr, Expression expression) { + return new IfStatement( + equalsNullExpr(fieldExpr), + new ExpressionStatement(fieldExpr), + new ExpressionStatement(expression)); + } + + public static Statement createConstructorStatementDefault(FieldNode fNode) { + final FieldExpression fieldExpr = new FieldExpression(fNode); + Expression initExpr = fNode.getInitialValueExpression(); + if (initExpr == null) initExpr = ConstantExpression.NULL; + Expression value = findArg(fNode.getName()); + return new IfStatement( + equalsNullExpr(value), + new IfStatement( + equalsNullExpr(initExpr), + new EmptyStatement(), + assignStatement(fieldExpr, initExpr)), + assignStatement(fieldExpr, value)); + } + + public static Expression findArg(String fName) { + return new PropertyExpression(new VariableExpression("args"), fName); + } + +} Index: src/main/groovy/transform/AutoClone.java =================================================================== --- src/main/groovy/transform/AutoClone.java (revision ) +++ src/main/groovy/transform/AutoClone.java (revision ) @@ -0,0 +1,186 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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; + +/** + * Note: This annotation is currently experimental! Use at your own risk! + *

    + * Class annotation used to assist in the creation of {@code Cloneable} classes. + * The {@code @AutoClone} annotation instructs the compiler to execute an + * AST transformation which adds a public {@code clone()} method and adds + * {@code Cloneable} to the interfaces which the class implements. The {@code clone()} + * method will call {@code super.clone()} before calling {@code clone()} on each + * {@code Cloneable} property of the class. + *

    + * Example usage: + *

    + * import groovy.transform.AutoClone
    + * {@code @AutoClone}
    + * class Person {
    + *   String first, last
    + *   List favItems
    + *   Date since
    + * }
    + * 
    + * Which will create a class of the following form: + *
    + * class Person implements Cloneable {
    + *   ...
    + *   public Object clone() throws CloneNotSupportedException {
    + *     Object result = super.clone()
    + *     result.favItems = favItems.clone()
    + *     result.since = since.clone()
    + *     return result
    + *   }
    + *   ...
    + * }
    + * 
    + * Which can be used as follows: + *
    + * def p = new Person(first:'John', last:'Smith', favItems:['ipod', 'shiraz'], since:new Date())
    + * def p2 = p.clone()
    + *
    + * assert p instanceof Cloneable
    + * assert p.favItems instanceof Cloneable
    + * assert p.since instanceof Cloneable
    + * assert !(p.first instanceof Cloneable)
    + *
    + * assert !p.is(p2)
    + * assert !p.favItems.is(p2.favItems)
    + * assert !p.since.is(p2.since)
    + * assert p.first.is(p2.first)
    + * 
    + * In the above example, {@code super.clone()} is called which in this case + * calls {@code clone()} from {@code java.lang.Object}. This does a bit-wise + * copy of all the properties (references and primitive values). Properties + * like {@code first} has type {@code String} which is not {@code Cloneable} + * so it is left as the bit-wise copy. Both {@code Date} and {@code ArrayList} + * are {@code Cloneable} so the {@code clone()} method on each of those properties + * will be called. For the list, a shallow copy is made during its {@code clone()} method. + *

    + * If your classes require deep cloning, it is up to you to provide the appropriate + * deep cloning logic in the respective {@code clone()} method for your class. + *

    + * If one of your properties contains an object that doesn't support cloning + * or attempts deep copying of a data structure containing an object that + * doesn't support cloning, then a {@code CloneNotSupportedException} may occur + * at runtime. + *

    + * If any of your fields are {@code final} and {@code Cloneable} you should set + * {@code useCopyConstructor=true} which will then use the copy constructor pattern. + * Here is an example making use of the copy constructor pattern: + *

    + * import groovy.transform.AutoClone
    + * import static groovy.transform.AutoCloneStyle.*
    + * {@code @AutoClone(style=COPY_CONSTRUCTOR)}
    + * class Person {
    + *   final String first, last
    + *   final Date birthday
    + * }
    + * {@code @AutoClone(style=COPY_CONSTRUCTOR)}
    + * class Customer extends Person {
    + *   final int numPurchases
    + *   final List favItems
    + * }
    + * 
    + * Which will create classes of the following form: + *
    + * class Person implements Cloneable {
    + *   ...
    + *   protected Person(Person other) throws CloneNotSupportedException {
    + *     super.clone()
    + *     birthday = other.birthday.clone()
    + *   }
    + *   public Object clone() throws CloneNotSupportedException {
    + *     return new Person(this)
    + *   }
    + *   ...
    + * }
    + * class Customer extends Person {
    + *   ...
    + *   protected Customer(Customer other) throws CloneNotSupportedException {
    + *     super(other)
    + *     numPurchases = other.numPurchases
    + *     favItems = other.favItems.clone()
    + *   }
    + *   public Object clone() throws CloneNotSupportedException {
    + *     return new Customer(this)
    + *   }
    + *   ...
    + * }
    + * 
    + * If you use this style on a child class, the parent class must + * also have a copy constructor. This approach is slightly slower than + * the traditional cloning approach but the {@code Cloneable} fields + * of your class can be final. + *

    + * As a final example, if your class already implements the {@code Serializable} + * or {@code Externalizable} interface, you can choose the following cloning style: + *

    + * {@code @AutoClone(style=SERIALIZATION)}
    + * class Person implements Serializable {
    + *   String first, last
    + *   Date birthday
    + * }
    + * 
    + * which outputs a class with the following form: + *
    + * class Person implements Cloneable, Serializable {
    + *   ...
    + *   Object clone() throws CloneNotSupportedException {
    + *     def baos = new ByteArrayOutputStream()
    + *     baos.withObjectOutputStream{ it.writeObject(this) }
    + *     def bais = new ByteArrayInputStream(baos.toByteArray())
    + *     bais.withObjectInputStream(getClass().classLoader){ it.readObject() }
    + *   }
    + *   ...
    + * }
    + * 
    + * This will output an error if your class doesn't implement one of + * {@code Serializable} or {@code Externalizable}, will typically be + * significantly slower than the other approaches, also doesn't + * allow fields to be final, will take up more memory as even immutable classes + * like String will be cloned but does have the advantage that it performs + * deep cloning automatically. + * + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.AutoCloneASTTransformation") +public @interface AutoClone { + /** + * Comma separated list of property names to exclude from cloning + */ + String excludes() default ""; + + /** + * Include fields as well as properties when cloning + */ + boolean includeFields() default false; + + /** + * Style to use when cloning + */ + groovy.transform.AutoCloneStyle style() default AutoCloneStyle.CLONE; +} Index: src/main/groovy/transform/Canonical.java =================================================================== --- src/main/groovy/transform/Canonical.java (revision ) +++ src/main/groovy/transform/Canonical.java (revision ) @@ -0,0 +1,88 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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. + *

    + * It allows you to write classes in this shortened form: + *

    + * {@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
    + *
    + * If you set the autoDefaults flag to true, you don't need to provide all arguments in constructors calls,
    + * in this case all properties not present are initialized to the default value:
    + * 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']
    + * 
    + * 
    + * The {@code @Canonical} annotation instructs the compiler to execute an + * AST transformation which adds positional constructors, + * equals, hashCode and a pretty print toString. + *

    + * A class created in this way has the following characteristics: + *

      + *
    • A no-arg constructor is provided which allows you to set properties by name using Groovy's normal bean conventions. + *
    • Tuple-style constructors are provided which allow you to set properties in the same order as they are defined. + *
    • 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: + *
      + *     String toString() {
      + *        _toString().replaceAll(/\(/, '(\n\t').replaceAll(/\)/, '\n)').replaceAll(/, /, '\n\t')
      + *    }
      + * 
      + * If an "underscore" version of the respective method already exists, then no default implementation is provided. + *
    + *

    + * Limitations: + *

      + *
    • + * If you explicitly add your own constructors, then the transformation will not add any other constructor to the class. + *
    • + * + * @author Paulo Poiati + * @author Paul King + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.CanonicalASTTransformation") +public @interface Canonical {} Index: src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy =================================================================== --- src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy (revision 19553) +++ src/test/org/codehaus/groovy/transform/TransformsAndCustomClassLoadersTest.groovy (revision ) @@ -39,7 +39,7 @@ def transformLoader = new GroovyClassLoader(TransformsAndCustomClassLoadersTest.classLoader) checkIsIsolated(resolvingLoader) - def clazz = compileAndLoadClass("@Immutable class Foo { String bar }", resolvingLoader, transformLoader) + def clazz = compileAndLoadClass("@groovy.transform.Immutable class Foo { String bar }", resolvingLoader, transformLoader) checkIsImmutable(clazz) } Index: src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/AutoCloneASTTransformation.java (revision ) @@ -0,0 +1,191 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.AutoClone; +import groovy.transform.AutoCloneStyle; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.VariableScope; +import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstanceNonPropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.getInstancePropertyFields; +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.isInstanceOf; + +/** + * Handles generation of code for the @AutoClone annotation. + * + * @author Paul King + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class AutoCloneASTTransformation extends AbstractASTTransformation { + static final Class MY_CLASS = AutoClone.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static final ClassNode CLONEABLE_TYPE = ClassHelper.make(Cloneable.class); + private static final ClassNode BAOS_TYPE = ClassHelper.make(ByteArrayOutputStream.class); + private static final ClassNode BAIS_TYPE = ClassHelper.make(ByteArrayInputStream.class); + private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); + + 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; + checkNotInterface(cNode, MY_TYPE_NAME); + cNode.addInterface(CLONEABLE_TYPE); + boolean includeFields = memberHasValue(anno, "includeFields", true); + AutoCloneStyle style = getStyle(anno, "style"); + String rawExcludes = (String) getMemberValue(anno, "excludes"); + List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", "); + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + if (style == null) style = AutoCloneStyle.CLONE; + switch(style) { + case COPY_CONSTRUCTOR: + createCloneCopyConstructor(cNode, list, excludes); break; + case SERIALIZATION: + createCloneSerialization(cNode, list, excludes); break; + case CLONE: + createClone(cNode, list, excludes); break; + } + } + } + + private void createCloneSerialization(ClassNode cNode, List list, List excludes) { + final BlockStatement body = new BlockStatement(); + // def baos = new ByteArrayOutputStream() + final Expression baos = new VariableExpression("baos"); + body.addStatement(new ExpressionStatement(new DeclarationExpression(baos, ASSIGN, new ConstructorCallExpression(BAOS_TYPE, MethodCallExpression.NO_ARGUMENTS)))); + + // baos.withObjectOutputStream{ it.writeObject(this) } + BlockStatement writeClosureCode = new BlockStatement(); + final Expression it = new VariableExpression("it"); + writeClosureCode.addStatement(new ExpressionStatement(new MethodCallExpression(it, "writeObject", VariableExpression.THIS_EXPRESSION))); + ClosureExpression writeClosure = new ClosureExpression(new Parameter[]{}, writeClosureCode); + writeClosure.setVariableScope(new VariableScope()); + body.addStatement(new ExpressionStatement(new MethodCallExpression(baos, "withObjectOutputStream", new ArgumentListExpression(writeClosure)))); + + // def bais = new ByteArrayInputStream(baos.toByteArray()) + final Expression bais = new VariableExpression("bais"); + ConstructorCallExpression bytes = new ConstructorCallExpression(BAIS_TYPE, new TupleExpression(new MethodCallExpression(baos, "toByteArray", MethodCallExpression.NO_ARGUMENTS))); + body.addStatement(new ExpressionStatement(new DeclarationExpression(bais, ASSIGN, bytes))); + + // return bais.withObjectInputStream(getClass().classLoader){ it.readObject() } + BlockStatement readClosureCode = new BlockStatement(); + readClosureCode.addStatement(new ExpressionStatement(new MethodCallExpression(it, "readObject", MethodCallExpression.NO_ARGUMENTS))); + ClosureExpression readClosure = new ClosureExpression(new Parameter[]{}, readClosureCode); + readClosure.setVariableScope(new VariableScope()); + Expression klass = new MethodCallExpression(VariableExpression.THIS_EXPRESSION, "getClass", MethodCallExpression.NO_ARGUMENTS); + Expression classLoader = new MethodCallExpression(klass, "getClassLoader", MethodCallExpression.NO_ARGUMENTS); + Expression result = new MethodCallExpression(bais, "withObjectInputStream", new ArgumentListExpression(classLoader, readClosure)); + body.addStatement(new ReturnStatement(result)); + + ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)}; + cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, body); + } + + private void createCloneCopyConstructor(ClassNode cNode, List list, List excludes) { + BlockStatement initBody = new BlockStatement(); + if (cNode.getDeclaredConstructors().size() == 0) { + // add no-arg constructor + initBody.addStatement(new EmptyStatement()); + cNode.addConstructor(ACC_PUBLIC, new Parameter[0], ClassNode.EMPTY_ARRAY, initBody); + initBody = new BlockStatement(); + } + Parameter initParam = new Parameter(cNode, "other"); + final Expression other = new VariableExpression(initParam); + boolean hasParent = cNode.getSuperClass() != ClassHelper.OBJECT_TYPE; + if (hasParent) { + initBody.addStatement(new ExpressionStatement(new ConstructorCallExpression(ClassNode.SUPER, other))); + } + for (FieldNode fieldNode : list) { + if (excludes.contains(fieldNode.getName())) continue; + PropertyExpression direct = new PropertyExpression(other, fieldNode.getName()); + Expression cloned = new MethodCallExpression(direct, "clone", MethodCallExpression.NO_ARGUMENTS); + Expression to = new FieldExpression(fieldNode); + Statement assignCloned = new ExpressionStatement(new BinaryExpression(to, ASSIGN, cloned)); + Statement assignDirect = new ExpressionStatement(new BinaryExpression(to, ASSIGN, direct)); + initBody.addStatement(new IfStatement(isInstanceOf(direct, CLONEABLE_TYPE), assignCloned, assignDirect)); + } + ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)}; + cNode.addConstructor(ACC_PROTECTED, new Parameter[]{initParam}, ClassNode.EMPTY_ARRAY, initBody); + final BlockStatement cloneBody = new BlockStatement(); + cloneBody.addStatement(new ExpressionStatement(new ConstructorCallExpression(cNode, new ArgumentListExpression(VariableExpression.THIS_EXPRESSION)))); + cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, cloneBody); + } + + private void createClone(ClassNode cNode, List list, List excludes) { + final BlockStatement body = new BlockStatement(); + final Expression result = new VariableExpression("_result"); + final Expression clone = new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "clone", MethodCallExpression.NO_ARGUMENTS); + body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, clone))); + for (FieldNode fieldNode : list) { + if (excludes.contains(fieldNode.getName())) continue; + Expression from = new MethodCallExpression(new FieldExpression(fieldNode), "clone", MethodCallExpression.NO_ARGUMENTS); + Expression to = new PropertyExpression(result, fieldNode.getName()); + Statement doClone = new ExpressionStatement(new BinaryExpression(to, ASSIGN, from)); + Statement doNothing = new EmptyStatement(); + body.addStatement(new IfStatement(isInstanceOf(new FieldExpression(fieldNode), CLONEABLE_TYPE), doClone, doNothing)); + } + body.addStatement(new ReturnStatement(result)); + ClassNode[] exceptions = {ClassHelper.make(CloneNotSupportedException.class)}; + cNode.addMethod("clone", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, new Parameter[0], exceptions, body); + } + + private AutoCloneStyle getStyle(AnnotationNode node, String name) { + final Expression member = node.getMember(name); + if (member != null && member instanceof PropertyExpression) { + PropertyExpression prop = (PropertyExpression) member; + Expression oe = prop.getObjectExpression(); + if (oe instanceof ClassExpression) { + ClassExpression ce = (ClassExpression) oe; + if (ce.getType().getTypeClass() == AutoCloneStyle.class) { + return AutoCloneStyle.valueOf(prop.getPropertyAsString()); + } + } + } + return null; + } + +} Index: src/main/groovy/transform/AutoExternalize.java =================================================================== --- src/main/groovy/transform/AutoExternalize.java (revision ) +++ src/main/groovy/transform/AutoExternalize.java (revision ) @@ -0,0 +1,82 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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; + +/** + * Note: This annotation is currently experimental! Use at your own risk! + *

      + * Class annotation used to assist in the creation of {@code Externalizable} classes. + * The {@code @AutoExternalize} annotation instructs the compiler to execute an + * AST transformation which adds {@code writeExternal()} and {@code readExternal()} methods + * to a class and adds {@code Externalizable} to the interfaces which the class implements. + * The {@code writeExternal()} method writes each property (or field) or the class while the + * {@code readExternal()} method will read each one in the same order. Properties or fields + * marked as {@code transient} are ignored. + *

      + * Example usage: + *

      + * import groovy.transform.*
      + * {@code @AutoExternalize}
      + * class Person {
      + *   String first, last
      + *   List favItems
      + *   Date since
      + * }
      + * 
      + * Which will create a class of the following form: + *
      + * class Person implements Externalizable {
      + *   ...
      + *   public void writeExternal(ObjectOutput out) throws IOException {
      + *     out.writeObject(first)
      + *     out.writeObject(last)
      + *     out.writeObject(favItems)
      + *     out.writeObject(since)
      + *   }
      + *
      + *   public void readExternal(ObjectInput oin) {
      + *     first = oin.readObject()
      + *     last = oin.readObject()
      + *     favItems = oin.readObject()
      + *     since = oin.readObject()
      + *   }
      + *   ...
      + * }
      + * 
      + * + * @author Paul King + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@GroovyASTTransformationClass("org.codehaus.groovy.transform.AutoExternalizeASTTransformation") +public @interface AutoExternalize { + /** + * Comma separated list of property names to exclude from externalizing + */ + String excludes() default ""; + + /** + * Include fields as well as properties when externalizing + */ + boolean includeFields() default false; +} Index: pom.xml =================================================================== --- pom.xml (revision 20310) +++ pom.xml (revision ) @@ -543,6 +543,10 @@ Alberto Vilches Raton + + Paulo Poiati + + Index: src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java =================================================================== --- src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java (revision ) +++ src/main/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java (revision ) @@ -0,0 +1,172 @@ +/* + * Copyright 2008-2010 the original author or authors. + * + * Licensed 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.EqualsAndHashCode; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.*; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.EmptyStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.IfStatement; +import org.codehaus.groovy.ast.stmt.ReturnStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; +import org.codehaus.groovy.util.HashCodeHelper; + +import java.util.ArrayList; +import java.util.List; + +import static org.codehaus.groovy.transform.AbstractASTTransformUtil.*; + +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class EqualsAndHashCodeASTTransformation extends AbstractASTTransformation { + static final Class MY_CLASS = EqualsAndHashCode.class; + static final ClassNode MY_TYPE = new ClassNode(MY_CLASS); + static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage(); + private static final ClassNode HASHUTIL_TYPE = new ClassNode(HashCodeHelper.class); + private static final Token ASSIGN = Token.newSymbol(Types.ASSIGN, -1, -1); + private static final ClassNode OBJECT_TYPE = new ClassNode(Object.class); + + 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; + checkNotInterface(cNode, MY_TYPE_NAME); + boolean callSuper = memberHasValue(anno, "callSuper", true); + if (callSuper && cNode.getSuperClass().getName().equals("java.lang.Object")) { + addError("Error during " + MY_TYPE_NAME + " processing: callSuper=true but '" + cNode.getName() + "' has no super class.", anno); + } + boolean includeFields = memberHasValue(anno, "includeFields", true); + String rawExcludes = (String) getMemberValue(anno, "excludes"); + List excludes = rawExcludes == null ? new ArrayList() : DefaultGroovyMethods.tokenize(rawExcludes, ", "); + createHashCode(cNode, false, includeFields, callSuper, excludes); + createEquals(cNode, includeFields, callSuper, excludes); + } + } + + public static void createHashCode(ClassNode cNode, boolean cacheResult, boolean includeFields, boolean callSuper, List excludes) { + // make a public method if none exists otherwise try a private method with leading underscore + boolean hasExistingHashCode = hasDeclaredMethod(cNode, "hashCode", 0); + if (hasExistingHashCode && hasDeclaredMethod(cNode, "_hashCode", 0)) return; + + final BlockStatement body = new BlockStatement(); + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + if (cacheResult) { + final FieldNode hashField = cNode.addField("$hash$code", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.int_TYPE, null); + final Expression hash = new FieldExpression(hashField); + body.addStatement(new IfStatement( + isZeroExpr(hash), + calculateHashStatements(hash, list, callSuper), + new EmptyStatement() + )); + body.addStatement(new ReturnStatement(hash)); + } else { + body.addStatement(calculateHashStatements(null, list, callSuper)); + } + + cNode.addMethod(new MethodNode(hasExistingHashCode ? "_hashCode" : "hashCode", hasExistingHashCode ? ACC_PRIVATE : ACC_PUBLIC, + ClassHelper.int_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, body)); + } + + private static Statement calculateHashStatements(Expression hash, List list, boolean callSuper) { + final BlockStatement body = new BlockStatement(); + // def _result = HashCodeHelper.initHash() + final Expression result = new VariableExpression("_result"); + final Expression init = new StaticMethodCallExpression(HASHUTIL_TYPE, "initHash", MethodCallExpression.NO_ARGUMENTS); + body.addStatement(new ExpressionStatement(new DeclarationExpression(result, ASSIGN, init))); + + for (FieldNode fNode : list) { + if (fNode.getName().contains("$")) continue; + // _result = HashCodeHelper.updateHash(_result, field) + final Expression fieldExpr = new FieldExpression(fNode); + final Expression args = new TupleExpression(result, fieldExpr); + final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args); + body.addStatement(assignStatement(result, current)); + } + if (callSuper) { + // _result = HashCodeHelper.updateHash(_result, super.hashCode()) + final Expression args = new TupleExpression(result, new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "hashCode", MethodCallExpression.NO_ARGUMENTS)); + final Expression current = new StaticMethodCallExpression(HASHUTIL_TYPE, "updateHash", args); + body.addStatement(assignStatement(result, current)); + } + // $hash$code = _result + if (hash != null) { + body.addStatement(assignStatement(hash, result)); + } else { + body.addStatement(new ReturnStatement(result)); + } + return body; + } + + public static void createEquals(ClassNode cNode, boolean includeFields, boolean callSuper, List excludes) { + // make a public method if none exists otherwise try a private method with leading underscore + boolean hasExistingEquals = hasDeclaredMethod(cNode, "equals", 1); + if (hasExistingEquals && hasDeclaredMethod(cNode, "_equals", 1)) return; + + final BlockStatement body = new BlockStatement(); + Expression other = new VariableExpression("other"); + + // some short circuit cases for efficiency + body.addStatement(returnFalseIfNull(other)); + body.addStatement(returnFalseIfWrongType(cNode, other)); + body.addStatement(returnTrueIfIdentical(VariableExpression.THIS_EXPRESSION, other)); + + body.addStatement(new ExpressionStatement(new BinaryExpression(other, ASSIGN, new CastExpression(cNode, other)))); + + List list = getInstancePropertyFields(cNode); + if (includeFields) { + list.addAll(getInstanceNonPropertyFields(cNode)); + } + for (FieldNode fNode : list) { + if (excludes.contains(fNode.getName()) || fNode.getName().contains("$")) continue; + body.addStatement(returnFalseIfPropertyNotEqual(fNode, other)); + } + if (callSuper) { + Statement result = new IfStatement( + isTrueExpr(new MethodCallExpression(VariableExpression.SUPER_EXPRESSION, "equals", other)), + new EmptyStatement(), + new ReturnStatement(ConstantExpression.FALSE) + ); + body.addStatement(result); + } + + // default + body.addStatement(new ReturnStatement(ConstantExpression.TRUE)); + + Parameter[] params = {new Parameter(OBJECT_TYPE, "other")}; + cNode.addMethod(new MethodNode(hasExistingEquals ? "_equals" : "equals", hasExistingEquals ? ACC_PRIVATE : ACC_PUBLIC, + ClassHelper.boolean_TYPE, params, ClassNode.EMPTY_ARRAY, body)); + } +} Index: gradle/pomconfigurer.gradle =================================================================== --- gradle/pomconfigurer.gradle (revision 20093) +++ gradle/pomconfigurer.gradle (revision ) @@ -433,7 +433,10 @@ contributor { name 'Alberto Vilches Raton' } + contributor { + name 'Paulo Poiati' - } + } + } mailingLists { mailingList { name 'Groovy JSR Discussion List'