google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.45k stars 2.02k forks source link

[ksp] Non explicit type specification on @Provides causes error.NonExistentClass #4051

Open G00fY2 opened 1 year ago

G00fY2 commented 1 year ago

Previously when using kapt the following @Provides provideApiBaseUrl method was working:

@Module
@InstallIn(SingletonComponent::class)
internal interface DevFlavorModule {

    @Binds
    @Singleton
    fun bindCurrentEndpointProvider(provider: DevCurrentEndpointProvider): CurrentEndpointProvider

    companion object {

        @Provides
        @ApiBaseUrl
        @Singleton
        fun provideApiBaseUrl(currentEndpointProvider: CurrentEndpointProvider) =
            when (currentEndpointProvider.provide()) {
                CurrentEndpoint.PROD -> BuildConfig.API_BASE_URL
                else -> BuildConfig.API_BASE_URL_DEV
            }
…

With ksp compilation breaks with the following error message:

Dependency trace: => element (OBJECT): com.example.app.di.DevFlavorModule.Companion => element (METHOD): provideApiBaseUrl(com.example.app.di.buildtype.CurrentEndpointProvider) => type (EXECUTABLE method): (com.example.app.di.buildtype.CurrentEndpointProvider)error.NonExistentClass => type (ERROR return type): error.NonExistentClass

Adding explixit type specification to the @Provides method fixes this issue:

fun provideApiBaseUrl(currentEndpointProvider: CurrentEndpointProvider): String =

Not sure if you could add a more helpful error message for such cases.

Mostly posting this for visibility reasons, since maybe more people run into such issues when migrating to ksp.

bcorso commented 1 year ago

Hi @G00fY2, thanks for posting this!

I'm still working on reproducing the issue you're seeing locally, but I haven't been able to reproduce it yet.

In the meantime, could you try something for me? My guess is that KSP can't determine the return type in this case because it's having trouble figuring out the type of BuildConfig.API_BASE_URL (e.g. maybe BuildConfig is a transitive dependency that's no longer on the compilation classpath?).

To test that, can you replace BuildConfig.API_BASE_URL with inline strings and see if you still get this issue? Something like this:

    fun provideApiBaseUrl(currentEndpointProvider: CurrentEndpointProvider) =
            when (currentEndpointProvider.provide()) {
                CurrentEndpoint.PROD -> "someString"
                else -> "someOtherString"
            }
danysantiago commented 1 year ago

I believe the issue here is that AGP's BuildConfig generated class is not wired correctly as an input in KSP and thus it is unable to resolve those constants and determine they are String which further prevent the type inference of the function's return type. See also: https://github.com/google/ksp/issues/350

bcorso commented 1 year ago

Ah, thanks for the pointers @danysantiago! Exactly what I was thinking was happening.

Just to clarify, is this a bug in KSP or AGP (I see the bug is filed in ksp's issues, but from the contents of the bug it sounds like it could be an issue within AGP itself)?

G00fY2 commented 1 year ago

Sorry, also had in mind that's this is related to referencing the Android BuildConfig but forgot to look this up in the KSP issues. Looks like generally explicit type specification is not mandatory. Feel free to close this issue. Maybe it will still help people facing this issue by pointing them in the right direction.

bcorso commented 1 year ago

Also, I should probably also mention that in general I would recommend always making the return type explicit for @Provides methods. It makes searching for where a particular Dagger key is bound easier, and it allows you to control the exact type that is used (which is important in Dagger).

jdelga commented 1 year ago

I am having a very similar stacktrace:

e: [ksp] InjectProcessingStep was unable to process 'mainHeaderColorDataSource' because 'error.NonExistentClass' could not be resolved.

Dependency trace:
    => element (CLASS): com.company.project.maps.presentation.MapFragment
    => type (DECLARED superclass): com.company.project.presentation.base.BaseFragment<error.NonExistentClass>
    => type (ERROR type argument): error.NonExistentClass

If type 'error.NonExistentClass' is a generated type, check above for compilation errors that may have prevented the type from being generated. Otherwise, ensure that type 'error.NonExistentClass' is on your classpath.

In my case, the dependency targets to a ViewBinding file, so, as @danysantiago says, it seems to be also related to the AGP generated classes wiring:

class MapFragment : BaseFragment<FragmentMapBinding>(
    FragmentMapBinding::inflate
)
danysantiago commented 1 year ago

For reference, here is the open issue regarding ViewBinding: https://github.com/google/dagger/issues/4049

bdushi commented 1 year ago

I have the same issue, but In my case I am trying to Provide PreferenceDataStore like shown below:

@Singleton
@Provides
fun providesDataStore(@ApplicationContext app: Context): DataStore<Preferences> {
        return PreferenceDataStoreFactory.create(
            corruptionHandler = null,
            migrations = listOf(),
            scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
            produceFile = {app.preferencesDataStoreFile(VIKI_PREFERENCES)}
        )
    }

In this case I got this error:

ModuleProcessingStep was unable to process 'al.bruno.core.di.CoreModule' because 'error.NonExistentClass' could not be resolved.

Dependency trace:
    => element (CLASS): al.bruno.core.di.CoreModule
    => element (METHOD): userPreferencesStore(android.content.Context)
    => type (ERROR return type): error.NonExistentClass

If type 'error.NonExistentClass' is a generated type, check above for compilation errors that may have prevented the type from being generated. Otherwise, ensure that type 'error.NonExistentClass' is on your classpath.
AmrAfifiy commented 1 year ago

We're working on a proper fix for the issue by exposing an API for generated java sources by the android gradle plugin to be consumed by KSP gradle plugin. Meanwhile, you can add the following snippet to your build file as a workaround to wire build config generated classes to the ksp task.

 androidComponents {
    onVariants(selector().all(), { variant ->
        afterEvaluate {
            // This is a workaround for https://issuetracker.google.com/301244513 which depends on internal
            // implementations of the android gradle plugin and the ksp gradle plugin which might change in the future
            // in an unpredictable way.
            project.tasks.getByName("ksp" + variant.name.capitalize() + "Kotlin") {
                def buildConfigTask = (com.android.build.gradle.tasks.GenerateBuildConfig) project.tasks.getByName("generate" + variant.name.capitalize() + "BuildConfig")

                ((org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool) it).setSource(
                        buildConfigTask.sourceOutputDir
                )
            }
        }
    })
}
trinhcu18 commented 1 year ago

thanks @AmrAfifiy. The snippet works well in my case

piyush921 commented 11 months ago

Had the same issue. I don't know how, but Code snippet provided by @AmrAfifiy worked.

erfansn commented 11 months ago

I also, have the same issue for Generic classes

hoc081098 commented 8 months ago

When using DataStore and Proto, add the below code. It works.


app.build.gradle.kts

android {
  androidComponents {
    onVariants(selector().all()) { variant ->
      afterEvaluate {
        project.tasks.getByName("ksp" + variant.name.capitalized() + "Kotlin") {
          val buildConfigTask = project.tasks.getByName("generate${variant.name.capitalized()}Proto")
              as com.google.protobuf.gradle.GenerateProtoTask
          dependsOn(buildConfigTask)
          (this as AbstractKotlinCompileTool<*>).setSource(buildConfigTask.outputBaseDir)
        }
      }
    }
  }
}
oscarg798 commented 5 months ago

This is still an issue when using KPS2 (ksp.useKSP2= true), I have something like

@Module
@InstallIn(ActivityRetainedComponent::class)
internal interface NavigationModule {

    @Binds
    @IntoMap
    @DeeplinkRouteFactoryKey(Route.VOLUME_DETAIL)
    fun bindTaskDetailDeepLinkRouteFactory(
        impl: VolumePageDeepLinkRouteFactory
    ): DeeplinkRouteFactory
}
@MapKey
annotation class DeeplinkRouteFactoryKey(val value: Route)

enum class Route { ... } 

and Im getting (... is my package name :))

[ksp] dagger.internal.codegen.base.DaggerSuperficialValidation$ValidationException$UnexpectedException:
  Validation trace:
    => element (INTERFACE): ...NavigationModule
    => element (METHOD): bindTaskDetailDeepLinkRouteFactory(...VolumePageDeepLinkRouteFactory)
    => annotation type: ...DeeplinkRouteFactoryKey

KPS version 2.0.0-1.0.21

G00fY2 commented 5 months ago

@oscarg798 Looks like KSP2 is currently not yet support https://github.com/google/dagger/issues/4303#issuecomment-2100469486

CurtesMalteser commented 3 months ago

This worked for me:

When using DataStore and Proto, add the below code. It works.

app.build.gradle.kts

android {
  androidComponents {
    onVariants(selector().all()) { variant ->
      afterEvaluate {
        project.tasks.getByName("ksp" + variant.name.capitalized() + "Kotlin") {
          val buildConfigTask = project.tasks.getByName("generate${variant.name.capitalized()}Proto")
              as com.google.protobuf.gradle.GenerateProtoTask
          dependsOn(buildConfigTask)
          (this as AbstractKotlinCompileTool<*>).setSource(buildConfigTask.outputBaseDir)
        }
      }
    }
  }
}

As an alternative, I found that not applying the com.google.dagger.hilt.android plugin and instead adding the classes to the annotations directly also resolved the issue:

@HiltAndroidApp(Application::class)
class App : Application()

Just leaving this here in case it helps anyone else facing similar problems.