blob: 890602602895acb1f696d2b6e41af4c3b9fdad50 [file] [log] [blame]
/*
* Copyright (C) 2016 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 dagger.internal.codegen.DaggerModuleMethodSubject.Factory.assertThatModuleMethod;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.util.Source;
import dagger.Module;
import dagger.producers.ProducerModule;
import dagger.testing.compile.CompilerTests;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@RunWith(Parameterized.class)
public final class ModuleValidationTest {
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> parameters() {
return Arrays.asList(new Object[][] {{ModuleType.MODULE}, {ModuleType.PRODUCER_MODULE}});
}
private enum ModuleType {
MODULE(Module.class),
PRODUCER_MODULE(ProducerModule.class),
;
private final Class<? extends Annotation> annotation;
ModuleType(Class<? extends Annotation> annotation) {
this.annotation = annotation;
}
String annotationWithSubcomponent(String subcomponent) {
return String.format("@%s(subcomponents = %s)", annotation.getSimpleName(), subcomponent);
}
String importStatement() {
return String.format("import %s;", annotation.getName());
}
String simpleName() {
return annotation.getSimpleName();
}
}
private final ModuleType moduleType;
public ModuleValidationTest(ModuleType moduleType) {
this.moduleType = moduleType;
}
@Test
public void moduleSubcomponents_notASubcomponent() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("NotASubcomponent.class"),
"class TestModule {}");
Source notASubcomponent =
CompilerTests.javaSource(
"test.NotASubcomponent",
"package test;",
"",
"class NotASubcomponent {}");
CompilerTests.daggerCompiler(module, notASubcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.NotASubcomponent is not a @Subcomponent or @ProductionSubcomponent")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_listsSubcomponentBuilder() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("Sub.Builder.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.Sub",
"package test;",
"",
"import dagger.Subcomponent;",
"",
"@Subcomponent",
"interface Sub {",
" @Subcomponent.Builder",
" interface Builder {",
" Sub build();",
" }",
"}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.Sub.Builder is a @Subcomponent.Builder. Did you mean to use test.Sub?")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_listsSubcomponentFactory() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("Sub.Factory.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.Sub",
"package test;",
"",
"import dagger.Subcomponent;",
"",
"@Subcomponent",
"interface Sub {",
" @Subcomponent.Factory",
" interface Factory {",
" Sub creator();",
" }",
"}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.Sub.Factory is a @Subcomponent.Factory. Did you mean to use test.Sub?")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_listsProductionSubcomponentBuilder() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("Sub.Builder.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.Sub",
"package test;",
"",
"import dagger.producers.ProductionSubcomponent;",
"",
"@ProductionSubcomponent",
"interface Sub {",
" @ProductionSubcomponent.Builder",
" interface Builder {",
" Sub build();",
" }",
"}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.Sub.Builder is a @ProductionSubcomponent.Builder. "
+ "Did you mean to use test.Sub?")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_listsProductionSubcomponentFactory() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("Sub.Factory.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.Sub",
"package test;",
"",
"import dagger.producers.ProductionSubcomponent;",
"",
"@ProductionSubcomponent",
"interface Sub {",
" @ProductionSubcomponent.Factory",
" interface Factory {",
" Sub create();",
" }",
"}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.Sub.Factory is a @ProductionSubcomponent.Factory. "
+ "Did you mean to use test.Sub?")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_noSubcomponentCreator() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("NoBuilder.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.NoBuilder",
"package test;",
"",
"import dagger.Subcomponent;",
"",
"@Subcomponent",
"interface NoBuilder {}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.NoBuilder doesn't have a @Subcomponent.Builder or "
+ "@Subcomponent.Factory, which is required when used with @"
+ moduleType.simpleName()
+ ".subcomponents")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponents_noProductionSubcomponentCreator() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"",
moduleType.annotationWithSubcomponent("NoBuilder.class"),
"class TestModule {}");
Source subcomponent =
CompilerTests.javaSource(
"test.NoBuilder",
"package test;",
"",
"import dagger.producers.ProductionSubcomponent;",
"",
"@ProductionSubcomponent",
"interface NoBuilder {}");
CompilerTests.daggerCompiler(module, subcomponent)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining(
"test.NoBuilder doesn't have a @ProductionSubcomponent.Builder or "
+ "@ProductionSubcomponent.Factory, which is required when used with @"
+ moduleType.simpleName()
+ ".subcomponents")
.onSource(module)
.onLine(5);
});
}
@Test
public void moduleSubcomponentsAreTypes() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
"import dagger.Module;",
"",
"@Module(subcomponents = int.class)",
"class TestModule {}");
CompilerTests.daggerCompiler(module)
.compile(
subject -> {
subject.hasErrorCount(1);
switch (CompilerTests.backend(subject)) {
case JAVAC:
subject.hasErrorContaining("int is not a valid subcomponent type")
.onSource(module)
.onLine(5);
break;
case KSP:
// TODO(b/245954367): Remove this pathway once this bug is fixed.
// KSP interprets the int.class type as a boxed type so we get a slightly
// different error message for now.
subject.hasErrorContaining(
"java.lang.Integer is not a @Subcomponent or @ProductionSubcomponent")
.onSource(module)
.onLine(5);
break;
}
});
}
@Test
public void tooManyAnnotations() {
assertThatModuleMethod(
"@BindsOptionalOf @Multibinds abstract Set<Object> tooManyAnnotations();")
.hasError("is annotated with more than one of");
}
@Test
public void invalidIncludedModule() {
Source badModule =
CompilerTests.javaSource(
"test.BadModule",
"package test;",
"",
"import dagger.Binds;",
"import dagger.Module;",
"",
"@Module",
"abstract class BadModule {",
" @Binds abstract Object noParameters();",
"}");
Source module =
CompilerTests.javaSource(
"test.IncludesBadModule",
"package test;",
"",
"import dagger.Module;",
"",
"@Module(includes = BadModule.class)",
"abstract class IncludesBadModule {}");
CompilerTests.daggerCompiler(badModule, module)
.compile(
subject -> {
subject.hasErrorCount(2);
subject.hasErrorContaining("test.BadModule has errors")
.onSource(module)
.onLine(5);
subject.hasErrorContaining(
"@Binds methods must have exactly one parameter, whose type is "
+ "assignable to the return type")
.onSource(badModule)
.onLine(8);
});
}
@Test
public void scopeOnModule() {
Source badModule =
CompilerTests.javaSource(
"test.BadModule",
"package test;",
"",
"import dagger.Module;",
"import javax.inject.Singleton;",
"",
"@Singleton",
"@Module",
"interface BadModule {}");
CompilerTests.daggerCompiler(badModule)
.compile(
subject -> {
subject.hasErrorCount(1);
subject.hasErrorContaining("@Modules cannot be scoped")
.onSource(badModule)
.onLineContaining("@Singleton");
});
}
@Test
public void moduleIncludesSelfCycle() {
Source module =
CompilerTests.javaSource(
"test.TestModule",
"package test;",
"",
moduleType.importStatement(),
"import dagger.Provides;",
"",
String.format("@%s(", moduleType.simpleName()),
" includes = {",
" TestModule.class, // first",
" OtherModule.class,",
" TestModule.class, // second",
" }",
")",
"class TestModule {",
" @Provides int i() { return 0; }",
"}");
Source otherModule =
CompilerTests.javaSource(
"test.OtherModule",
"package test;",
"",
"import dagger.Module;",
"",
"@Module",
"class OtherModule {}");
CompilerTests.daggerCompiler(module, otherModule)
.compile(
subject -> {
subject.hasErrorCount(2);
String error =
String.format("@%s cannot include themselves", moduleType.simpleName());
switch (CompilerTests.backend(subject)) {
case JAVAC:
subject.hasErrorContaining(error).onSource(module).onLineContaining("// first");
subject.hasErrorContaining(error).onSource(module).onLineContaining("// second");
break;
case KSP:
// KSP doesn't support reporting errors on individual annotation values, so both
// errors will be reported on the annotation itself.
subject.hasErrorContaining(error)
.onSource(module)
.onLineContaining("@" + moduleType.simpleName());
break;
}
});
}
// Regression test for b/264618194.
@Test
public void objectModuleInheritsInstanceBindingFails() {
Source objectModule =
CompilerTests.kotlinSource(
"test.ObjectModule.kt",
"package test",
"",
"import dagger.Module",
"import dagger.Provides",
"",
"@Module",
"object ObjectModule : ClassModule() {",
" @Provides fun provideString(): String = \"\"",
"}");
Source classModule =
CompilerTests.kotlinSource(
"test.ClassModule.kt",
"package test",
"",
"import dagger.Module",
"import dagger.Provides",
"",
"@Module",
"abstract class ClassModule {",
" @Provides fun provideInt(): Int = 1",
"}");
Source component =
CompilerTests.kotlinSource(
"test.TestComponent.kt",
"package test",
"",
"import dagger.Component",
"",
"@Component(modules = [ObjectModule::class])",
"interface TestComponent {",
" fun getInt(): Int",
" fun getString(): String",
"}");
CompilerTests.daggerCompiler(component, objectModule, classModule)
.compile(
subject -> {
subject.hasErrorCount(2);
subject.hasErrorContaining("test.ObjectModule has errors")
.onSource(component)
.onLineContaining("ObjectModule::class");
subject.hasErrorContaining(
"@Module-annotated Kotlin object cannot inherit instance "
+ "(i.e. non-abstract, non-JVM static) binding method: "
+ "@Provides int test.ClassModule.provideInt()")
.onSource(objectModule)
.onLineContaining(
// TODO(b/267223703): KAPT incorrectly reports the error on the annotation.
CompilerTests.backend(subject) == XProcessingEnv.Backend.JAVAC
? "@Module"
: "object ObjectModule");
});
}
// Regression test for b/264618194.
@Test
public void objectModuleInheritsNonInstanceBindingSucceeds() {
Source objectModule =
CompilerTests.kotlinSource(
"test.ObjectModule.kt",
"package test",
"",
"import dagger.Module",
"import dagger.Provides",
"",
"@Module",
"object ObjectModule : ClassModule() {",
" @Provides fun provideString(): String = \"\"",
"}");
Source classModule =
CompilerTests.javaSource(
"test.ClassModule",
"package test;",
"",
"import dagger.Binds;",
"import dagger.Module;",
"import dagger.Provides;",
"",
"@Module",
"public abstract class ClassModule {",
" // A non-binding instance method is okay.",
" public int nonBindingMethod() {",
" return 1;",
" }",
"",
" // A static binding method is also okay.",
" @Provides",
" public static int provideInt() {",
" return 1;",
" }",
"}");
Source component =
CompilerTests.kotlinSource(
"test.TestComponent.kt",
"package test",
"",
"import dagger.Component",
"",
"@Component(modules = [ObjectModule::class])",
"interface TestComponent {",
" fun getInt(): Int",
" fun getString(): String",
"}");
CompilerTests.daggerCompiler(component, objectModule, classModule)
.compile(subject -> subject.hasErrorCount(0));
}
}