Repository navigation
fix(resolution): a caller of an imported expect/actual reaches the actual its source set holds (#3033) - #3136
Open
johnhldiver-beep wants to merge 1 commit into
Conversation
…tual its source set holds (colbymchenry#3033) resolveJavaImportedReference (colbymchenry#3023) reads an imported Kotlin Multiplatform declaration by qualified name, which returns BOTH the commonMain expect and the platform actuals, and picks the owner with pickClosestJvmCandidate — a rule written for a commonSide import ref, which prefers an expect on a directory-proximity tie. A Java caller in an app module shares no directory segments with either, so the tie fired and the call landed on the expect — a declaration JVM bytecode never contains — instead of the actual: timber's Java sample calling Timber.plant(...) or extending Timber.Tree reached the commonMain declarations. The pick is now caller-aware: a Java caller, and a Kotlin caller in a platform source set, take the actuals — the one in the caller's own source set when there are several, proximity among them otherwise — while a commonMain caller keeps the expect its compile sees. The import-ref itself (resolveJvmImport) keeps the commonSide reading. Kotlin callers in a source set the library holds were already right through proximity; the tie-break was what betrayed callers outside it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A Java call to a Kotlin Multiplatform
objectlinks thecommonMainexpectdeclaration instead of the platformactual. Anexpectdoes notexist in JVM bytecode, so a Java caller can only ever reach the
actual.Regression after #3023: at 01e0ac3 the same call linked the
androidMainactual. Real case: JakeWharton/timber'sTimber.plant/Timber.tagmoved from
androidMaininto acommonMainexpect.Fixes #3033.
Root cause
resolveJavaImportedReference(the JVM import resolution #3023 added)reads the imported declaration's fully qualified name through
jvmImportedDeclarationsand picks its owner withpickClosestJvmCandidate.That rule was written for the common-side Kotlin
import refcase andprefers
expectwhen the evidence is tied. A Java caller fromapp/src/main/javashares no source set with either declaration, so thetie-break fires and picks
expect.Fix
In
src/resolution/import-resolver.ts(+29 lines):kmpSourceSetreads thesrc/<set>/path segment, so a declarationknows which source set holds it.
jvmExpectActualPick: when the candidates for one declaration spanexpectandactual, the pick becomes caller-aware — a Java caller(JVM bytecode holds no
expect) and a Kotlin caller outsidecommonMaintake theactual(the one from the caller's own sourceset when several exist, otherwise by proximity); a
commonMaincaller keeps the
expect.resolveJvmImport) still keeps its common-sideresolution; only the owner pick changes.
No public API touched; no unrelated code reformatted.
Regression tests
New
__tests__/kotlin-expect-actual.test.ts, 5 end-to-end cases: theissue's
Util.gorepro (Java caller →androidMainactual), timber'sTimber.plant(companion member of the actual), the issue'sextends Timber.Tree(the actual's nested type), a KotlinandroidMaincaller in another package (own source set's actual), and a
commonMaincaller keeping the
expect.Red-to-green on
main: the two Java-caller cases failed before the fixand pass after; the two Kotlin-caller cases already passed on
mainand still pass. After the fix: 5/5.
Verification (disk- and log-verified)
npm run buildgreen;npx tsc --noEmitclean after every edit.jvm-cross-language-imports(the fix(jvm): a call through an import reaches the Java or Kotlin class it names #3023regression surface) +
java/*+kotlin/*all green (51 + 51).All 8 failures are environmental or load flakes, proven unrelated to
this diff: the 6 known ones from earlier the same day (gitignore x4,
busiest-symbol x1, vitest-worker-flags x1, ui-package collect — all
reproduced on bare
main), plusmcp-daemon.test.tsx2 (socket-timingtests) which pass 24/24 in isolation on BOTH the branch and bare
main.console.errorlinesand a stray blank line; re-verified after cleanup (
tsc, build,expect-actual + cross-import suites 13/13).
filing); no rebase needed. One commit, 3 files, +167/-1.
RFC note
An RFC comment was posted on issue #3033 before implementation
(comment 6538257515, 2026-10-31) describing the reproduction, the root
cause in
src/resolution/import-resolver.ts, and the planned fix. Nomaintainer reply arrived after a full review round, so this PR is filed
for review with the RFC on record.
Risk
Callers that previously (wrongly) linked
expectnow linkactual.commonMaincallers are unchanged by design. The change is gated to theexpect/actual owner pick only; ordinary Java/Kotlin import resolution is
untouched, and the #3023 regression surface stays green.