From ad63ed3be7bcf971b6a565fef897ca31deb2104f Mon Sep 17 00:00:00 2001 From: Rafael Torres Date: Tue, 31 Mar 2015 12:22:28 -0700 Subject: [PATCH] Adding support to AutoValue.Id. This new annotation should be used when an autoValue does not want to use all its fields in the equals() and hashcode() method. --- .../java/com/google/auto/value/AutoValue.java | 11 + .../processor/AutoValueIdsProcessor.java | 79 ++++++++ .../value/processor/AutoValueProcessor.java | 19 +- .../processor/AutoValueTemplateVars.java | 4 + .../google/auto/value/processor/autovalue.vm | 6 +- .../auto/value/processor/CompilationTest.java | 191 ++++++++++++++++++ 6 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 value/src/main/java/com/google/auto/value/processor/AutoValueIdsProcessor.java diff --git a/value/src/main/java/com/google/auto/value/AutoValue.java b/value/src/main/java/com/google/auto/value/AutoValue.java index d05be39597..5a21418fd5 100644 --- a/value/src/main/java/com/google/auto/value/AutoValue.java +++ b/value/src/main/java/com/google/auto/value/AutoValue.java @@ -85,4 +85,15 @@ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.METHOD) public @interface Validate {} + + + /** + * Specifies that the annotated method is an id method. All methods containing this annotation + * will be used in the {@link Object#equals equals} and {@link Object#hashCode hashCode} implementation. + * + * @author Rafael Torres + */ + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.METHOD) + public @interface Id {} } diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueIdsProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueIdsProcessor.java new file mode 100644 index 0000000000..125287610b --- /dev/null +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueIdsProcessor.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * 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 com.google.auto.value.processor; + +import com.google.auto.common.MoreElements; +import com.google.auto.common.SuperficialValidation; +import com.google.auto.service.AutoService; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import java.util.Set; + +import static com.google.auto.common.MoreElements.isAnnotationPresent; + +/** + * Annotation processor that checks that the type that {@link com.google.auto.value.AutoValue.Id} + * is applied to is nested inside an {@code @AutoValue} class. The actual code generation for ids + * is done in {@link AutoValueProcessor}. + * + * @author Rafael Torres + */ +@AutoService(Processor.class) +public class AutoValueIdsProcessor extends AbstractProcessor { + @Override + public Set getSupportedAnnotationTypes() { + return ImmutableSet.of( + AutoValue.Id.class.getCanonicalName()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Set idsMethods = + roundEnv.getElementsAnnotatedWith(AutoValue.Id.class); + if (!SuperficialValidation.validateElements(idsMethods)) { + return false; + } + for (Element annotatedMethod : idsMethods) { + if (isAnnotationPresent(annotatedMethod, AutoValue.Id.class)) { + validate( + annotatedMethod, + "@AutoValue.Id can only be applied to a method inside an @AutoValue class"); + } + } + return false; + } + + private void validate(Element annotatedType, String errorMessage) { + Element container = annotatedType.getEnclosingElement(); + if (!MoreElements.isAnnotationPresent(container, AutoValue.class)) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, errorMessage, annotatedType); + } + } +} diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java index b9309c28a5..6dc3b6906e 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueProcessor.java @@ -15,6 +15,7 @@ */ package com.google.auto.value.processor; +import com.google.auto.common.MoreElements; import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; import com.google.common.base.Functions; @@ -414,15 +415,24 @@ private void defineVarsForType(TypeElement type, AutoValueTemplateVars vars) { Maps.newLinkedHashMap(methodToPropertyName); fixReservedIdentifiers(methodToIdentifier); List props = new ArrayList(); + List ids = new ArrayList(); for (ExecutableElement method : propertyMethods) { String propertyType = typeSimplifier.simplify(method.getReturnType()); String propertyName = methodToPropertyName.get(method); String identifier = methodToIdentifier.get(method); - props.add(new Property(propertyName, identifier, method, propertyType, typeSimplifier)); + Property p = new Property(propertyName, identifier, method, propertyType, typeSimplifier); + props.add(p); + if (MoreElements.isAnnotationPresent(method, AutoValue.Id.class)) { + ids.add(p); + } + } + if (ids.isEmpty()) { + ids.addAll(props); } // If we are running from Eclipse, undo the work of its compiler which sorts methods. eclipseHack().reorderProperties(props); vars.props = props; + vars.ids = ids; vars.serialVersionUID = getSerialVersionUID(type); vars.formalTypes = typeSimplifier.formalTypeParametersString(type); vars.actualTypes = TypeSimplifier.actualTypeParametersString(type); @@ -494,7 +504,7 @@ private void fixReservedIdentifiers(Map methodToIdent } private String disambiguate(String name, Collection existingNames) { - for (int i = 0; ; i++) { + for (int i = 0;; i++) { String candidate = name + i; if (!existingNames.contains(candidate)) { return candidate; @@ -554,6 +564,11 @@ private ImmutableSet methodsToImplement(List toImplement = ImmutableSet.builder(); boolean errors = false; for (ExecutableElement method : methods) { + if (MoreElements.isAnnotationPresent(method, AutoValue.Id.class) + && method.getModifiers().contains(Modifier.STATIC)) { + errorReporter.reportError("@AutoValue.Id cannot apply to a static method", method); + errors = true; + } if (method.getModifiers().contains(Modifier.ABSTRACT) && objectMethodToOverride(method) == ObjectMethodToOverride.NONE) { if (method.getParameters().isEmpty() && method.getReturnType().getKind() != TypeKind.VOID) { diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java b/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java index 1c68845ed8..6601da2e0b 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueTemplateVars.java @@ -36,6 +36,9 @@ class AutoValueTemplateVars extends TemplateVars { /** The properties defined by the parent class's abstract methods. */ List props; + /** The properties of all methods marked as ids or a copy of props in case ids is empty*/ + List ids; + /** Whether to generate an equals(Object) method. */ Boolean equals; /** Whether to generate a hashCode() method. */ @@ -146,6 +149,7 @@ class AutoValueTemplateVars extends TemplateVars { private static final SimpleNode TEMPLATE = parsedTemplateForResource("autovalue.vm"); + @Override SimpleNode parsedTemplate() { return TEMPLATE; diff --git a/value/src/main/java/com/google/auto/value/processor/autovalue.vm b/value/src/main/java/com/google/auto/value/processor/autovalue.vm index 4f1a057f88..6317335381 100644 --- a/value/src/main/java/com/google/auto/value/processor/autovalue.vm +++ b/value/src/main/java/com/google/auto/value/processor/autovalue.vm @@ -127,7 +127,7 @@ final class $subclass$formalTypes extends $origClass$actualTypes { } if (o instanceof $origClass) { - #if ($props.empty) + #if ($ids.empty) return true; @@ -135,7 +135,7 @@ final class $subclass$formalTypes extends $origClass$actualTypes { $origClass$wildcardTypes that = ($origClass$wildcardTypes) o; return ## - #foreach ($p in $props) + #foreach ($p in $ids) (#equalsThatExpression ($p))## #if ($foreach.hasNext) @@ -174,7 +174,7 @@ final class $subclass$formalTypes extends $origClass$actualTypes { public int hashCode() { int h = 1; - #foreach ($p in $props) + #foreach ($p in $ids) h *= 1000003; h ^= #hashCodeExpression($p); diff --git a/value/src/test/java/com/google/auto/value/processor/CompilationTest.java b/value/src/test/java/com/google/auto/value/processor/CompilationTest.java index f98d083f3a..d49fb651fd 100644 --- a/value/src/test/java/com/google/auto/value/processor/CompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/CompilationTest.java @@ -1523,4 +1523,195 @@ public void testReferencingGeneratedClass() { .processedWith(new AutoValueProcessor(), new FooProcessor()) .compilesWithoutError(); } + + public void testId() { + JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "", + "@AutoValue", + "public abstract class Baz {", + " public abstract String stringVar();", + " @AutoValue.Id public abstract String stringId();", + + + "}"); + JavaFileObject expectedOutput = JavaFileObjects.forSourceString( + "foo.bar.AutoValue_Baz", + "package foo.bar;\n" + + "\n" + + "import javax.annotation.Generated;\n" + + "\n" + + "@Generated(\"com.google.auto.value.processor.AutoValueProcessor\")\n" + + "final class AutoValue_Baz extends Baz {\n" + + "\n" + + " private final String stringVar;\n" + + " private final String stringId;\n" + + "\n" + + " AutoValue_Baz(\n" + + " String stringVar,\n" + + " String stringId) {\n" + + " if (stringVar == null) {\n" + + " throw new NullPointerException(\"Null stringVar\");\n" + + " }\n" + + " this.stringVar = stringVar;\n" + + " if (stringId == null) {\n" + + " throw new NullPointerException(\"Null stringId\");\n" + + " }\n" + + " this.stringId = stringId;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String stringVar() {\n" + + " return stringVar;\n" + + " }\n" + + "\n" + + " @com.google.auto.value.AutoValue.Id\n" + + " @Override\n" + + " public String stringId() {\n" + + " return stringId;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String toString() {\n" + + " return \"Baz{\"\n" + + " + \"stringVar=\" + stringVar + \", \"\n" + + " + \"stringId=\" + stringId\n" + + " + \"}\";\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean equals(Object o) {\n" + + " if (o == this) {\n" + + " return true;\n" + + " }\n" + + " if (o instanceof Baz) {\n" + + " Baz that = (Baz) o;\n" + + " return (this.stringId.equals(that.stringId()));\n" + + " }\n" + + " return false;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public int hashCode() {\n" + + " int h = 1;\n" + + " h *= 1000003;\n" + + " h ^= stringId.hashCode();\n" + + " return h;\n" + + " }\n" + + "\n" + + "}" + ); + assertAbout(javaSource()) + .that(javaFileObject) + .processedWith(new AutoValueProcessor()) + .compilesWithoutError() + .and().generatesSources(expectedOutput); + } + + + public void testIds() { + JavaFileObject javaFileObject = JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "", + "@AutoValue", + "public abstract class Baz {", + " public abstract String stringVar();", + " @AutoValue.Id public abstract String stringId();", + " @AutoValue.Id public abstract String stringId2();", + + "}"); + JavaFileObject expectedOutput = JavaFileObjects.forSourceString( + "foo.bar.AutoValue_Baz", + "package foo.bar;\n" + + "\n" + + "import javax.annotation.Generated;\n" + + "\n" + + "@Generated(\"com.google.auto.value.processor.AutoValueProcessor\")\n" + + "final class AutoValue_Baz extends Baz {\n" + + "\n" + + " private final String stringVar;\n" + + " private final String stringId;\n" + + " private final String stringId2;\n" + + "\n" + + " AutoValue_Baz(\n" + + " String stringVar,\n" + + " String stringId,\n" + + " String stringId2) {\n" + + " if (stringVar == null) {\n" + + " throw new NullPointerException(\"Null stringVar\");\n" + + " }\n" + + " this.stringVar = stringVar;\n" + + " if (stringId == null) {\n" + + " throw new NullPointerException(\"Null stringId\");\n" + + " }\n" + + " this.stringId = stringId;\n" + + " if (stringId2 == null) {\n" + + " throw new NullPointerException(\"Null stringId2\");\n" + + " }\n" + + " this.stringId2 = stringId2;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String stringVar() {\n" + + " return stringVar;\n" + + " }\n" + + "\n" + + " @com.google.auto.value.AutoValue.Id\n" + + " @Override\n" + + " public String stringId() {\n" + + " return stringId;\n" + + " }\n" + + "\n" + + " @com.google.auto.value.AutoValue.Id\n" + + " @Override\n" + + " public String stringId2() {\n" + + " return stringId2;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public String toString() {\n" + + " return \"Baz{\"\n" + + " + \"stringVar=\" + stringVar + \", \"\n" + + " + \"stringId=\" + stringId + \", \"\n" + + " + \"stringId2=\" + stringId2\n" + + " + \"}\";\n" + + " }\n" + + "\n" + + " @Override\n" + + " public boolean equals(Object o) {\n" + + " if (o == this) {\n" + + " return true;\n" + + " }\n" + + " if (o instanceof Baz) {\n" + + " Baz that = (Baz) o;\n" + + " return (this.stringId.equals(that.stringId()))\n" + + " && (this.stringId2.equals(that.stringId2()));\n" + + " }\n" + + " return false;\n" + + " }\n" + + "\n" + + " @Override\n" + + " public int hashCode() {\n" + + " int h = 1;\n" + + " h *= 1000003;\n" + + " h ^= stringId.hashCode();\n" + + " h *= 1000003;\n" + + " h ^= stringId2.hashCode();\n" + + " return h;\n" + + " }\n" + + "\n" + + "}" + ); + assertAbout(javaSource()) + .that(javaFileObject) + .processedWith(new AutoValueProcessor()) + .compilesWithoutError() + .and().generatesSources(expectedOutput); + } }