blob: f75345032df14800aa764e1997a66f122bbac2de [file] [log] [blame]
/*
* Copyright 2014 Google LLC
*
* 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.common;
import static com.google.common.collect.Multimaps.transformValues;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;
import static com.google.testing.compile.JavaSourcesSubjectFactory.javaSources;
import static javax.tools.Diagnostic.Kind.ERROR;
import static javax.tools.StandardLocation.SOURCE_OUTPUT;
import com.google.auto.common.BasicAnnotationProcessor.ProcessingStep;
import com.google.auto.common.BasicAnnotationProcessor.Step;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.truth.Correspondence;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.CompilationRule;
import com.google.testing.compile.JavaFileObjects;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.annotation.processing.Filer;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.JavaFileObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class BasicAnnotationProcessorTest {
private abstract static class BaseAnnotationProcessor extends BasicAnnotationProcessor {
static final String ENCLOSING_CLASS_NAME =
BasicAnnotationProcessorTest.class.getCanonicalName();
@Override
public final SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
@Retention(RetentionPolicy.SOURCE)
public @interface RequiresGeneratedCode {}
/**
* Rejects elements unless the class generated by {@link GeneratesCode}'s processor is present.
*/
private static class RequiresGeneratedCodeProcessor extends BaseAnnotationProcessor {
int rejectedRounds;
final ImmutableList.Builder<ImmutableSetMultimap<String, Element>> processArguments =
ImmutableList.builder();
@Override
protected Iterable<? extends Step> steps() {
return ImmutableSet.of(
new Step() {
@Override
public ImmutableSet<? extends Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
processArguments.add(ImmutableSetMultimap.copyOf(elementsByAnnotation));
TypeElement requiredClass =
processingEnv.getElementUtils().getTypeElement("test.SomeGeneratedClass");
if (requiredClass == null) {
rejectedRounds++;
return ImmutableSet.copyOf(elementsByAnnotation.values());
}
generateClass(processingEnv.getFiler(), "GeneratedByRequiresGeneratedCodeProcessor");
return ImmutableSet.of();
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".RequiresGeneratedCode");
}
},
new Step() {
@Override
public ImmutableSet<? extends Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
return ImmutableSet.of();
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".AnAnnotation");
}
});
}
ImmutableList<ImmutableSetMultimap<String, Element>> processArguments() {
return processArguments.build();
}
}
@Retention(RetentionPolicy.SOURCE)
public @interface GeneratesCode {}
/** Generates a class called {@code test.SomeGeneratedClass}. */
public static class GeneratesCodeProcessor extends BaseAnnotationProcessor {
@Override
protected Iterable<? extends Step> steps() {
return ImmutableSet.of(
new Step() {
@Override
public ImmutableSet<? extends Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
generateClass(processingEnv.getFiler(), "SomeGeneratedClass");
return ImmutableSet.of();
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".GeneratesCode");
}
});
}
}
public @interface AnAnnotation {}
/** When annotating a type {@code Foo}, generates a class called {@code FooXYZ}. */
public static class AnAnnotationProcessor extends BaseAnnotationProcessor {
@Override
protected Iterable<? extends Step> steps() {
return ImmutableSet.of(
new Step() {
@Override
public ImmutableSet<Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
for (Element element : elementsByAnnotation.values()) {
generateClass(processingEnv.getFiler(), element.getSimpleName() + "XYZ");
}
return ImmutableSet.of();
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".AnAnnotation");
}
});
}
}
/** An annotation which causes an annotation processing error. */
public @interface CauseError {}
/** Report an error for any class annotated. */
public static class CauseErrorProcessor extends BaseAnnotationProcessor {
@Override
protected Iterable<? extends Step> steps() {
return ImmutableSet.of(
new Step() {
@Override
public ImmutableSet<Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
for (Element e : elementsByAnnotation.values()) {
processingEnv.getMessager().printMessage(ERROR, "purposeful error", e);
}
return ImmutableSet.copyOf(elementsByAnnotation.values());
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(ENCLOSING_CLASS_NAME + ".CauseError");
}
});
}
}
public static class MissingAnnotationProcessor extends BaseAnnotationProcessor {
private ImmutableSetMultimap<String, Element> elementsByAnnotation;
@Override
protected Iterable<? extends Step> steps() {
return ImmutableSet.of(
new Step() {
@Override
public ImmutableSet<Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
MissingAnnotationProcessor.this.elementsByAnnotation = elementsByAnnotation;
for (Element element : elementsByAnnotation.values()) {
generateClass(processingEnv.getFiler(), element.getSimpleName() + "XYZ");
}
return ImmutableSet.of();
}
@Override
public ImmutableSet<String> annotations() {
return ImmutableSet.of(
"test.SomeNonExistentClass", ENCLOSING_CLASS_NAME + ".AnAnnotation");
}
});
}
ImmutableSetMultimap<String, Element> getElementsByAnnotation() {
return elementsByAnnotation;
}
}
@SuppressWarnings("deprecation") // Deprecated ProcessingStep is being explicitly tested.
static final class MultiAnnotationProcessingStep implements ProcessingStep {
private SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation;
@Override
public ImmutableSet<? extends Class<? extends Annotation>> annotations() {
return ImmutableSet.of(AnAnnotation.class, ReferencesAClass.class);
}
@Override
public ImmutableSet<? extends Element> process(
SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation) {
this.elementsByAnnotation = elementsByAnnotation;
return ImmutableSet.of();
}
SetMultimap<Class<? extends Annotation>, Element> getElementsByAnnotation() {
return elementsByAnnotation;
}
}
@Retention(RetentionPolicy.SOURCE)
public @interface ReferencesAClass {
Class<?> value();
}
@Rule public CompilationRule compilation = new CompilationRule();
@Test
public void properlyDefersProcessing_typeElement() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + RequiresGeneratedCode.class.getCanonicalName(),
"public class ClassA {",
" SomeGeneratedClass sgc;",
"}");
JavaFileObject classBFileObject =
JavaFileObjects.forSourceLines(
"test.ClassB",
"package test;",
"",
"@" + GeneratesCode.class.getCanonicalName(),
"public class ClassB {}");
RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor =
new RequiresGeneratedCodeProcessor();
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject, classBFileObject))
.processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor())
.compilesWithoutError()
.and()
.generatesFileNamed(
SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java");
assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0);
}
@Test
public void properlyDefersProcessing_nestedTypeValidBeforeOuterType() {
JavaFileObject source =
JavaFileObjects.forSourceLines(
"test.ValidInRound2",
"package test;",
"",
"@" + AnAnnotation.class.getCanonicalName(),
"public class ValidInRound2 {",
" ValidInRound1XYZ vir1xyz;",
" @" + AnAnnotation.class.getCanonicalName(),
" static class ValidInRound1 {}",
"}");
assertAbout(javaSource())
.that(source)
.processedWith(new AnAnnotationProcessor())
.compilesWithoutError()
.and()
.generatesFileNamed(SOURCE_OUTPUT, "test", "ValidInRound2XYZ.java");
}
@Test
public void properlyDefersProcessing_packageElement() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + GeneratesCode.class.getCanonicalName(),
"public class ClassA {",
"}");
JavaFileObject packageFileObject =
JavaFileObjects.forSourceLines(
"test.package-info",
"@" + RequiresGeneratedCode.class.getCanonicalName(),
"@" + ReferencesAClass.class.getCanonicalName() + "(SomeGeneratedClass.class)",
"package test;");
RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor =
new RequiresGeneratedCodeProcessor();
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject, packageFileObject))
.processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor())
.compilesWithoutError()
.and()
.generatesFileNamed(
SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java");
assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0);
}
@Test
public void properlyDefersProcessing_argumentElement() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"public class ClassA {",
" SomeGeneratedClass sgc;",
" public void myMethod(@"
+ RequiresGeneratedCode.class.getCanonicalName()
+ " int myInt)",
" {}",
"}");
JavaFileObject classBFileObject =
JavaFileObjects.forSourceLines(
"test.ClassB",
"package test;",
"",
"public class ClassB {",
" public void myMethod(@" + GeneratesCode.class.getCanonicalName() + " int myInt) {}",
"}");
RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor =
new RequiresGeneratedCodeProcessor();
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject, classBFileObject))
.processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor())
.compilesWithoutError()
.and()
.generatesFileNamed(
SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java");
assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(0);
}
@Test
public void properlyDefersProcessing_rejectsElement() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + RequiresGeneratedCode.class.getCanonicalName(),
"public class ClassA {",
" @" + AnAnnotation.class.getCanonicalName(),
" public void method() {}",
"}");
JavaFileObject classBFileObject =
JavaFileObjects.forSourceLines(
"test.ClassB",
"package test;",
"",
"@" + GeneratesCode.class.getCanonicalName(),
"public class ClassB {}");
RequiresGeneratedCodeProcessor requiresGeneratedCodeProcessor =
new RequiresGeneratedCodeProcessor();
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject, classBFileObject))
.processedWith(requiresGeneratedCodeProcessor, new GeneratesCodeProcessor())
.compilesWithoutError()
.and()
.generatesFileNamed(
SOURCE_OUTPUT, "test", "GeneratedByRequiresGeneratedCodeProcessor.java");
assertThat(requiresGeneratedCodeProcessor.rejectedRounds).isEqualTo(1);
// Re b/118372780: Assert that the right deferred elements are passed back, and not any enclosed
// elements annotated with annotations from a different step.
assertThat(requiresGeneratedCodeProcessor.processArguments())
.comparingElementsUsing(setMultimapValuesByString())
.containsExactly(
ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA"),
ImmutableSetMultimap.of(RequiresGeneratedCode.class.getCanonicalName(), "test.ClassA"))
.inOrder();
}
private static <K, V>
Correspondence<SetMultimap<K, V>, SetMultimap<K, String>> setMultimapValuesByString() {
return Correspondence.from(
(actual, expected) ->
ImmutableSetMultimap.copyOf(transformValues(actual, Object::toString)).equals(expected),
"is equivalent comparing multimap values by `toString()` to");
}
@Test
public void properlySkipsMissingAnnotations_generatesClass() {
JavaFileObject source =
JavaFileObjects.forSourceLines(
"test.ValidInRound2",
"package test;",
"",
"@" + AnAnnotation.class.getCanonicalName(),
"public class ValidInRound2 {",
" ValidInRound1XYZ vir1xyz;",
" @" + AnAnnotation.class.getCanonicalName(),
" static class ValidInRound1 {}",
"}");
Compilation compilation =
javac().withProcessors(new MissingAnnotationProcessor()).compile(source);
assertThat(compilation).succeeded();
assertThat(compilation).generatedSourceFile("test.ValidInRound2XYZ");
}
@Test
public void properlySkipsMissingAnnotations_passesValidAnnotationsToProcess() {
JavaFileObject source =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + AnAnnotation.class.getCanonicalName(),
"public class ClassA {",
"}");
MissingAnnotationProcessor missingAnnotationProcessor = new MissingAnnotationProcessor();
assertThat(javac().withProcessors(missingAnnotationProcessor).compile(source)).succeeded();
assertThat(missingAnnotationProcessor.getElementsByAnnotation().keySet())
.containsExactly(AnAnnotation.class.getCanonicalName());
assertThat(missingAnnotationProcessor.getElementsByAnnotation().values()).hasSize(1);
}
@Test
public void reportsMissingType() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + RequiresGeneratedCode.class.getCanonicalName(),
"public class ClassA {",
" SomeGeneratedClass bar;",
"}");
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject))
.processedWith(new RequiresGeneratedCodeProcessor())
.failsToCompile()
.withErrorContaining(RequiresGeneratedCodeProcessor.class.getCanonicalName())
.in(classAFileObject)
.onLine(4);
}
@Test
public void reportsMissingTypeSuppressedWhenOtherErrors() {
JavaFileObject classAFileObject =
JavaFileObjects.forSourceLines(
"test.ClassA",
"package test;",
"",
"@" + CauseError.class.getCanonicalName(),
"public class ClassA {}");
assertAbout(javaSources())
.that(ImmutableList.of(classAFileObject))
.processedWith(new CauseErrorProcessor())
.failsToCompile()
.withErrorCount(1)
.withErrorContaining("purposeful");
}
@Test
public void processingStepAsStepAnnotationsNamesMatchClasses() {
Step step = BasicAnnotationProcessor.asStep(new MultiAnnotationProcessingStep());
assertThat(step.annotations())
.containsExactly(
AnAnnotation.class.getCanonicalName(), ReferencesAClass.class.getCanonicalName());
}
/**
* Tests that a {@link ProcessingStep} passed to {@link
* BasicAnnotationProcessor#asStep(ProcessingStep)} still gets passed the correct arguments to
* {@link Step#process(ImmutableSetMultimap)}.
*/
@Test
public void processingStepAsStepProcessElementsMatchClasses() {
Elements elements = compilation.getElements();
String anAnnotationName = AnAnnotation.class.getCanonicalName();
String referencesAClassName = ReferencesAClass.class.getCanonicalName();
TypeElement anAnnotationElement = elements.getTypeElement(anAnnotationName);
TypeElement referencesAClassElement = elements.getTypeElement(referencesAClassName);
MultiAnnotationProcessingStep processingStep = new MultiAnnotationProcessingStep();
BasicAnnotationProcessor.asStep(processingStep)
.process(
ImmutableSetMultimap.of(
anAnnotationName,
anAnnotationElement,
referencesAClassName,
referencesAClassElement));
assertThat(processingStep.getElementsByAnnotation())
.containsExactly(
AnAnnotation.class,
anAnnotationElement,
ReferencesAClass.class,
referencesAClassElement);
}
private static void generateClass(Filer filer, String generatedClassName) {
PrintWriter writer = null;
try {
writer = new PrintWriter(filer.createSourceFile("test." + generatedClassName).openWriter());
writer.println("package test;");
writer.println("public class " + generatedClassName + " {}");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (writer != null) {
writer.close();
}
}
}
}