Skip to content

fix(kotlin): a call through a property or a receiver chain resolves on its declared type - #2972

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1933-kotlin-receiver-types
Oct 28, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1933-kotlin-receiver-types

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

In Kotlin, a call through a property now links to the method of the type the property is declared with: a lateinit var server: MockWebServer, a constructor property class A(private val repo: Repo), a val set from a constructor or from a call whose function declares its return type, a property a class inherits, a chain like engine.pump.drain() or Mode.ON.next(), and a local of the function around an anonymous object : …. A call on a type from outside the project, like Regex or a JDK class, no longer links to a same-named method of the project. A fun interface is now indexed as the interface it is, instead of a broken function that could hide the declaration after it.

Root cause

Kotlin indexes a class property without a signature, and a primary-constructor property is no node at all, so main's field reader found no type for either and the call fell to the name-only strategies: an interface's method, or whichever method had the name. A call through a chain was emitted as its bare method name, so its receiver was never typed. tree-sitter-kotlin predates fun interface: such a declaration parsed as a broken function, and with a doc comment after it, error recovery swallowed the next declaration too, so that interface and its methods went missing and its members surfaced as top-level functions.

From the contributor, and what we changed

From @danusha2345 (#1933), unchanged: the Kotlin declared-type reader (a property's own lines, the class header for a constructor property, a callee's declared return type, an enum entry's enum, aliases, requireNotNull-style pass-throughs, a superclass's property, an outer function's local inside an anonymous object), the receiver chain kept in the reference name by both extractors (tree-sitter.ts and the kernel's kotlin.rs) and typed segment by segment, the library-type rule (no edge unless the project declares an extension of that name on a library type), the fun interface preParse blanking, its kernel-parity fixture, and kotlin-property-receiver.test.ts.

Our changes: the two CHANGELOG bullets rewritten in the house format and moved after the other Kotlin call bullets. The PR was rebased onto main by its author after our first review; the new head's diff equals the old one apart from the resolution/index.ts import line, and we re-read every changed line before running it. Rebased onto main 77ee3484, which has #1942 (#2965): the two share inferJavaFieldReceiverType's field block, where the Java branch is #1942's and this PR adds the Kotlin kinds and the Kotlin branch.

Validation

Tests. kotlin-property-receiver.test.ts: 17 tests, 12 red on main, all green here; kernel-kotlin-parity.test.ts (6) green with the rebuilt kernel.

Kernel parity (scripts/kernel-parity.mjs --lang kotlin), 0 files with diffs: okhttp 588/619 files byte-identical (578 with main's kernel), mihon 891/940 (887), nowinandroid 340/350 (339), SystemUI's Kotlin 188/198, the torture fixture 1/1. The files that no longer defer to wasm are the fun interface ones, which now parse cleanly.

A/B against main 7973df0d (with #1942; 77ee3484 after it changes only TypeScript path aliases), fresh indexes with each arm's kernel. Edges are compared by position and target, without the reference name, since this PR keeps a chain in it. The changed sites are the same, site for site, as in an earlier run against 4eb8c2a5 with #1942 stacked under this PR:

repo edge sites added removed retargeted confidence raised
square/okhttp c317930 299 257 2,554 2,186
mihonapp/mihon d001e6f 82 118 337 169
android/nowinandroid 2982e63 0 9 23 51
SystemUI (Kotlin files of the sparse checkout) 1 104 7 37
jextract (Java), click, gin (controls) identical

Every changed edge has a Kotlin caller (one Java caller in okhttp reaches a fun interface method). Triage:

  • Retargeted: 2,158 of okhttp's are test calls (server.enqueue(…), url, takeRequest, useHttps) moving from the deprecated okhttp3.mockwebserver.MockWebServer, which main's 0.65 receiver-name guess picked, to mockwebserver3.MockWebServer, which all 47 calling files import. Others sampled right: finder.find() on private val finder = FastFallbackExchangeFinder(…), server.takeRequest() on private val server = FakeDns(), ANY_VALUE.fromDer onto BasicDerAdapter. mihon's are Preference calls (get 99+, changes 69, register 46, set 39) moving from a subclass main guessed to the declared Preference; nowinandroid's move properties onto the concrete test doubles they are declared as (TestUserDataRepository).
  • Removed: name guesses on library types: okio sinks and buffers (sink.writeUtf8 → a project DerWriter), collections' filter, clear, remove, OkHttp response bodies, coroutines' MutableStateFlow.update (which reached nowinandroid's ScrollThumbElement.update), Kotlin's apply, SQLDelight query objects, Android's SnackbarData. Right edges lost, 7: a chain through a library wrapper reads as a library type, so downloadManager.value.renameSource(…) and extensionManager.value.getExtensionPackage(…) (a Lazy), result?.first?.toSuccessSearchResult() (a Pair), activity.binding.navigationOverlay.setNavigation(…) ×3 (a generated view binding) on mihon, and viewModel.uiState.value.shouldKeepSplashScreen() (a StateFlow) on nowinandroid.
  • Added: typed calls main had no edge for: okhttp's WebSocketRecorder.assertOpen (47) and other recorder and MockWebServer calls through chains, and HttpLoggingInterceptor.Logger.log, a fun interface method now indexed; mihon's Preference.collectAsState (51).
  • Nodes: okhttp's 11 and mihon's 4 fun interface declarations are interfaces with their methods, and the declarations after them come back.

Cost (mihon, median of 3 interleaved fresh indexes): process CPU 25.8 s on main with #1942 vs 26.7 s with this PR, wall 8.3 s vs 8.4 s.

Suite: CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4 --minWorkers=1 on 7bcacd9b (Windows 11, kernel built): 772 files passed, 6 skipped; 9,241 tests passed, 85 skipped. Two tests hit the 5 s timeout while other jobs loaded the machine (explore-long-query.test.ts, mcp-nested-repo-projectpath.test.ts, neither Kotlin); both files pass re-run on the same commit, 26/26.

Follow-ups (not in this PR)

  • A chain through a library wrapper whose accessor returns a project type (Lazy.value, StateFlow.value, Pair.first, a view binding's views) reads as a library type: 7 right edges on mihon and nowinandroid.
  • JAVA_EXTERNAL_TYPE and KOTLIN_EXTERNAL_TYPE are the same sentinel string and could share one constant.

Lands #1933 by @danusha2345.

🤖 Generated with Claude Code

…n its declared type

Kotlin indexes a class property without a signature, and a
primary-constructor property (class A(private val repo: Repo)) is no
node at all, so a call through either (probe.close() on a lateinit var
probe: HttpProbe) found no receiver type and fell to the name-only
strategies: the interface's method, or any same-named method elsewhere.
A call through a chain (engine.pump.drain()) was emitted as its bare
method name, and a local bound to a qualified call (val lease =
HardwareLock.tryBegin()) was untyped. tree-sitter-kotlin also predates
fun interface: such a declaration parsed as a broken function and, with
a doc comment after it, its error recovery swallowed the next
declaration too.

The declared type is now read from the declaration itself: a property's
own lines, the class header for a constructor property, a callee's
declared return type for a local bound to a call, an enum entry's enum,
each segment of a receiver chain in turn (the chain is kept in the
reference name, by the wasm extractor and the native kernel alike), a
superclass's property, and an outer function's local inside an
anonymous object. A receiver whose type the project does not declare
(Regex, Properties, a JDK class) gets no edge instead of a name-only
guess, unless the project extends that type. The extractor's preParse
blanks the fun of a fun interface, offset-preserving, so the file parses
cleanly in both paths.

Validation:
- kotlin-property-receiver.test.ts: 17 tests, 12 red on main, all
  green here; kernel-kotlin-parity.test.ts pins the fun interface
  fixture.
- Kernel parity (kotlin), 0 diffs: okhttp 588/619 files byte-identical
  (main's kernel 578, the rest deferred to wasm on parse errors), mihon
  891/940 (887), nowinandroid 340/350 (339): the fun interface files
  no longer defer.
- A/B against main with #1942 (positions only: the extractor now keeps
  a chain in the reference name): okhttp +299/-257, 2,554 retargeted,
  2,158 of them test calls moving from the deprecated
  okhttp3.mockwebserver.MockWebServer (a 0.65 name guess) to the
  mockwebserver3.MockWebServer each file imports; mihon +82/-118, 337
  retargeted (Preference get/changes/set/register onto the declared
  Preference); nowinandroid -9, 23 retargeted (properties onto their
  concrete test doubles); SystemUI's Kotlin +1/-104, 7 retargeted;
  jextract, click and gin identical. The removed are name guesses on
  library types (okio sinks, collections' filter and clear, OkHttp
  bodies, MutableStateFlow.update, Kotlin's apply). 7 right edges are
  lost where a chain passes through a library wrapper (Lazy.value,
  StateFlow.value, Pair.first, a ViewBinding's view).
- Cost (mihon, median of 3 interleaved indexes): CPU 25.8 s on main
  with #1942 vs 26.7 s, wall 8.3 vs 8.4 s.

Original PR #1933 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 01e0ac3 into main Oct 28, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1933-kotlin-receiver-types branch October 28, 2026 02:14
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 28, 2026
…mchenry#2972; 10 landed PRs yield to main; EXTRACTION_VERSION 56)

Main landed ten more of our PRs as our own commits with the maintainer's additions
(colbymchenry#2923 ← colbymchenry#1936, colbymchenry#2924 ← colbymchenry#1951, colbymchenry#2925 ← colbymchenry#1939, colbymchenry#2930 ← colbymchenry#2921, colbymchenry#2940 ← colbymchenry#1736,
colbymchenry#2942 ← colbymchenry#1947, colbymchenry#2950 ← colbymchenry#1952, colbymchenry#2961 ← colbymchenry#1954, colbymchenry#2965 ← colbymchenry#1942, colbymchenry#2972 ← colbymchenry#1933) and the
module-path half of colbymchenry#2308 (colbymchenry#2955). Where the integration carried our version, main's
wins: the enclosing-scope member pick and its tests, the Go and Java receiver typers,
the delegating-call self-loop rule, the duplicated-module copy confidence, the C #if
collapse, the module-path resolution of Rust paths, the trail/path hardening in
utils.ts. Kept beside main: colbymchenry#1875's Verilog hooks and status fields, colbymchenry#2839's
implementations command, colbymchenry#1923, colbymchenry#2922, colbymchenry#2258, colbymchenry#1761, colbymchenry#1583, the bare-call half of
colbymchenry#2308 (rust-use-bindings.ts). CHANGELOG is main's plus the open PRs' bullets.

EXTRACTION_VERSION 56: the C #if group handling (colbymchenry#2940) and the kernel change.

Co-Authored-By: Claude Fable 5.1 <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.

1 participant