blob: dffe7014a78c565b8d98e4af4608b972ce61c3a8 [file] [log] [blame]
/*
* Copyright (C) 2021 The Dagger Authors.
*
* 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 dagger.internal.codegen;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XElement;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XProcessingStep;
import androidx.room.compiler.processing.XTypeElement;
import androidx.room.compiler.processing.XVariableElement;
import androidx.room.compiler.processing.util.Source;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import dagger.BindsInstance;
import dagger.Component;
import dagger.internal.codegen.base.DaggerSuperficialValidation;
import dagger.internal.codegen.base.DaggerSuperficialValidation.ValidationException;
import dagger.testing.compile.CompilerTests;
import java.util.Map;
import java.util.Set;
import javax.inject.Singleton;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class DaggerSuperficialValidationTest {
enum SourceKind {
JAVA,
KOTLIN
}
@Parameters(name = "sourceKind={0}")
public static ImmutableList<Object[]> parameters() {
return ImmutableList.of(new Object[] {SourceKind.JAVA}, new Object[] {SourceKind.KOTLIN});
}
private final SourceKind sourceKind;
public DaggerSuperficialValidationTest(SourceKind sourceKind) {
this.sourceKind = sourceKind;
}
private static final Joiner NEW_LINES = Joiner.on("\n ");
@Test
public void missingReturnType() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"abstract class TestClass {",
" abstract MissingType blah();",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"abstract class TestClass {",
" abstract fun blah(): MissingType",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (METHOD): blah()",
" => type (ERROR return type): %1$s"),
isJavac ? "MissingType" : "error.NonExistentClass"));
});
}
@Test
public void missingGenericReturnType() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"abstract class TestClass {",
" abstract MissingType<?> blah();",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"abstract class TestClass {",
" abstract fun blah(): MissingType<*>",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
final String errorType;
if (processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC) {
// JDK 24 improves error type information.
errorType =
Runtime.version().feature() >= 24
? isKAPT(processingEnv) ? "MissingType" : "MissingType<?>"
: "<any>";
} else {
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
errorType = "error.NonExistentClass";
}
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (METHOD): blah()",
" => type (ERROR return type): %1$s"),
errorType));
});
}
@Test
public void missingReturnTypeTypeParameter() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"import java.util.Map;",
"import java.util.Set;",
"",
"abstract class TestClass {",
" abstract Map<Set<?>, MissingType<?>> blah();",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"abstract class TestClass {",
" abstract fun blah(): Map<Set<*>, MissingType<*>>",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
final String errorType;
if (processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC) {
// JDK 24 improves error type information.
errorType = Runtime.version().feature() >= 24 ? "MissingType<?>" : "<any>";
} else {
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
errorType = "error.NonExistentClass";
}
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (METHOD): blah()",
" => type (DECLARED return type): "
+ "java.util.Map<java.util.Set<?>,%1$s>",
" => type (ERROR type argument): %1$s"),
errorType));
});
}
@Test
public void missingTypeParameter() {
runTest(
CompilerTests.javaSource(
"test.TestClass", //
"package test;",
"",
"class TestClass<T extends MissingType> {}"),
CompilerTests.kotlinSource(
"test.TestClass.kt", //
"package test",
"",
"class TestClass<T : MissingType>"),
(processingEnv, superficialValidation) -> {
if (isKAPT(processingEnv)) {
// The KAPT java stub doesn't reference the MissingType symbol (b/268536260#comment2).
return;
}
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (TYPE_PARAMETER): T",
" => type (ERROR bound type): %s"),
isJavac ? "MissingType" : "error.NonExistentClass"));
});
}
@Test
public void missingParameterType() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"abstract class TestClass {",
" abstract void foo(MissingType param);",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"abstract class TestClass {",
" abstract fun foo(param: MissingType);",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (METHOD): foo(%1$s)",
" => element (PARAMETER): param",
" => type (ERROR parameter type): %1$s"),
isJavac ? "MissingType" : "error.NonExistentClass"));
});
}
@Test
public void missingAnnotation() {
runTest(
CompilerTests.javaSource(
"test.TestClass", //
"package test;",
"",
"@MissingAnnotation",
"class TestClass {}"),
CompilerTests.kotlinSource(
"test.TestClass.kt", //
"package test",
"",
"@MissingAnnotation",
"class TestClass"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => annotation type: MissingAnnotation",
" => type (ERROR annotation type): %s"),
isJavac ? "MissingAnnotation" : "error.NonExistentClass"));
});
}
@Test
public void handlesRecursiveTypeParams() {
runSuccessfulTest(
CompilerTests.javaSource(
"test.TestClass", //
"package test;",
"",
"class TestClass<T extends Comparable<T>> {}"),
CompilerTests.kotlinSource(
"test.TestClass.kt", //
"package test",
"",
"class TestClass<T : Comparable<T>>"),
(processingEnv, superficialValidation) ->
superficialValidation.validateElement(processingEnv.findTypeElement("test.TestClass")));
}
@Test
public void handlesRecursiveType() {
runSuccessfulTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"abstract class TestClass {",
" abstract TestClass foo(TestClass x);",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"abstract class TestClass {",
" abstract fun foo(x: TestClass): TestClass",
"}"),
(processingEnv, superficialValidation) ->
superficialValidation.validateElement(processingEnv.findTypeElement("test.TestClass")));
}
@Test
public void missingWildcardBound() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"import java.util.Set;",
"",
"class TestClass {",
" static final class Foo<T> {}",
"",
" Foo<? extends MissingType> extendsTest() {",
" return null;",
" }",
"",
" Foo<? super MissingType> superTest() {",
" return null;",
" }",
"}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"class TestClass {",
" class Foo<T>",
"",
" fun extendsTest(): Foo<out MissingType> = TODO()",
"",
" fun superTest(): Foo<in MissingType> = TODO()",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (METHOD): extendsTest()",
" => type (DECLARED return type): test.TestClass.Foo<? extends %1$s>",
" => type (WILDCARD type argument): ? extends %1$s",
" => type (ERROR extends bound type): %1$s"),
isJavac ? "MissingType" : "error.NonExistentClass"));
});
}
@Test
public void missingIntersection() {
runTest(
CompilerTests.javaSource(
"test.TestClass",
"package test;",
"",
"class TestClass<T extends Number & Missing> {}"),
CompilerTests.kotlinSource(
"test.TestClass.kt",
"package test",
"",
"class TestClass<T> where T: Number, T: Missing"),
(processingEnv, superficialValidation) -> {
if (isKAPT(processingEnv)) {
// The KAPT java stub doesn't reference the MissingType symbol (b/268536260#comment2).
return;
}
XTypeElement testClassElement = processingEnv.findTypeElement("test.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.TestClass",
" => element (TYPE_PARAMETER): T",
" => type (ERROR bound type): %s"),
isJavac ? "Missing" : "error.NonExistentClass"));
});
}
@Test
public void invalidAnnotationValue() {
runTest(
CompilerTests.javaSource(
"test.Outer",
"package test;",
"",
"final class Outer {",
" @interface TestAnnotation {",
" Class[] classes();",
" }",
"",
" @TestAnnotation(classes = MissingType.class)",
" static class TestClass {}",
"}"),
CompilerTests.kotlinSource(
"test.Outer.kt",
"package test",
"",
"class Outer {",
" annotation class TestAnnotation(",
" val classes: Array<kotlin.reflect.KClass<*>>",
" )",
"",
" @TestAnnotation(classes = [MissingType::class])",
" class TestClass {}",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement testClassElement = processingEnv.findTypeElement("test.Outer.TestClass");
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(testClassElement));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
String expectedMessage =
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.Outer.TestClass",
" => annotation type: test.Outer.TestAnnotation",
" => annotation: @test.Outer.TestAnnotation(classes={<%1$s>})",
" => annotation value (TYPE_ARRAY): classes={<%1$s>}",
" => annotation value (TYPE): classes=<%1$s>"),
isJavac ? "error" : "ERROR TYPE: MissingType");
if (!isJavac) {
expectedMessage =
NEW_LINES.join(
expectedMessage,
" => type (ERROR annotation value type): error.NonExistentClass");
}
assertThat(exception).hasMessageThat().contains(expectedMessage);
});
}
@Test
public void invalidAnnotationValueOnParameter() {
runTest(
CompilerTests.javaSource(
"test.Outer",
"package test;",
"",
"final class Outer {",
" @interface TestAnnotation {",
" Class[] classes();",
" }",
"",
" static class TestClass {",
" TestClass(@TestAnnotation(classes = MissingType.class) String strParam) {}",
" }",
"}"),
CompilerTests.kotlinSource(
"test.Outer.kt",
"package test",
"",
"class Outer {",
" annotation class TestAnnotation(",
" val classes: Array<kotlin.reflect.KClass<*>>",
" )",
"",
" class TestClass(",
" @TestAnnotation(classes = [MissingType::class]) strParam: String",
" )",
"}"),
(processingEnv, superficialValidation) -> {
if (isKAPT(processingEnv)) {
// The KAPT java stub doesn't reference the MissingType symbol (b/268536260#comment2).
return;
}
XTypeElement testClassElement = processingEnv.findTypeElement("test.Outer.TestClass");
XConstructorElement constructor = testClassElement.getConstructors().get(0);
XVariableElement parameter = constructor.getParameters().get(0);
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() -> superficialValidation.validateElement(parameter));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
String expectedMessage =
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.Outer.TestClass",
" => element (CONSTRUCTOR): TestClass(java.lang.String)",
" => element (PARAMETER): strParam",
" => annotation type: test.Outer.TestAnnotation",
" => annotation: @test.Outer.TestAnnotation(classes={<%1$s>})",
" => annotation value (TYPE_ARRAY): classes={<%1$s>}",
" => annotation value (TYPE): classes=<%1$s>"),
isJavac ? "error" : "ERROR TYPE: MissingType");
if (!isJavac) {
expectedMessage =
NEW_LINES.join(
expectedMessage,
" => type (ERROR annotation value type): error.NonExistentClass");
}
assertThat(exception).hasMessageThat().contains(expectedMessage);
});
}
@Test
public void invalidSuperclassInTypeHierarchy() {
runTest(
CompilerTests.javaSource(
"test.Outer",
"package test;",
"",
"final class Outer {",
" Child<Long> getChild() { return null; }",
" static class Child<T> extends Parent<T> {}",
" static class Parent<T> extends MissingType<T> {}",
"}"),
CompilerTests.kotlinSource(
"test.Outer.kt",
"package test",
"",
"class Outer {",
" fun getChild(): Child<Long> = TODO()",
" class Child<T> : Parent<T>",
" open class Parent<T> : MissingType<T>",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement outerElement = processingEnv.findTypeElement("test.Outer");
XMethodElement getChildMethod = outerElement.getDeclaredMethods().get(0);
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() ->
superficialValidation.validateTypeHierarchyOf(
"return type", getChildMethod, getChildMethod.getReturnType()));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.Outer",
" => element (METHOD): getChild()",
" => type (DECLARED return type): test.Outer.Child<java.lang.Long>",
" => type (DECLARED supertype): test.Outer.Parent<java.lang.Long>",
" => type (ERROR supertype): %s"),
isJavac ? "MissingType<T>" : "error.NonExistentClass"));
});
}
@Test
public void invalidSuperclassTypeParameterInTypeHierarchy() {
runTest(
CompilerTests.javaSource(
"test.Outer",
"package test;",
"",
"final class Outer {",
" Child getChild() { return null; }",
" static class Child extends Parent<MissingType> {}",
" static class Parent<T> {}",
"}"),
CompilerTests.kotlinSource(
"test.Outer.kt",
"package test",
"",
"class Outer {",
" fun getChild(): Child = TODO()",
" class Child : Parent<MissingType>()",
" open class Parent<T>",
"}"),
(processingEnv, superficialValidation) -> {
XTypeElement outerElement = processingEnv.findTypeElement("test.Outer");
XMethodElement getChildMethod = outerElement.getDeclaredMethods().get(0);
if (isKAPT(processingEnv)) {
// https://youtrack.jetbrains.com/issue/KT-34193/Kapt-CorrectErrorTypes-doesnt-work-for-generics
// There's no way to work around this bug in KAPT so validation doesn't catch this case.
superficialValidation.validateTypeHierarchyOf(
"return type", getChildMethod, getChildMethod.getReturnType());
return;
}
ValidationException exception =
assertThrows(
ValidationException.KnownErrorType.class,
() ->
superficialValidation.validateTypeHierarchyOf(
"return type", getChildMethod, getChildMethod.getReturnType()));
// TODO(b/248552462): Javac and KSP should match once this bug is fixed.
boolean isJavac = processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC;
assertThat(exception)
.hasMessageThat()
.contains(
String.format(
NEW_LINES.join(
"Validation trace:",
" => element (CLASS): test.Outer",
" => element (METHOD): getChild()",
" => type (DECLARED return type): test.Outer.Child",
" => type (DECLARED supertype): test.Outer.Parent<%1$s>",
" => type (ERROR type argument): %1$s"),
isJavac ? "MissingType" : "error.NonExistentClass"));
});
}
private void runTest(
Source.JavaSource javaSource,
Source.KotlinSource kotlinSource,
AssertionHandler assertionHandler) {
CompilerTests.daggerCompiler(sourceKind == SourceKind.JAVA ? javaSource : kotlinSource)
.withProcessingSteps(() -> new AssertingStep(assertionHandler))
// We're expecting compiler errors that we assert on in the assertionHandler.
.compile(subject -> subject.hasError());
}
private void runSuccessfulTest(
Source.JavaSource javaSource,
Source.KotlinSource kotlinSource,
AssertionHandler assertionHandler) {
CompilerTests.daggerCompiler(sourceKind == SourceKind.JAVA ? javaSource : kotlinSource)
.withProcessingSteps(() -> new AssertingStep(assertionHandler))
.compile(subject -> subject.hasErrorCount(0));
}
private boolean isKAPT(XProcessingEnv processingEnv) {
return processingEnv.getBackend() == XProcessingEnv.Backend.JAVAC
&& sourceKind == SourceKind.KOTLIN;
}
private interface AssertionHandler {
void runAssertions(
XProcessingEnv processingEnv, DaggerSuperficialValidation superficialValidation);
}
private static final class AssertingStep implements XProcessingStep {
private final AssertionHandler assertionHandler;
private boolean processed = false;
AssertingStep(AssertionHandler assertionHandler) {
this.assertionHandler = assertionHandler;
}
@Override
public final ImmutableSet<String> annotations() {
return ImmutableSet.of("*");
}
@Override
public ImmutableSet<XElement> process(
XProcessingEnv env, Map<String, ? extends Set<? extends XElement>> elementsByAnnotation) {
if (!processed) {
processed = true; // only process once.
TestComponent component =
DaggerDaggerSuperficialValidationTest_TestComponent.factory().create(env);
assertionHandler.runAssertions(env, component.superficialValidation());
}
return ImmutableSet.of();
}
@Override
public void processOver(
XProcessingEnv env, Map<String, ? extends Set<? extends XElement>> elementsByAnnotation) {}
}
@Singleton
@Component(modules = ProcessingEnvironmentModule.class)
interface TestComponent {
DaggerSuperficialValidation superficialValidation();
@Component.Factory
interface Factory {
TestComponent create(@BindsInstance XProcessingEnv processingEnv);
}
}
}