diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoOneOfJava8Test.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoOneOfJava8Test.java new file mode 100644 index 0000000000..1b513b3cd2 --- /dev/null +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoOneOfJava8Test.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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; + +import static com.google.common.truth.Truth.assertThat; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Method; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for Java8-specific {@code @AutoOneOf} behaviour. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +@RunWith(JUnit4.class) +public class AutoOneOfJava8Test { + @AutoOneOf(EqualsNullable.Kind.class) + public abstract static class EqualsNullable { + + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.RUNTIME) + public @interface Nullable {} + + public enum Kind {THING} + public abstract Kind kind(); + public abstract String thing(); + + public static EqualsNullable ofThing(String thing) { + return AutoOneOf_AutoOneOfJava8Test_EqualsNullable.thing(thing); + } + + @Override + public abstract boolean equals(@Nullable Object x); + + @Override + public abstract int hashCode(); + } + + /** + * Tests that a type annotation on the parameter of {@code equals(Object)} is copied into the + * implementation class. + */ + @Test + public void equalsNullable() throws ReflectiveOperationException { + if (BugDetector.typeVisitorDropsAnnotations()) { + System.err.println("TYPE VISITORS DO NOT SEE TYPE ANNOTATIONS, SKIPPING TEST"); + return; + } + EqualsNullable x = EqualsNullable.ofThing("foo"); + Class c = x.getClass(); + Method equals = c.getMethod("equals", Object.class); + assertThat(equals.getDeclaringClass()).isNotSameAs(EqualsNullable.class); + AnnotatedType parameterType = equals.getAnnotatedParameterTypes()[0]; + assertThat(parameterType.isAnnotationPresent(EqualsNullable.Nullable.class)) + .isTrue(); + } +} diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java index f12fe1e070..1bf08daa34 100644 --- a/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java +++ b/value/src/it/functional/src/test/java/com/google/auto/value/AutoValueJava8Test.java @@ -31,6 +31,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Arrays; @@ -512,4 +513,41 @@ public void testFunkyNullable() { FunkyNullable implicitNull = FunkyNullable.builder().build(); assertThat(explicitNull).isEqualTo(implicitNull); } + + @AutoValue + abstract static class EqualsNullable { + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @Retention(RetentionPolicy.RUNTIME) + @interface Nullable {} + + abstract String foo(); + + static EqualsNullable create(String foo) { + return new AutoValue_AutoValueJava8Test_EqualsNullable(foo); + } + + @Override + public abstract boolean equals(@Nullable Object x); + + @Override + public abstract int hashCode(); + } + + /** + * Tests that a type annotation on the parameter of {@code equals(Object)} is copied into the + * implementation class. + */ + @Test + public void testEqualsNullable() throws ReflectiveOperationException { + if (BugDetector.typeVisitorDropsAnnotations()) { + System.err.println("TYPE VISITORS DO NOT SEE TYPE ANNOTATIONS, SKIPPING TEST"); + return; + } + EqualsNullable x = EqualsNullable.create("foo"); + Class implClass = x.getClass(); + Method equals = implClass.getDeclaredMethod("equals", Object.class); + AnnotatedType[] parameterTypes = equals.getAnnotatedParameterTypes(); + assertThat(parameterTypes[0].isAnnotationPresent(EqualsNullable.Nullable.class)) + .isTrue(); + } } diff --git a/value/src/it/functional/src/test/java/com/google/auto/value/BugDetector.java b/value/src/it/functional/src/test/java/com/google/auto/value/BugDetector.java new file mode 100644 index 0000000000..a40f379032 --- /dev/null +++ b/value/src/it/functional/src/test/java/com/google/auto/value/BugDetector.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.testing.compile.CompilationSubject.assertThat; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.Compiler; +import com.google.testing.compile.JavaFileObjects; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.SimpleTypeVisitor8; +import javax.tools.JavaFileObject; + +/** + * Detects javac bugs that might prevent tests from working. + * + * @author emcmanus@google.com (Éamonn McManus) + */ +final class BugDetector { + private BugDetector() {} + + /** + * Returns true if {@link TypeMirror#accept} gives the unannotated type to the type visitor. It + * should obviously receive the type that {@code accept} was called on, but in at least some + * Java 8 versions it ends up being the unannotated one. + */ + // I have not been able to find a reference for this bug. + static boolean typeVisitorDropsAnnotations() { + JavaFileObject testClass = JavaFileObjects.forSourceLines( + "com.example.Test", + "package com.example;", + "", + "import java.lang.annotation.*;", + "", + "abstract class Test {", + " @Target(ElementType.TYPE_USE)", + " @interface Nullable {}", + "", + " @Override public abstract boolean equals(@Nullable Object x);", + "}"); + BugDetectorProcessor bugDetectorProcessor = new BugDetectorProcessor(); + Compilation compilation = + Compiler.javac().withProcessors(bugDetectorProcessor).compile(testClass); + assertThat(compilation).succeeded(); + return bugDetectorProcessor.typeAnnotationsNotReturned; + } + + @SupportedAnnotationTypes("*") + @SupportedSourceVersion(SourceVersion.RELEASE_8) + private static class BugDetectorProcessor extends AbstractProcessor { + volatile boolean typeAnnotationsNotReturned; + + @Override + public boolean process( + Set annotations, RoundEnvironment roundEnv) { + if (!roundEnv.processingOver()) { + TypeElement test = processingEnv.getElementUtils().getTypeElement("com.example.Test"); + ExecutableElement equals = ElementFilter.methodsIn(test.getEnclosedElements()).get(0); + assertThat(equals.getSimpleName().toString()).isEqualTo("equals"); + TypeMirror parameterType = equals.getParameters().get(0).asType(); + List annotationsFromVisitor = + parameterType.accept(new BugDetectorVisitor(), null); + typeAnnotationsNotReturned = annotationsFromVisitor.isEmpty(); + } + return false; + } + + private static class BugDetectorVisitor + extends SimpleTypeVisitor8, Void> { + @Override + public List visitDeclared(DeclaredType t, Void p) { + return Collections.unmodifiableList(t.getAnnotationMirrors()); + } + } + } +} diff --git a/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java index 8270af41a6..7b4c84ddf7 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoOneOfProcessor.java @@ -110,10 +110,12 @@ void processType(TypeElement autoOneOfType) { vars.generatedClass = TypeSimplifier.simpleNameOf(subclass); vars.types = processingEnv.getTypeUtils(); vars.propertyToKind = propertyToKind; - Set methodsToGenerate = determineObjectMethodsToGenerate(methods); - vars.toString = methodsToGenerate.contains(ObjectMethod.TO_STRING); - vars.equals = methodsToGenerate.contains(ObjectMethod.EQUALS); - vars.hashCode = methodsToGenerate.contains(ObjectMethod.HASH_CODE); + Map methodsToGenerate = + determineObjectMethodsToGenerate(methods); + vars.toString = methodsToGenerate.containsKey(ObjectMethod.TO_STRING); + vars.equals = methodsToGenerate.containsKey(ObjectMethod.EQUALS); + vars.hashCode = methodsToGenerate.containsKey(ObjectMethod.HASH_CODE); + vars.equalsParameterType = equalsParameterType(methodsToGenerate); defineVarsForType(autoOneOfType, vars, propertyMethods, kindGetter); String text = vars.toText(); diff --git a/value/src/main/java/com/google/auto/value/processor/AutoOneOfTemplateVars.java b/value/src/main/java/com/google/auto/value/processor/AutoOneOfTemplateVars.java index 810618234e..42b3c35206 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoOneOfTemplateVars.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoOneOfTemplateVars.java @@ -41,6 +41,13 @@ class AutoOneOfTemplateVars extends TemplateVars { /** Whether to generate a toString() method. */ Boolean toString; + /** + * A string representing the parameter type declaration of the equals(Object) method, including + * any annotations. If {@link #equals} is false, this field is ignored (but it must still be + * non-null). + */ + String equalsParameterType; + /** The type utilities returned by {@link ProcessingEnvironment#getTypeUtils()}. */ Types types; diff --git a/value/src/main/java/com/google/auto/value/processor/AutoValueOrOneOfProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoValueOrOneOfProcessor.java index a2bf9068e1..c0ed8e67f4 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoValueOrOneOfProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoValueOrOneOfProcessor.java @@ -16,6 +16,7 @@ package com.google.auto.value.processor; import static com.google.auto.common.AnnotationMirrors.getAnnotationValue; +import static com.google.common.collect.Iterables.getOnlyElement; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -33,7 +34,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.EnumSet; +import java.util.EnumMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -411,9 +412,14 @@ static ObjectMethod objectMethodToOverride(ExecutableElement method) { } break; case 1: - if (name.equals("equals") - && method.getParameters().get(0).asType().toString().equals("java.lang.Object")) { - return ObjectMethod.EQUALS; + if (name.equals("equals")) { + TypeMirror param = getOnlyElement(method.getParameters()).asType(); + if (param.getKind().equals(TypeKind.DECLARED)) { + TypeElement paramType = MoreTypes.asTypeElement(param); + if (paramType.getQualifiedName().contentEquals("java.lang.Object")) { + return ObjectMethod.EQUALS; + } + } } break; default: @@ -541,22 +547,39 @@ private static String disambiguate(String name, Collection existingNames } /** - * Given a list of all methods defined in or inherited by a class, returns a set indicating - * which of equals, hashCode, and toString should be generated. + * Given a list of all methods defined in or inherited by a class, returns a map indicating + * which of equals, hashCode, and toString should be generated. Each value in the map is + * the method that will be overridden by the generated method, which might be a method in + * {@code Object} or an abstract method in the {@code @AutoValue} class or an ancestor. */ - static Set determineObjectMethodsToGenerate(Set methods) { - Set methodsToGenerate = EnumSet.noneOf(ObjectMethod.class); + static Map + determineObjectMethodsToGenerate(Set methods) { + Map methodsToGenerate = new EnumMap<>(ObjectMethod.class); for (ExecutableElement method : methods) { ObjectMethod override = objectMethodToOverride(method); boolean canGenerate = method.getModifiers().contains(Modifier.ABSTRACT) || isJavaLangObject((TypeElement) method.getEnclosingElement()); if (!override.equals(ObjectMethod.NONE) && canGenerate) { - methodsToGenerate.add(override); + methodsToGenerate.put(override, method); } } return methodsToGenerate; } + /** + * Returns the encoded parameter type of the {@code equals(Object)} method that is to be + * generated, or an empty string if the method is not being generated. The parameter type + * includes any type annotations, for example {@code @Nullable}. + */ + static String equalsParameterType(Map methodsToGenerate) { + ExecutableElement equals = methodsToGenerate.get(ObjectMethod.EQUALS); + if (equals == null) { + return ""; // this will not be referenced because no equals method will be generated + } + TypeMirror parameterType = equals.getParameters().get(0).asType(); + return TypeEncoder.encodeWithAnnotations(parameterType); + } + /** * Returns the subset of all abstract methods in the given set of methods. A given method * signature is only mentioned once, even if it is inherited on more than one path. 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 4bd6dec86d..7de3a2b403 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 @@ -47,6 +47,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.ServiceConfigurationError; @@ -222,10 +223,12 @@ void processType(TypeElement type) { vars.types = processingEnv.getTypeUtils(); vars.identifiers = !processingEnv.getOptions().containsKey("com.google.auto.value.OmitIdentifiers"); - Set methodsToGenerate = determineObjectMethodsToGenerate(methods); - vars.toString = methodsToGenerate.contains(ObjectMethod.TO_STRING); - vars.equals = methodsToGenerate.contains(ObjectMethod.EQUALS); - vars.hashCode = methodsToGenerate.contains(ObjectMethod.HASH_CODE); + Map methodsToGenerate = + determineObjectMethodsToGenerate(methods); + vars.toString = methodsToGenerate.containsKey(ObjectMethod.TO_STRING); + vars.hashCode = methodsToGenerate.containsKey(ObjectMethod.HASH_CODE); + vars.equals = methodsToGenerate.containsKey(ObjectMethod.EQUALS); + vars.equalsParameterType = equalsParameterType(methodsToGenerate); defineVarsForType(type, vars, toBuilderMethods, propertyMethods, builder); // Only copy annotations from a class if it has @AutoValue.CopyAnnotations. 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 0bba7d112c..89dfd86c65 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 @@ -45,6 +45,13 @@ class AutoValueTemplateVars extends TemplateVars { /** Whether to generate a toString() method. */ Boolean toString; + /** + * A string representing the parameter type declaration of the equals(Object) method, including + * any annotations. If {@link #equals} is false, this field is ignored (but it must still be + * non-null). + */ + String equalsParameterType; + /** * Whether to include identifiers in strings in the generated code. If false, exception messages * will not mention properties by name, and {@code toString()} will include neither property diff --git a/value/src/main/java/com/google/auto/value/processor/autooneof.vm b/value/src/main/java/com/google/auto/value/processor/autooneof.vm index faa33fe9e6..b0767d7608 100644 --- a/value/src/main/java/com/google/auto/value/processor/autooneof.vm +++ b/value/src/main/java/com/google/auto/value/processor/autooneof.vm @@ -112,7 +112,7 @@ final class $generatedClass { #if ($equals) @Override - public boolean equals(Object x) { + public boolean equals($equalsParameterType x) { if (x instanceof $origClass) { $origClass$wildcardTypes that = ($origClass$wildcardTypes) x; return this.${kindGetter}() == that.${kindGetter}() 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 bbdbf2f374..3eab6f09f6 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 @@ -125,7 +125,7 @@ $a #if ($equals) @Override - public boolean equals(`java.lang.Object` o) { + public boolean equals($equalsParameterType o) { if (o == this) { return true; }