blob: 83951e0a58647a1fd5207aa0b8434115cb9d4bc5 [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.truth.Truth.assertThat;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import static java.util.stream.Collectors.joining;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.processor.MissingTypes.MissingTypeException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.CompilationRule;
import com.google.testing.compile.JavaFileObjects;
import java.util.List;
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.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TypeEncoder}.
*
* @author emcmanus@google.com (Éamonn McManus)
*/
@RunWith(JUnit4.class)
public class TypeEncoderTest {
@Rule public final CompilationRule compilationRule = new CompilationRule();
private Types typeUtils;
private Elements elementUtils;
@Before
public void setUp() {
typeUtils = compilationRule.getTypes();
elementUtils = compilationRule.getElements();
}
/**
* Assert that the fake program returned by fakeProgramForTypes has the given list of imports and
* the given list of spellings. Here, "spellings" means the way each type is referenced in the
* decoded program, for example {@code Timer} if {@code java.util.Timer} can be imported, or
* {@code java.util.Timer} if not.
*
* <p>We construct a fake program that references each of the given types in turn.
* TypeEncoder.decode doesn't have any real notion of Java syntax, so our program just consists of
* START and END markers around the {@code `import`} tag, followed by each type in braces, as
* encoded by TypeEncoder.encode. Once decoded, the program should consist of the appropriate
* imports (inside START...END) and each type in braces, spelled appropriately.
*
* @param fakePackage the package that TypeEncoder should consider the fake program to be in.
* Classes in the same package don't usually need to be imported.
*/
private void assertTypeImportsAndSpellings(
Set<TypeMirror> types, String fakePackage, List<String> imports, List<String> spellings) {
String fakeProgram =
"START\n`import`\nEND\n"
+ types.stream().map(TypeEncoder::encode).collect(joining("}\n{", "{", "}"));
String decoded =
TypeEncoder.decode(
fakeProgram, elementUtils, typeUtils, fakePackage, baseWithoutContainedTypes());
String expected =
"START\n"
+ imports.stream().map(s -> "import " + s + ";\n").collect(joining())
+ "\nEND\n"
+ spellings.stream().collect(joining("}\n{", "{", "}"));
assertThat(decoded).isEqualTo(expected);
}
private static class MultipleBounds<K extends List<V> & Comparable<K>, V> {}
@Test
public void testImportsForNoTypes() {
assertTypeImportsAndSpellings(
typeMirrorSet(), "foo.bar", ImmutableList.of(), ImmutableList.of());
}
@Test
public void testImportsForImplicitlyImportedTypes() {
Set<TypeMirror> types =
typeMirrorSet(
typeMirrorOf(java.lang.String.class),
typeMirrorOf(javax.management.MBeanServer.class), // Same package, so no import.
typeUtils.getPrimitiveType(TypeKind.INT),
typeUtils.getPrimitiveType(TypeKind.BOOLEAN));
assertTypeImportsAndSpellings(
types,
"javax.management",
ImmutableList.of(),
ImmutableList.of("String", "MBeanServer", "int", "boolean"));
}
@Test
public void testImportsForPlainTypes() {
Set<TypeMirror> types =
typeMirrorSet(
typeUtils.getPrimitiveType(TypeKind.INT),
typeMirrorOf(java.lang.String.class),
typeMirrorOf(java.net.Proxy.class),
typeMirrorOf(java.net.Proxy.Type.class),
typeMirrorOf(java.util.regex.Pattern.class),
typeMirrorOf(javax.management.MBeanServer.class));
assertTypeImportsAndSpellings(
types,
"foo.bar",
ImmutableList.of(
"java.net.Proxy", "java.util.regex.Pattern", "javax.management.MBeanServer"),
ImmutableList.of("int", "String", "Proxy", "Proxy.Type", "Pattern", "MBeanServer"));
}
@Test
public void testImportsForComplicatedTypes() {
TypeElement list = typeElementOf(java.util.List.class);
TypeElement map = typeElementOf(java.util.Map.class);
Set<TypeMirror> types =
typeMirrorSet(
typeUtils.getPrimitiveType(TypeKind.INT),
typeMirrorOf(java.util.regex.Pattern.class),
typeUtils.getDeclaredType(
list, // List<Timer>
typeMirrorOf(java.util.Timer.class)),
typeUtils.getDeclaredType(
map, // Map<? extends Timer, ? super BigInteger>
typeUtils.getWildcardType(typeMirrorOf(java.util.Timer.class), null),
typeUtils.getWildcardType(null, typeMirrorOf(java.math.BigInteger.class))));
// Timer is referenced twice but should obviously only be imported once.
assertTypeImportsAndSpellings(
types,
"foo.bar",
ImmutableList.of(
"java.math.BigInteger",
"java.util.List",
"java.util.Map",
"java.util.Timer",
"java.util.regex.Pattern"),
ImmutableList.of(
"int", "Pattern", "List<Timer>", "Map<? extends Timer, ? super BigInteger>"));
}
@Test
public void testImportsForArrayTypes() {
TypeElement list = typeElementOf(java.util.List.class);
TypeElement set = typeElementOf(java.util.Set.class);
Set<TypeMirror> types =
typeMirrorSet(
typeUtils.getArrayType(typeUtils.getPrimitiveType(TypeKind.INT)),
typeUtils.getArrayType(typeMirrorOf(java.util.regex.Pattern.class)),
typeUtils.getArrayType( // Set<Matcher[]>[]
typeUtils.getDeclaredType(
set, typeUtils.getArrayType(typeMirrorOf(java.util.regex.Matcher.class)))),
typeUtils.getDeclaredType(
list, // List<Timer[]>
typeUtils.getArrayType(typeMirrorOf(java.util.Timer.class))));
// Timer is referenced twice but should obviously only be imported once.
assertTypeImportsAndSpellings(
types,
"foo.bar",
ImmutableList.of(
"java.util.List",
"java.util.Set",
"java.util.Timer",
"java.util.regex.Matcher",
"java.util.regex.Pattern"),
ImmutableList.of("int[]", "Pattern[]", "Set<Matcher[]>[]", "List<Timer[]>"));
}
@Test
public void testImportNestedType() {
Set<TypeMirror> types = typeMirrorSet(typeMirrorOf(java.net.Proxy.Type.class));
assertTypeImportsAndSpellings(
types, "foo.bar", ImmutableList.of("java.net.Proxy"), ImmutableList.of("Proxy.Type"));
}
@Test
public void testImportsForAmbiguousNames() {
TypeMirror wildcard = typeUtils.getWildcardType(null, null);
Set<TypeMirror> types =
typeMirrorSet(
typeUtils.getPrimitiveType(TypeKind.INT),
typeMirrorOf(java.awt.List.class),
typeMirrorOf(java.lang.String.class),
typeUtils.getDeclaredType( // List<?>
typeElementOf(java.util.List.class), wildcard),
typeUtils.getDeclaredType( // Map<?, ?>
typeElementOf(java.util.Map.class), wildcard, wildcard));
assertTypeImportsAndSpellings(
types,
"foo.bar",
ImmutableList.of("java.util.Map"),
ImmutableList.of("int", "java.awt.List", "String", "java.util.List<?>", "Map<?, ?>"));
}
@Test
public void testSimplifyJavaLangString() {
Set<TypeMirror> types = typeMirrorSet(typeMirrorOf(java.lang.String.class));
assertTypeImportsAndSpellings(types, "foo.bar", ImmutableList.of(), ImmutableList.of("String"));
}
@Test
public void testSimplifyJavaLangThreadState() {
Set<TypeMirror> types = typeMirrorSet(typeMirrorOf(java.lang.Thread.State.class));
assertTypeImportsAndSpellings(
types, "foo.bar", ImmutableList.of(), ImmutableList.of("Thread.State"));
}
@Test
public void testSimplifyJavaLangNamesake() {
TypeMirror javaLangType = typeMirrorOf(java.lang.RuntimePermission.class);
TypeMirror notJavaLangType =
typeMirrorOf(com.google.auto.value.processor.testclasses.RuntimePermission.class);
Set<TypeMirror> types = typeMirrorSet(javaLangType, notJavaLangType);
assertTypeImportsAndSpellings(
types,
"foo.bar",
ImmutableList.of(),
ImmutableList.of(javaLangType.toString(), notJavaLangType.toString()));
}
@Test
public void testSimplifyComplicatedTypes() {
// This test constructs a set of types and feeds them to TypeEncoder. Then it verifies that
// the resultant rewrites of those types are what we would expect.
TypeElement list = typeElementOf(java.util.List.class);
TypeElement map = typeElementOf(java.util.Map.class);
TypeMirror string = typeMirrorOf(java.lang.String.class);
TypeMirror integer = typeMirrorOf(java.lang.Integer.class);
TypeMirror pattern = typeMirrorOf(java.util.regex.Pattern.class);
TypeMirror timer = typeMirrorOf(java.util.Timer.class);
TypeMirror bigInteger = typeMirrorOf(java.math.BigInteger.class);
ImmutableMap<TypeMirror, String> typeMap =
ImmutableMap.<TypeMirror, String>builder()
.put(typeUtils.getPrimitiveType(TypeKind.INT), "int")
.put(typeUtils.getArrayType(typeUtils.getPrimitiveType(TypeKind.BYTE)), "byte[]")
.put(pattern, "Pattern")
.put(typeUtils.getArrayType(pattern), "Pattern[]")
.put(typeUtils.getArrayType(typeUtils.getArrayType(pattern)), "Pattern[][]")
.put(typeUtils.getDeclaredType(list, typeUtils.getWildcardType(null, null)), "List<?>")
.put(typeUtils.getDeclaredType(list, timer), "List<Timer>")
.put(typeUtils.getDeclaredType(map, string, integer), "Map<String, Integer>")
.put(
typeUtils.getDeclaredType(
map,
typeUtils.getWildcardType(timer, null),
typeUtils.getWildcardType(null, bigInteger)),
"Map<? extends Timer, ? super BigInteger>")
.build();
assertTypeImportsAndSpellings(
typeMap.keySet(),
"foo.bar",
ImmutableList.of(
"java.math.BigInteger",
"java.util.List",
"java.util.Map",
"java.util.Timer",
"java.util.regex.Pattern"),
ImmutableList.copyOf(typeMap.values()));
}
@Test
public void testSimplifyMultipleBounds() {
TypeElement multipleBoundsElement = typeElementOf(MultipleBounds.class);
TypeMirror multipleBoundsMirror = multipleBoundsElement.asType();
String text = "`import`\n";
text += "{" + TypeEncoder.encode(multipleBoundsMirror) + "}";
text += "{" + TypeEncoder.typeParametersString(multipleBoundsElement.getTypeParameters()) + "}";
String myPackage = getClass().getPackage().getName();
String decoded =
TypeEncoder.decode(text, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
String expected =
"import java.util.List;\n\n"
+ "{TypeEncoderTest.MultipleBounds<K, V>}"
+ "{<K extends List<V> & Comparable<K>, V>}";
assertThat(decoded).isEqualTo(expected);
}
@SuppressWarnings("ClassCanBeStatic")
static class Outer<T extends Number> {
class InnerWithoutTypeParam {}
class Middle<U> {
class InnerWithTypeParam<V> {}
}
}
@Test
public void testOuterParameterizedInnerNot() {
TypeElement outerElement = typeElementOf(Outer.class);
DeclaredType doubleMirror = typeMirrorOf(Double.class);
DeclaredType outerOfDoubleMirror = typeUtils.getDeclaredType(outerElement, doubleMirror);
TypeElement innerWithoutTypeParamElement = typeElementOf(Outer.InnerWithoutTypeParam.class);
DeclaredType parameterizedInnerWithoutTypeParam =
typeUtils.getDeclaredType(outerOfDoubleMirror, innerWithoutTypeParamElement);
String encoded = TypeEncoder.encode(parameterizedInnerWithoutTypeParam);
String myPackage = getClass().getPackage().getName();
String decoded =
TypeEncoder.decode(
encoded, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
String expected = "TypeEncoderTest.Outer<Double>.InnerWithoutTypeParam";
assertThat(decoded).isEqualTo(expected);
}
@Test
public void testOuterParameterizedInnerAlso() {
TypeElement outerElement = typeElementOf(Outer.class);
DeclaredType doubleMirror = typeMirrorOf(Double.class);
DeclaredType outerOfDoubleMirror = typeUtils.getDeclaredType(outerElement, doubleMirror);
TypeElement middleElement = typeElementOf(Outer.Middle.class);
DeclaredType stringMirror = typeMirrorOf(String.class);
DeclaredType middleOfStringMirror =
typeUtils.getDeclaredType(outerOfDoubleMirror, middleElement, stringMirror);
TypeElement innerWithTypeParamElement = typeElementOf(Outer.Middle.InnerWithTypeParam.class);
DeclaredType integerMirror = typeMirrorOf(Integer.class);
DeclaredType parameterizedInnerWithTypeParam =
typeUtils.getDeclaredType(middleOfStringMirror, innerWithTypeParamElement, integerMirror);
String encoded = TypeEncoder.encode(parameterizedInnerWithTypeParam);
String myPackage = getClass().getPackage().getName();
String decoded =
TypeEncoder.decode(
encoded, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
String expected = "TypeEncoderTest.Outer<Double>.Middle<String>.InnerWithTypeParam<Integer>";
assertThat(decoded).isEqualTo(expected);
}
private static Set<TypeMirror> typeMirrorSet(TypeMirror... typeMirrors) {
Set<TypeMirror> set = new TypeMirrorSet();
for (TypeMirror typeMirror : typeMirrors) {
assertThat(set.add(typeMirror)).isTrue();
}
return set;
}
private TypeElement typeElementOf(Class<?> c) {
return elementUtils.getTypeElement(c.getCanonicalName());
}
private DeclaredType typeMirrorOf(Class<?> c) {
return MoreTypes.asDeclared(typeElementOf(c).asType());
}
/**
* Returns a "base type" for TypeSimplifier that does not contain any nested types. The point
* being that every {@code TypeSimplifier} has a base type that the class being generated is going
* to extend, and if that class has nested types they will be in scope, and therefore a possible
* source of ambiguity.
*/
private TypeMirror baseWithoutContainedTypes() {
return typeMirrorOf(Object.class);
}
// This test checks that we correctly throw MissingTypeException if there is an ErrorType anywhere
// inside a type we are asked to simplify. There's no way to get an ErrorType from typeUtils or
// elementUtils, so we need to fire up the compiler with an erroneous source file and use an
// annotation processor to capture the resulting ErrorType. Then we can run tests within that
// annotation processor, and propagate any failures out of this test.
@Test
public void testErrorTypes() {
JavaFileObject source =
JavaFileObjects.forSourceString(
"ExtendsUndefinedType", "class ExtendsUndefinedType extends UndefinedParent {}");
Compilation compilation = javac().withProcessors(new ErrorTestProcessor()).compile(source);
assertThat(compilation).failed();
assertThat(compilation).hadErrorContaining("UndefinedParent");
assertThat(compilation).hadErrorCount(1);
}
@SupportedAnnotationTypes("*")
private static class ErrorTestProcessor extends AbstractProcessor {
Types typeUtils;
Elements elementUtils;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
test();
}
return false;
}
private void test() {
TypeElement extendsUndefinedType = elementUtils.getTypeElement("ExtendsUndefinedType");
ErrorType errorType = (ErrorType) extendsUndefinedType.getSuperclass();
TypeElement list = elementUtils.getTypeElement("java.util.List");
TypeMirror listOfError = typeUtils.getDeclaredType(list, errorType);
TypeMirror queryExtendsError = typeUtils.getWildcardType(errorType, null);
TypeMirror listOfQueryExtendsError = typeUtils.getDeclaredType(list, queryExtendsError);
TypeMirror querySuperError = typeUtils.getWildcardType(null, errorType);
TypeMirror listOfQuerySuperError = typeUtils.getDeclaredType(list, querySuperError);
TypeMirror arrayOfError = typeUtils.getArrayType(errorType);
testErrorType(errorType);
testErrorType(listOfError);
testErrorType(listOfQueryExtendsError);
testErrorType(listOfQuerySuperError);
testErrorType(arrayOfError);
}
@SuppressWarnings("MissingFail") // error message gets converted into assertion failure
private void testErrorType(TypeMirror typeWithError) {
try {
TypeEncoder.encode(typeWithError);
processingEnv
.getMessager()
.printMessage(Diagnostic.Kind.ERROR, "Expected exception for type: " + typeWithError);
} catch (MissingTypeException expected) {
}
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
}