Defer processing of an @AutoValue class with missing types, and attempt to handle it in a later annotation processing round, when those types may have come into existence.
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=79467609
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 cf39ae7..020c598 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
@@ -24,7 +24,6 @@
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -117,25 +116,48 @@
throw new AbortProcessingException();
}
+ /**
+ * Qualified names of {@code @AutoValue} classes that we attempted to process but had to abandon
+ * because we needed other types that they referenced and those other types were missing.
+ */
+ private final List<String> deferredTypeNames = new ArrayList<String>();
+
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
- if (annotations.size() == 1
- && Iterables.getOnlyElement(annotations).getQualifiedName().toString().equals(
- AutoValue.class.getName())) {
- process(roundEnv);
+ List<TypeElement> deferredTypes = new ArrayList<TypeElement>();
+ for (String deferred : deferredTypeNames) {
+ deferredTypes.add(processingEnv.getElementUtils().getTypeElement(deferred));
}
- return false; // never claim annotation, because who knows what other processors want?
- }
-
- private void process(RoundEnvironment roundEnv) {
+ if (roundEnv.processingOver()) {
+ // This means that the previous round didn't generate any new sources, so we can't have found
+ // any new instances of @AutoValue; and we can't have any new types that are the reason a type
+ // was in deferredTypes.
+ for (TypeElement type : deferredTypes) {
+ reportError("Did not generate @AutoValue class for " + type.getQualifiedName()
+ + " because it references undefined types", type);
+ }
+ return false;
+ }
Collection<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(AutoValue.class);
- Collection<? extends TypeElement> types = ElementFilter.typesIn(annotatedElements);
+ List<TypeElement> types = new ImmutableList.Builder<TypeElement>()
+ .addAll(deferredTypes)
+ .addAll(ElementFilter.typesIn(annotatedElements))
+ .build();
+ deferredTypeNames.clear();
for (TypeElement type : types) {
try {
processType(type);
} catch (AbortProcessingException e) {
- // We abandoned this type, but continue with the next.
+ // We abandoned this type; continue with the next.
+ } catch (MissingTypeException e) {
+ // We abandoned this type, but only because we needed another type that it references and
+ // that other type was missing. It is possible that the missing type will be generated by
+ // further annotation processing, so we will try again on the next round (perhaps failing
+ // again and adding it back to the list). We save the name of the @AutoValue type rather
+ // than its TypeElement because it is not guaranteed that it will be represented by
+ // the same TypeElement on the next round.
+ deferredTypeNames.add(type.getQualifiedName().toString());
} catch (RuntimeException e) {
// Don't propagate this exception, which will confusingly crash the compiler.
// Instead, report a compiler error with the stack trace.
@@ -143,6 +165,7 @@
reportError("@AutoValue processor threw an exception: " + trace, type);
}
}
+ return false; // never claim annotation, because who knows what other processors want?
}
private String generatedClassName(TypeElement type, String prefix) {
diff --git a/value/src/main/java/com/google/auto/value/processor/MissingTypeException.java b/value/src/main/java/com/google/auto/value/processor/MissingTypeException.java
new file mode 100644
index 0000000..1411d1c
--- /dev/null
+++ b/value/src/main/java/com/google/auto/value/processor/MissingTypeException.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Exception thrown in the specific case where processing of a class was abandoned because it
+ * required types that the class references to be present and they were not. This case is handled
+ * specially because it is possible that those types might be generated later during annotation
+ * processing, so we should reattempt the processing of the class in a later annotation processing
+ * round.
+ *
+ * @author Éamonn McManus
+ */
+@SuppressWarnings("serial")
+class MissingTypeException extends RuntimeException {
+}
diff --git a/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java b/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java
index 6b24627..0b75eb6 100644
--- a/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java
+++ b/value/src/main/java/com/google/auto/value/processor/TypeSimplifier.java
@@ -80,7 +80,7 @@
* because nested classes in that class or one of its ancestors are in scope in the generated
* subclass, so a reference to another class with the same name as one of them is ambiguous.
*
- * @throws AbortProcessingException if one of the input types contains an error (typically,
+ * @throws MissingTypeException if one of the input types contains an error (typically,
* is undefined). This may be something like {@code UndefinedClass}, or something more subtle
* like {@code Set<UndefinedClass<?>>}.
*/
@@ -357,7 +357,7 @@
}
@Override public Void visitError(ErrorType t, Void p) {
- throw new AbortProcessingException();
+ throw new MissingTypeException();
}
}
@@ -390,7 +390,7 @@
Set<String> simpleNames = new HashSet<String>();
for (TypeMirror type : types) {
if (type.getKind() == TypeKind.ERROR) {
- throw new AbortProcessingException();
+ throw new MissingTypeException();
}
String simpleName = typeUtils.asElement(type).getSimpleName().toString();
if (!simpleNames.add(simpleName)) {
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 ffcda31..a2eff0e 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
@@ -16,20 +16,32 @@
package com.google.auto.value.processor;
import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth.assertAbout;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;
+import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.testing.compile.JavaFileObjects;
import junit.framework.TestCase;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;
/**
@@ -560,4 +572,83 @@
.withErrorContaining(exception.toString())
.in(javaFileObject).onLine(6);
}
+
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Foo {}
+
+ /* Processor that generates an empty class BarFoo every time it sees a class Bar annotated with
+ * @Foo.
+ */
+ public static class FooProcessor extends AbstractProcessor {
+ @Override
+ public Set<String> getSupportedAnnotationTypes() {
+ return ImmutableSet.of(Foo.class.getCanonicalName());
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Foo.class);
+ for (TypeElement type : ElementFilter.typesIn(elements)) {
+ try {
+ generateFoo(type);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+ return false;
+ }
+
+ private void generateFoo(TypeElement type) throws IOException {
+ String pkg = TypeSimplifier.packageNameOf(type);
+ String className = type.getSimpleName().toString();
+ String generatedClassName = className + "Foo";
+ JavaFileObject source =
+ processingEnv.getFiler().createSourceFile(pkg + "." + generatedClassName, type);
+ PrintWriter writer = new PrintWriter(source.openWriter());
+ writer.println("package " + pkg + ";");
+ writer.println("public class " + generatedClassName + " {}");
+ writer.close();
+ }
+ }
+
+ public void testReferencingGeneratedClass() {
+ // Test that ensures that a type that does not exist can be the type of an @AutoValue property
+ // 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.
+ JavaFileObject bazFileObject = JavaFileObjects.forSourceLines(
+ "foo.bar.Baz",
+ "package foo.bar;",
+ "",
+ "import com.google.auto.value.AutoValue;",
+ "",
+ "@AutoValue",
+ "public abstract class Baz {",
+ " public abstract BarFoo barFoo();",
+ "",
+ " public static Baz create(BarFoo barFoo) {",
+ " return new AutoValue_Baz(barFoo);",
+ " }",
+ "}");
+ JavaFileObject barFileObject = JavaFileObjects.forSourceLines(
+ "foo.bar.Bar",
+ "package foo.bar;",
+ "",
+ "import com.google.auto.value.AutoValue;",
+ "",
+ "@" + Foo.class.getCanonicalName(),
+ "public abstract class Bar {",
+ " public abstract BarFoo barFoo();",
+ "}");
+ assertAbout(javaSources())
+ .that(ImmutableList.of(bazFileObject, barFileObject))
+ .processedWith(new AutoValueProcessor(), new FooProcessor())
+ .compilesWithoutError();
+ }
}
diff --git a/value/src/test/java/com/google/auto/value/processor/TypeSimplifierTest.java b/value/src/test/java/com/google/auto/value/processor/TypeSimplifierTest.java
index 0255e39..062b941 100644
--- a/value/src/test/java/com/google/auto/value/processor/TypeSimplifierTest.java
+++ b/value/src/test/java/com/google/auto/value/processor/TypeSimplifierTest.java
@@ -659,7 +659,7 @@
try {
new TypeSimplifier(typeUtil, "foo.bar", ImmutableSet.of(typeWithError), javaLangObject);
fail("Expected exception for type: " + typeWithError);
- } catch (AbortProcessingException expected) {
+ } catch (MissingTypeException expected) {
}
}
}