Skip to content

KMP: invisible inline-member usage in commonMain #1791

Description

@eygraber

Plugin version
3.17.0

Gradle version
9.6.1

JDK version
26

Kotlin and Kotlin Gradle Plugin (KGP) version
2.4.10

Android Gradle Plugin (AGP) version
9.3.0

reason output for bugs relating to incorrect advice

------------------------------------------------------------
You asked about the dependency 'com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1 (libs.kotlin.result.coroutines)'.
You have been advised to remove this dependency from 'commonMainImplementation'.
------------------------------------------------------------

Shortest path from :core:database to com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1 (libs.kotlin.result.coroutines) for androidCompileClasspath:
:core:database
\--- com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1

Source: androidMain
-------------------
(no usages)

Following the advice fails compilation:

e: file://.../core/database/src/commonMain/kotlin/.../DefaultLocalDataSource.kt:9:38 Unresolved reference 'coroutines'.
e: file://.../core/database/src/commonMain/kotlin/.../DefaultLocalDataSource.kt:51:16 Unresolved reference 'runSuspendCatching'.

Describe the bug
In a KMP module, a commonMain dependency whose only used API is an inline function is reported as unused, even though removing it breaks compilation.

The module has in commonMain:

import com.github.michaelbull.result.coroutines.runSuspendCatching

runSuspendCatching is inline, which it seems like DAGP handles with import matching (FindKotlinMagicTask, ComputeUsagesTask.usesInlineMember), which compares the project's source imports against inline members found in dependency jars.

I did some digging and the producer half works. intermediates/inline-usage.json for the androidMain analysis correctly lists the artifact's inline members:

{
  "coordinates": { "identifier": "com.michael-bull.kotlin-result:kotlin-result-coroutines-jvm", ... },
  "inlineMembers": [
    { "className": "com.github.michaelbull.result.coroutines.RunSuspendCatchingKt",
      "packageName": "com.github.michaelbull.result.coroutines",
      "inlineMembers": ["runSuspendCatching"] }, ...
  ]
}

The consumer half never sees the import. intermediates/exploded-source.json for the same analysis contains only the androidMain file:

[ { "relativePath": "src/androidMain/kotlin/.../DatabaseTypeFactory.android.kt", ... } ]

The commonMain file containing the runSuspendCatching import is absent, so usesInlineMember finds no matching import and the dependency falls through to Bucket.NONE.

The cause appears to be in source collection for KMP compilations. KmpSourceSet uses only the compilation's default source set:

// internal/analyzer/JvmSourceSet.kt
override val sourceCode: SourceDirectorySet = compilation.defaultSourceSet.kotlin

For the Android compilation of a KMP module, defaultSourceSet is androidMain; the dependsOn closure (commonMain, nativeMain, etc.) is not included. So any usage evidence that exists only in common source (e.g. inline function imports) is invisible to every per-compilation analysis. (KotlinCommonSources.kt collects commonMain/commonTest sources, but its only caller is the unused ListSourceFilesTask.)

To Reproduce

  1. Create a KMP library module with an Android target.
  2. In commonMain, declare implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1").
  3. In commonMain source, call runSuspendCatching { ... } (its result must flow into non-inline API from the base kotlin-result artifact, which is typical).
  4. Run buildHealth: the dependency is advised for removal from commonMainImplementation.
  5. Remove it: compilation fails with Unresolved reference 'runSuspendCatching'.

Expected behavior
The usage of the inline function is picked up.

Additional context
Workaround:

dependencyAnalysis {
  issues {
    all {
      onUnusedDependencies {
        exclude("com.michael-bull.kotlin-result:kotlin-result-coroutines")
      }
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions