/* * 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 data() { ImmutableList.Builder 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 javacOptions; private final String expectedAnnotation; public GeneratedDoesNotExistTest(ImmutableList 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 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 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 partialProxy(Class type, OverridableInvocationHandler handler) { return Reflection.newProxy(type, handler); } private static class ElementsHandler extends OverridableInvocationHandler { private final Set ignoredGenerated; ElementsHandler(Elements original, Set 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 { private final Elements noGeneratedElements; ProcessingEnvironmentHandler(ProcessingEnvironment original, Set 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 { private final Set ignoredGenerated; ProcessorHandler(Processor original, Set 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 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); } }