diff options
author | Éamonn McManus <emcmanus@google.com> | 2021-10-19 10:52:20 -0700 |
---|---|---|
committer | Google Java Core Libraries <java-libraries-firehose+copybara@google.com> | 2021-10-19 10:53:03 -0700 |
commit | e0740327d830597e17273946418f6adc976bc619 (patch) | |
tree | 99a493f52552c986d328492ff50ed8d6d563182b | |
parent | e66de800e245561108cef75a99214a38af18e872 (diff) | |
download | auto-e0740327d830597e17273946418f6adc976bc619.tar.gz |
Handle missing type when copying annotations.
If you have `@CopyAnnotations` and `@Foo(Bar.class)`, and if `Bar` is undefined, we want to defer processing to give some other annotation processor a chance to define `Bar`. This turns out to be quite difficult, because javac essentially makes it look as if you have written `@Foo("<error>")` in this case. So we check to see if an annotation element that _should_ be a class is something else (a string in this case).
RELNOTES=We now handle better the case where an annotation being copied references a missing class.
PiperOrigin-RevId: 404307632
-rw-r--r-- | value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java | 52 | ||||
-rw-r--r-- | value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java | 70 |
2 files changed, 120 insertions, 2 deletions
diff --git a/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java b/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java index 0c8b8f0f..ed6abaa6 100644 --- a/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java +++ b/value/src/main/java/com/google/auto/value/processor/AnnotationOutput.java @@ -15,6 +15,8 @@ */ package com.google.auto.value.processor; +import com.google.auto.common.MoreTypes; +import com.google.auto.value.processor.MissingTypes.MissingTypeException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.util.List; @@ -24,8 +26,10 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.SimpleAnnotationValueVisitor8; import javax.tools.Diagnostic; @@ -222,11 +226,59 @@ final class AnnotationOutput { * Java source file to reproduce the annotation in source form. */ static String sourceFormForAnnotation(AnnotationMirror annotationMirror) { + // If a value in the annotation is a reference to a class constant and that class constant is + // undefined, javac unhelpfully converts it into a string "<error>" and visits that instead. We + // want to catch this case and defer processing to allow the class to be defined by another + // annotation processor. So we look for annotation elements whose type is Class but whose + // reported value is a string. Unfortunately we can't extract the ErrorType corresponding to the + // missing class portably. With javac, the AttributeValue is a + // com.sun.tools.javac.code.Attribute.UnresolvedClass, which has a public field classType that + // is the ErrorType we need, but obviously that's nonportable and fragile. + validateClassValues(annotationMirror); StringBuilder sb = new StringBuilder(); new AnnotationSourceFormVisitor().visitAnnotation(annotationMirror, sb); return sb.toString(); } + /** + * Throws an exception if this annotation contains a value for a Class element that is not + * actually a type. The assumption is that the value is the string {@code "<error>"} which javac + * presents when a Class value is an undefined type. + */ + private static void validateClassValues(AnnotationMirror annotationMirror) { + // A class literal can appear in three places: + // * for an element of type Class, for example @SomeAnnotation(Foo.class); + // * for an element of type Class[], for example @SomeAnnotation({Foo.class, Bar.class}); + // * inside a nested annotation, for example @SomeAnnotation(@Nested(Foo.class)). + // These three possibilities are the three branches of the if/else chain below. + annotationMirror + .getElementValues() + .forEach( + (method, value) -> { + TypeMirror type = method.getReturnType(); + if (isJavaLangClass(type) && !(value.getValue() instanceof TypeMirror)) { + throw new MissingTypeException(null); + } else if (type.getKind().equals(TypeKind.ARRAY) + && isJavaLangClass(MoreTypes.asArray(type).getComponentType()) + && value.getValue() instanceof List<?>) { + @SuppressWarnings("unchecked") // a List can only be a List<AnnotationValue> here + List<AnnotationValue> values = (List<AnnotationValue>) value.getValue(); + if (values.stream().anyMatch(av -> !(av.getValue() instanceof TypeMirror))) { + throw new MissingTypeException(null); + } + } else if (type.getKind().equals(TypeKind.DECLARED) + && MoreTypes.asElement(type).getKind().equals(ElementKind.ANNOTATION_TYPE) + && value.getValue() instanceof AnnotationMirror) { + validateClassValues((AnnotationMirror) value.getValue()); + } + }); + } + + private static boolean isJavaLangClass(TypeMirror type) { + return type.getKind().equals(TypeKind.DECLARED) + && MoreTypes.asTypeElement(type).getQualifiedName().contentEquals("java.lang.Class"); + } + private static StringBuilder appendQuoted(StringBuilder sb, String s) { sb.append('"'); for (int i = 0; i < s.length(); i++) { diff --git a/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java b/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java index 9d7f7856..b5dbb408 100644 --- a/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java +++ b/value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java @@ -21,6 +21,7 @@ import static com.google.testing.compile.CompilationSubject.compilations; import static com.google.testing.compile.Compiler.javac; import static java.util.stream.Collectors.joining; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.truth.Expect; @@ -2913,8 +2914,6 @@ public class AutoValueCompilationTest { "foo.bar.Bar", "package foo.bar;", "", - "import com.google.auto.value.AutoValue;", - "", "@" + Foo.class.getCanonicalName(), "public abstract class Bar {", " public abstract BarFoo barFoo();", @@ -2928,6 +2927,73 @@ public class AutoValueCompilationTest { } @Test + public void referencingGeneratedClassInAnnotation() { + // Test that ensures that a type that does not exist can be referenced by a copied annotation + // as long as it later does come into existence. The BarFoo type referenced here does not exist + // when the AutoValueProcessor runs on the first round, but the FooProcessor then generates it. + // That generation provokes a further round of annotation processing and AutoValueProcessor + // should succeed then. + // We test the three places that a class reference could appear: as the value of a Class + // element, as the value of a Class[] element, in a nested annotation. + JavaFileObject barFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.Bar", + "package foo.bar;", + "", + "@" + Foo.class.getCanonicalName(), + "public abstract class Bar {", + "}"); + JavaFileObject referenceClassFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.ReferenceClass", + "package foo.bar;", + "", + "@interface ReferenceClass {", + " Class<?> value() default Void.class;", + " Class<?>[] values() default {};", + " Nested nested() default @Nested;", + " @interface Nested {", + " Class<?>[] values() default {};", + " }", + "}"); + ImmutableList<String> annotations = ImmutableList.of( + "@ReferenceClass(BarFoo.class)", + "@ReferenceClass(values = {Void.class, BarFoo.class})", + "@ReferenceClass(nested = @ReferenceClass.Nested(values = {Void.class, BarFoo.class}))"); + for (String annotation : annotations) { + JavaFileObject bazFileObject = + JavaFileObjects.forSourceLines( + "foo.bar.Baz", + "package foo.bar;", + "", + "import com.google.auto.value.AutoValue;", + "", + "@AutoValue", + "@AutoValue.CopyAnnotations", + annotation, + "public abstract class Baz {", + " public abstract int foo();", + "", + " public static Baz create(int foo) {", + " return new AutoValue_Baz(foo);", + " }", + "}"); + Compilation compilation = + javac() + .withProcessors(new AutoValueProcessor(), new FooProcessor()) + .withOptions("-Xlint:-processing", "-implicit:none") + .compile(bazFileObject, barFileObject, referenceClassFileObject); + expect.about(compilations()).that(compilation).succeededWithoutWarnings(); + if (compilation.status().equals(Compilation.Status.SUCCESS)) { + expect.about(compilations()).that(compilation) + .generatedSourceFile("foo.bar.AutoValue_Baz") + .contentsAsUtf8String() + .contains(annotation); + } + } + } + + @Test public void annotationReferencesUndefined() { // Test that we don't throw an exception if asked to compile @SuppressWarnings(UNDEFINED) // where UNDEFINED is an undefined symbol. |