aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉamonn McManus <emcmanus@google.com>2021-10-19 10:52:20 -0700
committerGoogle Java Core Libraries <java-libraries-firehose+copybara@google.com>2021-10-19 10:53:03 -0700
commite0740327d830597e17273946418f6adc976bc619 (patch)
tree99a493f52552c986d328492ff50ed8d6d563182b
parente66de800e245561108cef75a99214a38af18e872 (diff)
downloadauto-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.java52
-rw-r--r--value/src/test/java/com/google/auto/value/processor/AutoValueCompilationTest.java70
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.