blob: f3d3d6110b59bc951c8c6134ebeb2844011ff16b [file] [log] [blame]
/*
* Copyright 2015 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.value.processor;
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 com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.Reflection;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.JavaFileObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* Tests that {@link AutoValueProcessor} works even if run in a context where the {@code @Generated}
* annotation does not exist.
*
* @author emcmanus@google.com (Éamonn McManus)
*/
@RunWith(Parameterized.class)
public class GeneratedDoesNotExistTest {
@Parameters(name = "{0}")
public static Collection<Object[]> data() {
ImmutableList.Builder<Object[]> params = ImmutableList.builder();
if (SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0) {
// use default options when running on JDK > 8
// TODO(b/72513371): use --release 8 once compile-testing supports that
params.add(
new Object[] {
ImmutableList.of(), "javax.annotation.processing.Generated",
});
}
params.add(
new Object[] {
ImmutableList.of("-source", "8", "-target", "8"), "javax.annotation.Generated",
});
return params.build();
}
private final ImmutableList<String> javacOptions;
private final String expectedAnnotation;
public GeneratedDoesNotExistTest(ImmutableList<String> javacOptions, String expectedAnnotation) {
this.javacOptions = javacOptions;
this.expectedAnnotation = expectedAnnotation;
}
// The classes here are basically just rigmarole to ensure that
// Types.getTypeElement("javax.annotation.Generated") returns null, and to check that something
// called that. We want a Processor that forwards everything to AutoValueProcessor, except that
// the init(ProcessingEnvironment) method should forward a ProcessingEnvironment that filters
// out the Generated class. So that ProcessingEnvironment forwards everything to the real
// ProcessingEnvironment, except the ProcessingEnvironment.getElementUtils() method. That method
// returns an Elements object that forwards everything to the real Elements except
// getTypeElement("javax.annotation.Generated") and
// getTypeElement("javax.annotation.processing.Generated").
private static final ImmutableSet<String> GENERATED_ANNOTATIONS =
ImmutableSet.of("javax.annotation.Generated", "javax.annotation.processing.Generated");
/**
* InvocationHandler that forwards every method to an original object, except methods where there
* is an implementation in this class with the same signature. So for example in the subclass
* {@link ElementsHandler} there is a method {@link ElementsHandler#getTypeElement(CharSequence)},
* which means that a call of {@link Elements#getTypeElement(CharSequence)} on the proxy with this
* invocation handler will end up calling that method, but a call of any of the other methods of
* {@code Elements} will end up calling the method on the original object.
*/
private abstract static class OverridableInvocationHandler<T> implements InvocationHandler {
final T original;
OverridableInvocationHandler(T original) {
this.original = original;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Method override = getClass().getMethod(method.getName(), method.getParameterTypes());
if (override.getDeclaringClass() == getClass()) {
return override.invoke(this, args);
}
} catch (NoSuchMethodException ignored) {
// OK: we don't have an override for this method, so just invoke the original method.
}
return method.invoke(original, args);
}
}
private static <T> T partialProxy(Class<T> type, OverridableInvocationHandler<T> handler) {
return Reflection.newProxy(type, handler);
}
private static class ElementsHandler extends OverridableInvocationHandler<Elements> {
private final Set<String> ignoredGenerated;
ElementsHandler(Elements original, Set<String> ignoredGenerated) {
super(original);
this.ignoredGenerated = ignoredGenerated;
}
public TypeElement getTypeElement(CharSequence name) {
if (GENERATED_ANNOTATIONS.contains(name.toString())) {
ignoredGenerated.add(name.toString());
return null;
} else {
return original.getTypeElement(name);
}
}
}
private static class ProcessingEnvironmentHandler
extends OverridableInvocationHandler<ProcessingEnvironment> {
private final Elements noGeneratedElements;
ProcessingEnvironmentHandler(ProcessingEnvironment original, Set<String> ignoredGenerated) {
super(original);
ElementsHandler elementsHandler =
new ElementsHandler(original.getElementUtils(), ignoredGenerated);
this.noGeneratedElements = partialProxy(Elements.class, elementsHandler);
}
public Elements getElementUtils() {
return noGeneratedElements;
}
}
private static class ProcessorHandler extends OverridableInvocationHandler<Processor> {
private final Set<String> ignoredGenerated;
ProcessorHandler(Processor original, Set<String> ignoredGenerated) {
super(original);
this.ignoredGenerated = ignoredGenerated;
}
public void init(ProcessingEnvironment processingEnv) {
ProcessingEnvironmentHandler processingEnvironmentHandler =
new ProcessingEnvironmentHandler(processingEnv, ignoredGenerated);
ProcessingEnvironment noGeneratedProcessingEnvironment =
partialProxy(ProcessingEnvironment.class, processingEnvironmentHandler);
original.init(noGeneratedProcessingEnvironment);
}
}
@Test
public void test() {
JavaFileObject javaFileObject =
JavaFileObjects.forSourceLines(
"foo.bar.Baz",
"package foo.bar;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"public abstract class Baz {",
" public static Baz create() {",
" return new AutoValue_Baz();",
" }",
"}");
JavaFileObject expectedOutput =
JavaFileObjects.forSourceLines(
"foo.bar.AutoValue_Baz",
"package foo.bar;",
"",
"final class AutoValue_Baz extends Baz {",
" AutoValue_Baz() {",
" }",
"",
" @Override public String toString() {",
" return \"Baz{\"",
" + \"}\";",
" }",
"",
" @Override public boolean equals(Object o) {",
" if (o == this) {",
" return true;",
" }",
" if (o instanceof Baz) {",
" return true;",
" }",
" return false;",
" }",
"",
" @Override public int hashCode() {",
" int h$ = 1;",
" return h$;",
" }",
"}");
Set<String> ignoredGenerated = ConcurrentHashMap.newKeySet();
Processor autoValueProcessor = new AutoValueProcessor();
ProcessorHandler handler = new ProcessorHandler(autoValueProcessor, ignoredGenerated);
Processor noGeneratedProcessor = partialProxy(Processor.class, handler);
Compilation compilation =
javac()
.withOptions(javacOptions)
.withProcessors(noGeneratedProcessor)
.compile(javaFileObject);
assertThat(compilation).succeededWithoutWarnings();
assertThat(compilation)
.generatedSourceFile("foo.bar.AutoValue_Baz")
.hasSourceEquivalentTo(expectedOutput);
assertThat(ignoredGenerated).containsExactly(expectedAnnotation);
}
}