google/dagger

[KSP] Multibinding example from developer docs does not compile under Kotlin + KSP

Opened this issue · 1 comments

Reference: https://dagger.dev/dev-guide/multibindings.html

Kotlin - 2.0
KSP - 2.0.0-1.0.21
Hilt - 2.51.1

In an attempt to troubleshoot my multi-binding issues within my application, I am attempting to get a very simplistic example working from the Dagger documentation however I can not figure out how to get it to compile correctly. The usage of @JvmSuppressWildcards doesn't seem to fix the error:

[Dagger/MissingBinding] java.util.Map<kotlin.reflect.KClass<? extends java.lang.Number>,java.lang.String> cannot be provided without an @Provides-annotated method.

The test code:

enum class MyEnum {
    ABC, DEF
}

@MapKey
annotation class MyEnumKey(val value: MyEnum)

@MapKey
annotation class MyNumberClassKey(val value: KClass<out Number>)

@Module
@InstallIn(SingletonComponent::class)
object MyModule {
    @Provides
    @IntoMap
    @MyEnumKey(MyEnum.ABC)
    fun provideABCValue(): String {
        return "value for ABC"
    }

    @Provides
    @IntoMap
    @MyNumberClassKey(BigDecimal::class)
    fun provideBigDecimalValue(): String {
        return "value for BigDecimal"
    }

}
// @JvmSuppressWildcards in different locations within this interface makes no difference
@Component(modules = [MyModule::class])
interface MyComponent {
    fun myEnumStringMap() : Map<MyEnum, String>
    fun stringsByNumberClassMap() : @JvmSuppressWildcards Map<KClass<out Number>, String>
}

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class HiltIntoMapInjectionTest {
    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Before
    fun init() {
        hiltRule.inject()
    }


    @Test
    fun testIntoMapInjection() {
        val myComponent = DaggerMyComponent.create()
        assertTrue(myComponent.myEnumStringMap()[MyEnum.ABC] =="value for ABC") //Works without any @JvmSuppressWildcards annotation
        assertTrue(myComponent.stringsByNumberClassMap()[BigDecimal::class] =="value for BigDecimal") //Needs @JvmSuppressWildcards annotation but that doesn't work
    }
}

When accessing the map you will need to use Class instead of KClass:

@Component(modules = [MyModule::class])
interface MyComponent {
    fun myEnumStringMap() : Map<MyEnum, String>
    fun stringsByNumberClassMap() : Map<Class<out Number>, String>
}

This is for backwards compatibility with KAPT, which will generate a Java stub for your map key like below:

@MapKey
@interface MyNumberClassKey {
    Class<? extends Number> value();
}

I think we could add a better error message for users in this case though.