blob: c5ccc5f6aef9dd3de050edc0e7cd38fa920acdfa [file] [log] [blame]
/*
* Copyright 2016 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.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static javax.lang.model.util.ElementFilter.methodsIn;
import com.google.common.base.Converter;
import com.google.common.base.Optional;
import com.google.common.base.StandardSystemProperty;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.io.Files;
import com.google.common.truth.Expect;
import com.google.testing.compile.CompilationRule;
import java.io.File;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
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.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.model.Statement;
/**
* Tests that the {@link Overrides} class has behaviour consistent with javac. We test this in
* two ways: once with {@link Overrides.ExplicitOverrides} using javac's own {@link Elements} and
* {@link Types}, and once with it using the version of those objects from the Eclipse compiler
* (ecj).
*
* @author emcmanus@google.com (Éamonn McManus)
*/
@RunWith(Parameterized.class)
public class OverridesTest {
@Parameterized.Parameters(name = "{0}")
public static ImmutableList<CompilerType> data() {
return ImmutableList.of(CompilerType.JAVAC, CompilerType.ECJ);
}
@Rule public CompilationRule compilation = new CompilationRule();
@Rule public EcjCompilationRule ecjCompilation = new EcjCompilationRule();
@Rule public Expect expect = Expect.create();
public enum CompilerType {
JAVAC {
@Override
void initUtils(OverridesTest test) {
test.typeUtils = test.compilation.getTypes();
test.elementUtils = test.compilation.getElements();
}
},
ECJ {
@Override
void initUtils(OverridesTest test) {
test.typeUtils = test.ecjCompilation.types;
test.elementUtils = test.ecjCompilation.elements;
}
};
abstract void initUtils(OverridesTest test);
}
private final CompilerType compilerType;
private Types typeUtils;
private Elements elementUtils;
private Elements javacElementUtils;
private Overrides javacOverrides;
private Overrides.ExplicitOverrides explicitOverrides;
public OverridesTest(CompilerType compilerType) {
this.compilerType = compilerType;
}
@Before
public void initializeTestElements() {
javacElementUtils = compilation.getElements();
javacOverrides = new Overrides.NativeOverrides(javacElementUtils);
compilerType.initUtils(this);
explicitOverrides = new Overrides.ExplicitOverrides(typeUtils);
}
static class TypesForInheritance {
interface One {
void m();
void m(String x);
void n();
}
interface Two {
void m();
void m(int x);
}
static class Parent {
public void m() {}
}
static class ChildOfParent extends Parent {}
static class ChildOfOne implements One {
@Override
public void m() {}
@Override
public void m(String x) {}
@Override
public void n() {}
}
static class ChildOfOneAndTwo implements One, Two {
@Override
public void m() {}
@Override
public void m(String x) {}
@Override
public void m(int x) {}
@Override
public void n() {}
}
static class ChildOfParentAndOne extends Parent implements One {
@Override
public void m() {}
@Override
public void m(String x) {}
@Override
public void n() {}
}
static class ChildOfParentAndOneAndTwo extends Parent implements One, Two {
@Override
public void m(String x) {}
@Override
public void m(int x) {}
@Override
public void n() {}
}
abstract static class AbstractChildOfOne implements One {}
abstract static class AbstractChildOfOneAndTwo implements One, Two {}
abstract static class AbstractChildOfParentAndOneAndTwo extends Parent implements One, Two {}
}
static class MoreTypesForInheritance {
interface Key {}
interface BindingType {}
interface ContributionType {}
interface HasKey {
Key key();
}
interface HasBindingType {
BindingType bindingType();
}
interface HasContributionType {
ContributionType contributionType();
}
abstract static class BindingDeclaration implements HasKey {
abstract Optional<Element> bindingElement();
abstract Optional<TypeElement> contributingModule();
}
abstract static class MultibindingDeclaration extends BindingDeclaration
implements HasBindingType, HasContributionType {
@Override
public abstract Key key();
@Override
public abstract ContributionType contributionType();
@Override
public abstract BindingType bindingType();
}
}
static class TypesForVisibility {
public abstract static class PublicGrandparent {
public abstract String foo();
}
private static class PrivateParent extends PublicGrandparent {
@Override
public String foo() {
return "foo";
}
}
static class Child extends PrivateParent {}
}
static class TypesForGenerics {
interface GCollection<E> {
boolean add(E x);
}
interface GList<E> extends GCollection<E> {
@Override
boolean add(E x);
}
static class StringList implements GList<String> {
@Override
public boolean add(String x) {
return false;
}
}
@SuppressWarnings("rawtypes")
static class RawList implements GList {
@Override
public boolean add(Object x) {
return false;
}
}
}
@SuppressWarnings("rawtypes")
static class TypesForRaw {
static class RawParent {
void frob(List x) {}
}
static class RawChildOfRaw extends RawParent {
@Override
void frob(List x) {}
}
static class NonRawParent {
void frob(List<String> x) {}
}
static class RawChildOfNonRaw extends NonRawParent {
@Override
void frob(List x) {}
}
}
@Test
public void overridesInheritance() {
checkOverridesInContainedClasses(TypesForInheritance.class);
}
@Test
public void overridesMoreInheritance() {
checkOverridesInContainedClasses(MoreTypesForInheritance.class);
}
@Test
public void overridesVisibility() {
checkOverridesInContainedClasses(TypesForVisibility.class);
}
@Test
public void overridesGenerics() {
checkOverridesInContainedClasses(TypesForGenerics.class);
}
@Test
public void overridesRaw() {
checkOverridesInContainedClasses(TypesForRaw.class);
}
// Test a tricky diamond inheritance hierarchy:
// Collection
// / \
// AbstractCollection List
// \ /
// AbstractList
// This also tests that we do the right thing with generics, since naively the TypeMirror
// that you get for List<E> will not appear to be a subtype of the one you get for Collection<E>
// since the two Es are not the same.
@Test
public void overridesDiamond() {
checkOverridesInSet(
ImmutableSet.<Class<?>>of(
Collection.class, List.class, AbstractCollection.class, AbstractList.class));
}
private void checkOverridesInContainedClasses(Class<?> container) {
checkOverridesInSet(ImmutableSet.copyOf(container.getDeclaredClasses()));
}
private void checkOverridesInSet(ImmutableSet<Class<?>> testClasses) {
assertThat(testClasses).isNotEmpty();
ImmutableSet.Builder<TypeElement> testTypesBuilder = ImmutableSet.builder();
for (Class<?> testClass : testClasses) {
testTypesBuilder.add(getTypeElement(testClass));
}
ImmutableSet<TypeElement> testTypes = testTypesBuilder.build();
ImmutableSet.Builder<ExecutableElement> testMethodsBuilder = ImmutableSet.builder();
for (TypeElement testType : testTypes) {
testMethodsBuilder.addAll(methodsIn(testType.getEnclosedElements()));
}
ImmutableSet<ExecutableElement> testMethods = testMethodsBuilder.build();
for (TypeElement in : testTypes) {
TypeElement javacIn = javacType(in);
List<ExecutableElement> inMethods = methodsIn(elementUtils.getAllMembers(in));
for (ExecutableElement overrider : inMethods) {
ExecutableElement javacOverrider = javacMethod(overrider);
for (ExecutableElement overridden : testMethods) {
ExecutableElement javacOverridden = javacMethod(overridden);
boolean javacSays = javacOverrides.overrides(javacOverrider, javacOverridden, javacIn);
boolean weSay = explicitOverrides.overrides(overrider, overridden, in);
if (javacSays != weSay) {
expect
.withMessage(
"%s.%s overrides %s.%s in %s: javac says %s, we say %s",
overrider.getEnclosingElement(),
overrider,
overridden.getEnclosingElement(),
overridden,
in,
javacSays,
weSay)
.fail();
}
}
}
}
}
private TypeElement getTypeElement(Class<?> c) {
return elementUtils.getTypeElement(c.getCanonicalName());
}
private ExecutableElement getMethod(TypeElement in, String name, TypeKind... parameterTypeKinds) {
ExecutableElement found = null;
methods:
for (ExecutableElement method : methodsIn(in.getEnclosedElements())) {
if (method.getSimpleName().contentEquals(name)
&& method.getParameters().size() == parameterTypeKinds.length) {
for (int i = 0; i < parameterTypeKinds.length; i++) {
if (method.getParameters().get(i).asType().getKind() != parameterTypeKinds[i]) {
continue methods;
}
}
assertThat(found).isNull();
found = method;
}
}
assertThat(found).isNotNull();
return requireNonNull(found);
}
// These skeletal parallels to the real collection classes ensure that the test is independent
// of the details of those classes, for example whether List<E> redeclares add(E) even though
// it also inherits it from Collection<E>.
private interface XCollection<E> {
boolean add(E e);
}
private interface XList<E> extends XCollection<E> {}
private abstract static class XAbstractCollection<E> implements XCollection<E> {
@Override
public boolean add(E e) {
return false;
}
}
private abstract static class XAbstractList<E> extends XAbstractCollection<E>
implements XList<E> {
@Override
public boolean add(E e) {
return true;
}
}
private abstract static class XStringList extends XAbstractList<String> {}
private abstract static class XAbstractStringList implements XList<String> {}
private abstract static class XNumberList<E extends Number> extends XAbstractList<E> {}
// Parameter of add(E) in StringList is String.
// That means that we successfully recorded E[AbstractList] = String and E[List] = E[AbstractList]
// and String made it all the way through.
@Test
public void methodParameters_StringList() {
TypeElement xAbstractList = getTypeElement(XAbstractList.class);
TypeElement xStringList = getTypeElement(XStringList.class);
TypeElement string = getTypeElement(String.class);
ExecutableElement add = getMethod(xAbstractList, "add", TypeKind.TYPEVAR);
List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xStringList);
List<TypeMirror> expectedParams = ImmutableList.of(string.asType());
assertTypeListsEqual(params, expectedParams);
}
// Parameter of add(E) in AbstractStringList is String.
// That means that we successfully recorded E[List] = String and E[Collection] = E[List].
@Test
public void methodParameters_AbstractStringList() {
TypeElement xCollection = getTypeElement(XCollection.class);
TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
TypeElement string = getTypeElement(String.class);
ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);
List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xAbstractStringList);
List<TypeMirror> expectedParams = ImmutableList.of(string.asType());
assertTypeListsEqual(params, expectedParams);
}
// Parameter of add(E) in NumberList is Number.
// That means that we successfully recorded E[AbstractList] = Number and on from
// there, with Number being used because it is the erasure of <E extends Number>.
@Test
public void methodParams_NumberList() {
TypeElement xCollection = getTypeElement(XCollection.class);
TypeElement xNumberList = getTypeElement(XNumberList.class);
TypeElement number = getTypeElement(Number.class);
ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);
List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xNumberList);
List<TypeMirror> expectedParams = ImmutableList.of(number.asType());
assertTypeListsEqual(params, expectedParams);
}
// This is derived from a class that provoked a StackOverflowError in an earlier version.
private abstract static class StringToRangeConverter<T extends Comparable<T>>
extends Converter<String, Range<T>> {
@Override
protected String doBackward(Range<T> b) {
return "";
}
}
@Test
public void methodParams_RecursiveBound() {
TypeElement stringToRangeConverter = getTypeElement(StringToRangeConverter.class);
TypeElement range = getTypeElement(Range.class);
ExecutableElement valueConverter =
getMethod(stringToRangeConverter, "doBackward", TypeKind.DECLARED);
List<TypeMirror> params =
explicitOverrides.erasedParameterTypes(valueConverter, stringToRangeConverter);
List<TypeMirror> expectedParams =
ImmutableList.<TypeMirror>of(typeUtils.erasure(range.asType()));
assertTypeListsEqual(params, expectedParams);
}
@Test
public void methodFromSuperclasses() {
TypeElement xAbstractCollection = getTypeElement(XAbstractCollection.class);
TypeElement xAbstractList = getTypeElement(XAbstractList.class);
TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
TypeElement xStringList = getTypeElement(XStringList.class);
ExecutableElement add = getMethod(xAbstractCollection, "add", TypeKind.TYPEVAR);
ExecutableElement addInAbstractStringList =
explicitOverrides.methodFromSuperclasses(xAbstractStringList, add);
assertThat(addInAbstractStringList).isNull();
ExecutableElement addInStringList = explicitOverrides.methodFromSuperclasses(xStringList, add);
assertThat(requireNonNull(addInStringList).getEnclosingElement()).isEqualTo(xAbstractList);
}
@Test
public void methodFromSuperinterfaces() {
TypeElement xCollection = getTypeElement(XCollection.class);
TypeElement xAbstractList = getTypeElement(XAbstractList.class);
TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
TypeElement xNumberList = getTypeElement(XNumberList.class);
TypeElement xList = getTypeElement(XList.class);
ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);
ExecutableElement addInAbstractStringList =
explicitOverrides.methodFromSuperinterfaces(xAbstractStringList, add);
assertThat(requireNonNull(addInAbstractStringList).getEnclosingElement())
.isEqualTo(xCollection);
ExecutableElement addInNumberList =
explicitOverrides.methodFromSuperinterfaces(xNumberList, add);
assertThat(requireNonNull(addInNumberList).getEnclosingElement()).isEqualTo(xAbstractList);
ExecutableElement addInList = explicitOverrides.methodFromSuperinterfaces(xList, add);
assertThat(requireNonNull(addInList).getEnclosingElement()).isEqualTo(xCollection);
}
private void assertTypeListsEqual(@Nullable List<TypeMirror> actual, List<TypeMirror> expected) {
requireNonNull(actual);
assertThat(actual).hasSize(expected.size());
for (int i = 0; i < actual.size(); i++) {
assertThat(typeUtils.isSameType(actual.get(i), expected.get(i))).isTrue();
}
}
// TODO(emcmanus): replace this with something from compile-testing when that's available.
/**
* An equivalent to {@link CompilationRule} that uses ecj (the Eclipse compiler) instead of javac.
* If the parameterized test is not selecting ecj then this rule has no effect.
*/
public class EcjCompilationRule implements TestRule {
Elements elements;
Types types;
@Override
public Statement apply(Statement base, Description description) {
if (!compilerType.equals(CompilerType.ECJ)) {
return base;
}
return new EcjCompilationStatement(base);
}
}
private class EcjCompilationStatement extends Statement {
private final Statement statement;
EcjCompilationStatement(Statement base) {
this.statement = base;
}
@Override
public void evaluate() throws Throwable {
File tmpDir = File.createTempFile("OverridesTest", "dir");
tmpDir.delete();
tmpDir.mkdir();
File dummySourceFile = new File(tmpDir, "Dummy.java");
try {
Files.asCharSink(dummySourceFile, UTF_8).write("class Dummy {}");
evaluate(dummySourceFile);
} finally {
dummySourceFile.delete();
tmpDir.delete();
}
}
private void evaluate(File dummySourceFile) throws Throwable {
JavaCompiler compiler = new EclipseCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, UTF_8);
// This hack is only needed in a Google-internal Java 8 environment where symbolic links make
// it hard for ecj to find the boot class path. Elsewhere it is unnecessary but harmless.
File rtJar = new File(StandardSystemProperty.JAVA_HOME.value() + "/lib/rt.jar");
if (rtJar.exists()) {
List<File> bootClassPath =
ImmutableList.<File>builder()
.add(rtJar)
.addAll(fileManager.getLocation(StandardLocation.PLATFORM_CLASS_PATH))
.build();
fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath);
}
Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjects(dummySourceFile);
JavaCompiler.CompilationTask task =
compiler.getTask(null, fileManager, null, null, null, sources);
EcjTestProcessor processor = new EcjTestProcessor(statement);
task.setProcessors(ImmutableList.of(processor));
assertThat(task.call()).isTrue();
processor.maybeThrow();
}
}
@SupportedAnnotationTypes("*")
private class EcjTestProcessor extends AbstractProcessor {
private final Statement statement;
private Throwable thrown;
EcjTestProcessor(Statement statement) {
this.statement = statement;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
ecjCompilation.elements = processingEnv.getElementUtils();
ecjCompilation.types = processingEnv.getTypeUtils();
try {
statement.evaluate();
} catch (Throwable t) {
thrown = t;
}
}
return false;
}
void maybeThrow() throws Throwable {
if (thrown != null) {
throw thrown;
}
}
}
private TypeElement javacType(TypeElement type) {
return javacElementUtils.getTypeElement(type.getQualifiedName().toString());
}
private ExecutableElement javacMethod(ExecutableElement method) {
if (elementUtils == javacElementUtils) {
return method;
}
TypeElement containingType = MoreElements.asType(method.getEnclosingElement());
TypeElement javacContainingType = javacType(containingType);
List<ExecutableElement> candidates = new ArrayList<ExecutableElement>();
methods:
for (ExecutableElement javacMethod : methodsIn(javacContainingType.getEnclosedElements())) {
if (javacMethod.getSimpleName().contentEquals(method.getSimpleName())
&& javacMethod.getParameters().size() == method.getParameters().size()) {
for (int i = 0; i < method.getParameters().size(); i++) {
VariableElement parameter = method.getParameters().get(i);
VariableElement javacParameter = javacMethod.getParameters().get(i);
if (!erasedToString(parameter.asType()).equals(erasedToString(javacParameter.asType()))) {
continue methods;
}
}
candidates.add(javacMethod);
}
}
if (candidates.size() == 1) {
return candidates.get(0);
} else {
throw new IllegalStateException(
"Expected one javac method matching " + method + " but found " + candidates);
}
}
private static String erasedToString(TypeMirror type) {
return ERASED_STRING_TYPE_VISITOR.visit(type);
}
private static final TypeVisitor<String, Void> ERASED_STRING_TYPE_VISITOR =
new SimpleTypeVisitor6<String, Void>() {
@Override
protected String defaultAction(TypeMirror e, Void p) {
return e.toString();
}
@Override
public String visitArray(ArrayType t, Void p) {
return visit(t.getComponentType()) + "[]";
}
@Override
public String visitDeclared(DeclaredType t, Void p) {
return MoreElements.asType(t.asElement()).getQualifiedName().toString();
}
@Override
public String visitTypeVariable(TypeVariable t, Void p) {
return visit(t.getUpperBound());
}
};
}