blob: 7bc67790312467ce56ddded558124ddb1c8ec9df [file] [log] [blame]
/*
* Copyright 2017 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.collect.MoreCollectors.onlyElement;
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 javax.lang.model.util.ElementFilter.fieldsIn;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.truth.Expect;
import com.google.testing.compile.JavaFileObjects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.JavaFileObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* This test verifies the method {@link TypeEncoder#encodeWithAnnotations(TypeMirror).
* It takes a list of "type spellings", like {@code @Nullable String}, and compiles a class
* with one field for each spelling. So there might be a field {@code @Nullable String x2;}.
* Then it examines each compiled field to extract its {@code TypeMirror}, and uses the
* {@code TypeSimplifier} method to reconvert that into a string. It should get back the same
* type spelling in each case.
*
* <p>I originally tried to write a less convoluted test using compile-testing. In my test,
* each type to be tested was an actual type in the test class (the type of a field, or the
* return type of a method). However, I found that if I examined these types by looking up a class
* with {@link javax.lang.model.util.Elements#getTypeElement} and following through to the type
* of interest, it never had any type annotations.
*
* @author emcmanus@google.com (Éamonn McManus)
*/
@RunWith(JUnit4.class)
public class SimplifyWithAnnotationsTest {
@Rule public final Expect expect = Expect.create();
/**
* The types that we will compile and then recreate. They are referenced in a context where {@code
* Set} is unambiguous but not {@code List}, which allows us to test the placement of annotations
* in unqualified types like {@code Set<T>} and qualified types like {@code java.util.List<T>}.
*/
private static final ImmutableList<String> TYPE_SPELLINGS =
ImmutableList.of(
"Object",
"Set",
"String",
"Nullable",
"@Nullable String",
"String[]",
"@Nullable String[]",
"String @Nullable []",
"String @Nullable [] @Nullable []",
"java.awt.List",
"java.util.List<String>",
"Set<@Nullable String>",
"@Nullable Set<String>",
"int",
"@Nullable int", // whatever that might mean
"@Nullable int[]",
"int @Nullable []",
"T",
"@Nullable T",
"Set<@Nullable T>",
"Set<? extends @Nullable T>",
"Set<? extends @Nullable String>",
"Set<? extends @Nullable String @Nullable []>",
"java.util.@Nullable List<@Nullable T>",
"java.util.@Nullable List<java.util.@Nullable List<T>>");
private static final JavaFileObject NULLABLE_FILE_OBJECT =
JavaFileObjects.forSourceLines(
"pkg.Nullable",
"package pkg;",
"",
"import java.lang.annotation.ElementType;",
"import java.lang.annotation.Target;",
"",
"@Target(ElementType.TYPE_USE)",
"public @interface Nullable {}");
private static final JavaFileObject TEST_CLASS_FILE_OBJECT =
JavaFileObjects.forSourceLines("pkg.TestClass", buildTestClass());
private static ImmutableList<String> buildTestClass() {
// Some older versions of javac don't handle type annotations at all well in annotation
// processors. The `witness` method in the generated class is there to detect that, and
// skip the test if it is the case.
ImmutableList.Builder<String> builder = ImmutableList.builder();
builder.add(
"package pkg;",
"",
"import java.util.Set;",
"",
"public abstract class TestClass<T> {",
" abstract @Nullable T witness();");
int i = 0;
for (String typeSpelling : TYPE_SPELLINGS) {
builder.add(String.format(" %s x%d;\n", typeSpelling, i++));
}
builder.add("}");
return builder.build();
}
@Test
public void testSimplifyWithAnnotations() {
// The real test happens inside the .compile(...), by virtue of TestProcessor.
assertThat(
javac()
.withOptions("-proc:only")
.withProcessors(new TestProcessor())
.compile(NULLABLE_FILE_OBJECT, TEST_CLASS_FILE_OBJECT))
.succeededWithoutWarnings();
}
@SupportedAnnotationTypes("*")
private static class TestProcessor extends AbstractProcessor {
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
TypeElement testClass = processingEnv.getElementUtils().getTypeElement("pkg.TestClass");
testTypeSpellings(testClass);
}
return false;
}
void testTypeSpellings(TypeElement testClass) {
ExecutableElement witness =
ElementFilter.methodsIn(testClass.getEnclosedElements()).stream()
.filter(m -> m.getSimpleName().contentEquals("witness"))
.collect(onlyElement());
if (witness.getReturnType().getAnnotationMirrors().isEmpty()) {
System.err.println("SKIPPING TEST BECAUSE OF BUGGY COMPILER");
return;
}
ImmutableMap<String, TypeMirror> typeSpellingToType = typesFromTestClass(testClass);
assertThat(typeSpellingToType).isNotEmpty();
StringBuilder text = new StringBuilder();
StringBuilder expected = new StringBuilder();
// Build up a fake source text with the encodings for the types in it, and decode it to
// ensure the type spellings are what we expect.
typeSpellingToType.forEach(
(typeSpelling, type) -> {
text.append("{").append(TypeEncoder.encodeWithAnnotations(type)).append("}");
expected.append("{").append(typeSpelling).append("}");
});
String decoded = TypeEncoder.decode(text.toString(), processingEnv, "pkg", null);
assertThat(decoded).isEqualTo(expected.toString());
}
private static ImmutableMap<String, TypeMirror> typesFromTestClass(TypeElement type) {
// Reads the types of the fields from the compiled TestClass and uses them to produce
// a map from type spellings to types. This method depends on type.getEnclosedElements()
// returning the fields in source order, which it is specified to do.
ImmutableMap.Builder<String, TypeMirror> typeSpellingToTypeBuilder = ImmutableMap.builder();
int i = 0;
for (VariableElement field : fieldsIn(type.getEnclosedElements())) {
String spelling = TYPE_SPELLINGS.get(i);
typeSpellingToTypeBuilder.put(spelling, field.asType());
i++;
}
return typeSpellingToTypeBuilder.build();
}
}
}