/* * Copyright (C) 2023 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 androidx.room.compiler.processing.util.CompilationResultSubject; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import dagger.testing.compile.CompilerTests; import java.util.function.Consumer; 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 IgnoreProvisionKeyWildcardsTest { enum SourceKind { JAVA, KOTLIN } @Parameters(name = "sourceKind={0}, isIgnoreProvisionKeyWildcardsEnabled={1}") public static ImmutableList parameters() { return ImmutableList.of( new Object[] {SourceKind.JAVA, false}, new Object[] {SourceKind.KOTLIN, false}, new Object[] {SourceKind.JAVA, true}, new Object[] {SourceKind.KOTLIN, true} ); } private static final Joiner NEW_LINES = Joiner.on("\n"); private static final Joiner NEW_LINES_FOR_ERROR_MSG = Joiner.on("\n "); private final boolean isIgnoreProvisionKeyWildcardsEnabled; private final SourceKind sourceKind; private final ImmutableMap processingOptions; public IgnoreProvisionKeyWildcardsTest( SourceKind sourceKind, boolean isIgnoreProvisionKeyWildcardsEnabled) { this.sourceKind = sourceKind; this.isIgnoreProvisionKeyWildcardsEnabled = isIgnoreProvisionKeyWildcardsEnabled; processingOptions = isIgnoreProvisionKeyWildcardsEnabled ? ImmutableMap.of("dagger.ignoreProvisionKeyWildcards", "enabled") : ImmutableMap.of("dagger.ignoreProvisionKeyWildcards", "disabled"); } @Test public void testProvidesUniqueBindingsWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Foo fooExtends();", " Foo foo();", "}", "@Module", "interface MyModule {", " @Provides static Foo fooExtends() { return null; }", " @Provides static Foo foo() { return null; }", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun fooExtends(): Foo", " fun foo(): Foo", "}", "@Module", "object MyModule {", " @Provides fun fooExtends(): Foo = TODO()", " @Provides fun foo(): Foo = TODO()", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Foo is bound multiple times:", " @Provides Foo MyModule.foo()", " @Provides Foo MyModule.fooExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesUniqueBindingsWithMatchingWildcardArguments() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Map, Foo> mapFooExtendsBarFooExtendsBar();", " Map, Foo> mapFooExtendsBarFooBar();", " Map, Foo> mapFooBarFooExtendsBar();", " Map, Foo> mapFooBarFooBar();", "}", "@Module", "class MyModule {", " @Provides", " Map, Foo> mapFooExtendsBarFooExtendsBar() {", " return null; ", " }", " @Provides", " Map, Foo> mapFooExtendsBarFooBar() {", " return null;", " }", " @Provides", " Map, Foo> mapFooBarFooExtendsBar() {", " return null;", " }", " @Provides", " Map, Foo> mapFooBarFooBar() {", " return null;", " }", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun mapFooExtendsBarFooExtendsBar(): Map, Foo>", " fun mapFooExtendsBarFooBar(): Map, Foo>", " fun mapFooBarFooExtendsBar(): Map, Foo>", " fun mapFooBarFooBar(): Map, Foo>", "}", "@Module", "class MyModule {", " @Provides", " fun mapFooExtendsBarFooExtendsBar(): Map, Foo> = TODO()", " @Provides", " fun mapFooExtendsBarFooBar(): Map, Foo> = TODO()", " @Provides", " fun mapFooBarFooExtendsBar(): Map, Foo> = TODO()", " @Provides", " fun mapFooBarFooBar(): Map, Foo> = TODO()", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Map,Foo> is bound multiple times:", " @Provides Map,Foo> MyModule.mapFooBarFooBar()", " @Provides Map,Foo> " + "MyModule.mapFooBarFooExtendsBar()", " @Provides Map,Foo> " + "MyModule.mapFooExtendsBarFooBar()", " @Provides Map,Foo> " + "MyModule.mapFooExtendsBarFooExtendsBar()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsSetDeclarationsWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Set> setExtends();", " Set> set();", "}", "@Module", "interface MyModule {", " @Multibinds Set> setExtends();", " @Multibinds Set> set();", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun setExtends(): Set>", " fun set(): Set>", "}", "@Module", "interface MyModule {", " @Multibinds fun setExtends(): Set>", " @Multibinds fun set(): Set>", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Set> has incompatible bindings or declarations:", " Set bindings and declarations:", " @Multibinds Set> MyModule.set()", " @Multibinds Set> MyModule.setExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsSetContributionsWithDifferentTypeVariances() { compile( /* javaComponentClass= */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Set> setExtends();", " Set> set();", "}", "@Module", "interface MyModule {", " @Provides @IntoSet static Foo setExtends() { return null; }", " @Provides @IntoSet static Foo set() { return null; }", "}"), /* kotlinComponentClass= */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun setExtends(): Set>", " fun set(): Set>", "}", "@Module", "object MyModule {", " @Provides @IntoSet fun setExtends(): Foo = TODO()", " @Provides @IntoSet fun set(): Foo = TODO()", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Set> has incompatible bindings or declarations:", " Set bindings and declarations:", " @Provides @IntoSet Foo MyModule.set()", " @Provides @IntoSet Foo MyModule.setExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsSetContributionAndMultibindsWithDifferentVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Set> setExtends();", " Set> set();", "}", "@Module", "interface MyModule {", " @Provides @IntoSet static Foo setExtends() { return null; }", " @Multibinds Set> mulitbindSet();", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun setExtends(): Set>", " fun set(): Set>", "}", "@Module", "interface MyModule {", " @Multibinds abstract fun mulitbindSet(): Set>", "", " companion object {", " @Provides @IntoSet fun setExtends(): Foo = TODO()", " }", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( String.format( NEW_LINES_FOR_ERROR_MSG.join( "Set> has incompatible bindings or declarations:", " Set bindings and declarations:", " @Multibinds Set> MyModule.mulitbindSet()", " @Provides @IntoSet Foo %s.setExtends()", " in component: [MyComponent]"), sourceKind == SourceKind.KOTLIN ? "MyModule.Companion" : "MyModule")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesIntoSetAndElementsIntoSetContributionsWithDifferentVariances() { compile( /* javaComponentClass= */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Set> setExtends();", " Set> set();", "}", "@Module", "interface MyModule {", " @Provides @IntoSet static Foo setExtends() { return null; }", "", " @Provides", " @ElementsIntoSet", " static Set> set() { return null; }", "}"), /* kotlinComponentClass= */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun setExtends(): Set>", " fun set(): Set>", "}", "@Module", "object MyModule {", " @Provides @IntoSet fun setExtends(): Foo = TODO()", " @Provides @ElementsIntoSet fun set(): Set> = TODO()", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Set> has incompatible bindings or declarations:", " Set bindings and declarations:", " @Provides @ElementsIntoSet Set> MyModule.set()", " @Provides @IntoSet Foo MyModule.setExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsSetContributionsWithSameTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Set> set();", "}", "@Module", "interface MyModule {", " @Provides @IntoSet static Foo set1() { return null; }", " @Provides @IntoSet static Foo set2() { return null; }", " @Provides @ElementsIntoSet static Set> set3() { return null; }", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun set(): Set>", "}", "@Module", "object MyModule {", " @Provides @IntoSet fun set1(): Foo = TODO()", " @Provides @IntoSet fun set2(): Foo = TODO()", " @Provides @ElementsIntoSet fun set3(): Set> = TODO()", "}"), subject -> subject.hasErrorCount(0)); } @Test public void testProvidesMultibindsMapDeclarationValuesWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Map> mapExtends();", " Map> map();", "}", "@Module", "interface MyModule {", " @Multibinds Map> mapExtends();", " @Multibinds Map> map();", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun mapExtends(): Map>", " fun map(): Map>", "}", "@Module", "interface MyModule {", " @Multibinds fun mapExtends():Map>", " @Multibinds fun map(): Map>", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Map> has incompatible bindings or declarations:", " Map bindings and declarations:", " @Multibinds Map> MyModule.map()", " @Multibinds Map> MyModule.mapExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsMapDeclarationKeysWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Map, String> mapExtends();", " Map, String> map();", "}", "@Module", "interface MyModule {", " @Multibinds Map, String> mapExtends();", " @Multibinds Map, String> map();", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun mapExtends(): Map, String>", " fun map(): Map, String>", "}", "@Module", "interface MyModule {", " @Multibinds fun mapExtends():Map, String>", " @Multibinds fun map(): Map, String>", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Map,String> has incompatible bindings or declarations:", " Map bindings and declarations:", " @Multibinds Map,String> MyModule.map()", " @Multibinds Map,String> MyModule.mapExtends()", " in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesMultibindsMapContributionsWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Map> mapExtends();", " Map> map();", "}", "@Module", "interface MyModule {", " @Provides", " @IntoMap", " @StringKey(\"fooExtends\")", " static Foo fooExtends() { return null; }", "", " @Provides", " @IntoMap", " @StringKey(\"foo\")", " static Foo foo() { return null; }", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun mapExtends(): Map>", " fun map(): Map>", "}", "@Module", "object MyModule {", " @Provides", " @IntoMap", " @StringKey(\"fooExtends\")", " fun fooExtends(): Foo = TODO()", "", " @Provides", " @IntoMap", " @StringKey(\"foo\")", " fun foo(): Foo = TODO()", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorContaining( String.format( NEW_LINES_FOR_ERROR_MSG.join( "Map> has incompatible bindings or declarations:", " Map bindings and declarations:", " %s Foo MyModule.foo()", " %s Foo MyModule.fooExtends()", " in component: [MyComponent]"), "@Provides @IntoMap @StringKey(\"foo\")", "@Provides @IntoMap @StringKey(\"fooExtends\")")); } else { subject.hasErrorCount(0); } }); } @Test public void testProvidesOptionalDeclarationWithDifferentTypeVariances() { compile( /* javaComponentClass = */ NEW_LINES.join( "@Component(modules = MyModule.class)", "interface MyComponent {", " Optional> fooExtends();", " Optional> foo();", "}", "@Module", "interface MyModule {", " @BindsOptionalOf Foo fooExtends();", " @BindsOptionalOf Foo foo();", "}"), /* kotlinComponentClass = */ NEW_LINES.join( "@Component(modules = [MyModule::class])", "interface MyComponent {", " fun fooExtends(): Optional>", " fun foo(): Optional>", "}", "@Module", "interface MyModule {", " @BindsOptionalOf fun fooExtends(): Foo", " @BindsOptionalOf fun foo(): Foo", "}"), subject -> { if (isIgnoreProvisionKeyWildcardsEnabled) { subject.hasErrorCount(1); subject.hasErrorContaining( NEW_LINES_FOR_ERROR_MSG.join( "Optional> is bound multiple times:", " @BindsOptionalOf Foo MyModule.foo()", " @BindsOptionalOf Foo MyModule.fooExtends()", "in component: [MyComponent]")); } else { subject.hasErrorCount(0); } }); } private void compile( String javaComponentClass, String kotlinComponentClass, Consumer onCompilationResult) { compileInternal( javaComponentClass, kotlinComponentClass, subject -> { if (!isIgnoreProvisionKeyWildcardsEnabled) { if (CompilerTests.backend(subject) == androidx.room.compiler.processing.XProcessingEnv.Backend.KSP) { subject.hasErrorCount(1); subject.hasErrorContaining( "When using KSP, you must also enable the 'dagger.ignoreProvisionKeyWildcards'"); return; } } onCompilationResult.accept(subject); }); } private void compileInternal( String javaComponentClass, String kotlinComponentClass, Consumer onCompilationResult) { if (sourceKind == SourceKind.JAVA) { // Compile with Java sources CompilerTests.daggerCompiler( CompilerTests.javaSource( "test.MyComponent", "package test;", "", "import dagger.BindsOptionalOf;", "import dagger.Component;", "import dagger.Module;", "import dagger.Provides;", "import dagger.multibindings.ElementsIntoSet;", "import dagger.multibindings.IntoSet;", "import dagger.multibindings.IntoMap;", "import dagger.multibindings.Multibinds;", "import dagger.multibindings.StringKey;", "import java.util.Map;", "import java.util.Optional;", "import java.util.Set;", "import javax.inject.Inject;", "import javax.inject.Provider;", "", javaComponentClass, "", "interface Foo {}", "", "class Bar {}")) .withProcessingOptions(processingOptions) .compile(onCompilationResult); } if (sourceKind == SourceKind.KOTLIN) { // Compile with Kotlin sources CompilerTests.daggerCompiler( CompilerTests.kotlinSource( "test.MyComponent.kt", // TODO(bcorso): See if there's a better way to fix the following error. // // Error: Cannot inline bytecode built with JVM target 11 into bytecode that is // being built with JVM target 1.8 "@file:Suppress(\"INLINE_FROM_HIGHER_PLATFORM\")", "package test", "", "import dagger.BindsOptionalOf", "import dagger.Component", "import dagger.Module", "import dagger.Provides", "import dagger.multibindings.ElementsIntoSet", "import dagger.multibindings.IntoSet", "import dagger.multibindings.IntoMap", "import dagger.multibindings.Multibinds", "import dagger.multibindings.StringKey", "import java.util.Optional;", "import javax.inject.Inject", "import javax.inject.Provider", "", kotlinComponentClass, "", "interface Foo", "", "class Bar")) .withProcessingOptions(processingOptions) .compile(onCompilationResult); } } }