Skip to content

fix(kotlin): a Multiplatform import reaches what the caller compiles against - #3191

Merged
colbymchenry merged 1 commit into
mainfrom
claude/fix-kmp-expect-actual-callers
Nov 1, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/fix-kmp-expect-actual-callers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Reimplements #3136 by @johnhldiver-beep.

A Kotlin Multiplatform expect and its actuals share one qualified name,
and a reference through an import took the nearest declaration, the expect
on a tie. That suits common code only. PR #3023 sent Java's calls through this
pick, so Util.go() from app/src/main/java, which 1.6.2 linked to the
androidMain actual by a name guess, reached the commonMain expect, and
so did extends Timber.Tree, which 1.6.2 left unresolved. An expect isn't in
JVM bytecode, so a Java caller can never reach one. Kotlin in androidUnitTest
or a plain Android module went the same way.

The fix: pickJvmDeclaration chooses by what the caller compiles against, for
the owner of an imported reference and for the import itself. Common source
sets (commonMain, commonTest, okhttp's commonJvmAndroid) keep the nearest
declaration. A platform's source sets take that platform's actual, so
androidUnitTest takes androidMain's, or that of a target they build on
(iosArm64Main from iosMain), and otherwise the expect, as an intermediate
set like nativeMain sees. Java and a plain Kotlin module take a JVM target's
actual, never iosMain's: the one their path names, else not Android's, so
okhttp's samples reach jvmMain and its android-test androidMain.

This reimplements the idea of PR #3136 by @johnhldiver-beep, who found the
cause. Beyond it: commonTest stays on the expect (that PR's check named
commonMain alone), androidUnitTest finds androidMain, Java never lands on
a native target's actual, and the import edge agrees with the calls.

Validation:

  • kotlin-expect-actual-callers.test.ts, 10 cases built from the issue's repro
    and timber's and okhttp's shapes: 8 fail on main, all pass.
  • 1.6.2 run first: the Java cases link the actual by a 0.65 guess, so this is
    a regression from fix(jvm): a call through an import reaches the Java or Kotlin class it names #3023, as the issue says.
  • A/B against main 4499d00: okhttp 9 imports and references move from the
    expect in commonJvmAndroid to the platform actual, each to the target
    its module builds for; nowinandroid, mihon and every Java repo unchanged,
    having no expect. Controls flask and express identical.
  • CPU, mihon, median of 3 interleaved: 20.02 to 19.86 s. Resolution only.

Fixes #3033

Suite: 874 files passed, 6 skipped; 10,847 tests passed, 2 expected fail, 87 skipped, with the kernel on Windows, on the stack of these 8 PRs at 00b79ff2 over main 1f188da. The A/B numbers are against 4499d00; the stack on 1f188da is edge for edge the same arm on jsoup, rocksdb's java/, Minestom, okhttp, tokio, caddy, Alamofire, nowinandroid and mihon, and those repos give the same graph on both mains.

🤖 Generated with Claude Code

…against

A Kotlin Multiplatform `expect` and its `actual`s share one qualified name,
and a reference through an import took the nearest declaration, the `expect`
on a tie. That suits common code only. PR #3023 sent Java's calls through this
pick, so `Util.go()` from `app/src/main/java`, which 1.6.2 linked to the
`androidMain` `actual` by a name guess, reached the `commonMain` `expect`, and
so did `extends Timber.Tree`, which 1.6.2 left unresolved. An `expect` isn't in
JVM bytecode, so a Java caller can never reach one. Kotlin in `androidUnitTest`
or a plain Android module went the same way.

The fix: pickJvmDeclaration chooses by what the caller compiles against, for
the owner of an imported reference and for the import itself. Common source
sets (`commonMain`, `commonTest`, okhttp's `commonJvmAndroid`) keep the nearest
declaration. A platform's source sets take that platform's `actual`, so
`androidUnitTest` takes `androidMain`'s, or that of a target they build on
(`iosArm64Main` from `iosMain`), and otherwise the `expect`, as an intermediate
set like `nativeMain` sees. Java and a plain Kotlin module take a JVM target's
`actual`, never `iosMain`'s: the one their path names, else not Android's, so
okhttp's samples reach `jvmMain` and its `android-test` `androidMain`.

This reimplements the idea of PR #3136 by @johnhldiver-beep, who found the
cause. Beyond it: `commonTest` stays on the `expect` (that PR's check named
`commonMain` alone), `androidUnitTest` finds `androidMain`, Java never lands on
a native target's `actual`, and the import edge agrees with the calls.

Validation:
- kotlin-expect-actual-callers.test.ts, 10 cases built from the issue's repro
  and timber's and okhttp's shapes: 8 fail on main, all pass.
- 1.6.2 run first: the Java cases link the `actual` by a 0.65 guess, so this is
  a regression from #3023, as the issue says.
- A/B against main 4499d00: okhttp 9 imports and references move from the
  `expect` in `commonJvmAndroid` to the platform `actual`, each to the target
  its module builds for; nowinandroid, mihon and every Java repo unchanged,
  having no `expect`. Controls flask and express identical.
- CPU, mihon, median of 3 interleaved: 20.02 to 19.86 s. Resolution only.

Fixes #3033

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kotlin Multiplatform: a Java call to a Kotlin object links the commonMain expect instead of the platform actual

1 participant