/* * 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 parameters() { return Arrays.asList(new Object[][] {{ModuleType.MODULE}, {ModuleType.PRODUCER_MODULE}}); } private enum ModuleType { MODULE(Module.class), PRODUCER_MODULE(ProducerModule.class), ; private final Class annotation; ModuleType(Class 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 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)); } }