Repository navigation
fix(kotlin): type receivers from declarations; parse fun interface - #1933
danusha2345 wants to merge 2 commits into
Conversation
|
Follow-up to The separate Java |
Integrate Kotlin property receiver resolution from upstream colbymchenry#1933
|
Thanks for splitting these out. I reviewed #1948 against the Android corpus and approved it, and it is in the build I run alongside this PR. The two touch different receiver shapes: #1948 covers explicitly imported types and aliases, while this PR types receivers from their own declarations. So they should land in either order. |
b04da73 to
beaea10
Compare
3e8fdd6 to
9217f66
Compare
…tion tree-sitter-kotlin has no `fun interface` (Kotlin 1.4 functional interfaces). The declaration parsed as a broken function, and when a doc comment followed it, error recovery swallowed the NEXT declaration too: an interface and its methods went missing, surfacing as top-level functions, or a class vanished with its members. The Kotlin extractor's preParse now blanks the `fun` of a `fun interface` declaration (three spaces for three letters, so offsets hold); the kernel receives the same bytes through the preParse hoist and no longer defers these files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…guessed Kotlin properties carry no signature and primary-constructor properties are no nodes, and upstream's member inference (colbymchenry#2171) reads only a written type annotation (`val x: T`). A Kotlin receiver typed any other way fell to name-only guessing: the interface method, a same-named method in the caller's file, or any project method when the receiver is a library type. The resolver now reads a Kotlin receiver's type from its declaration: - a property's own lines or the class header (a constructor property), a `val` (indexed as a constant) and a companion property (a variable); - a constructor call, including a trailing lambda (`Thread { … }`) and type arguments (`Crate<Int>()`); a call, typed by the callee's declared return type (a nested return type keeps its owner: `HardwareLock::Lease`); a project enum entry (`var mode = Mode.ON`); an alias (`val old = current ?: return`, `current!!`, `requireNotNull(x)`); a typed parameter; - a property inherited from a project supertype (breadth-first, four levels; a library or ambiguous supertype is not walked, so the receiver keeps the name-only resolution); - a local of the outer function captured by an anonymous object's member. A receiver chain (`engine.pump.drain()`, `this.engine.drain()`, `a?.b?.c()`, up to four segments) is now kept by the extractor (wasm path and kernel, in parity) and typed segment by segment; an untyped chain resolves as the bare method name, exactly as before. A type the project does not declare (`Regex`, `Executors`, stdlib factories such as `mutableSetOf`) gets no edge, unless the project declares an extension of that name on a library type (`fun Thread.label()`). Rebased onto the 2026-09-30 resolution wave; squashes the PR's receiver commits (648e02c, 230c91c, 2a47c9b, a956373, beaea10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9217f66 to
caeab08
Compare
Problem
inferJavaFieldReceiverTypereads a field's type from itssignature, in the Java shapeType name. Kotlin can't give it a type there:private lateinit var probe: HttpProbe) is indexed withsignature: null;class A(private val repo: Repo)) is not indexed at all.So every Kotlin call through a property got no receiver type and fell through to name-only matching (
instance-method, confidence 0.65–0.8). That returns the interface method, or any same-named method in another class. On a real Android app (anonymized here):Change
For Kotlin,
inferJavaFieldReceiverTypenow reads the declared type from the declaration itself:val|var name: Typeorval|var name = Type(…);HardwareLock.LeasebecomesHardwareLock::Lease, so it matches thatLeaseand not another class's. A lowercase package prefix is dropped.HttpProbe?) reads asHttpProbe.resolveMethodOnTypestill validates the method on that type, so a wrong read yields no edge rather than a wrong one. Java is unchanged: it keeps the signature path.Measured
I indexed three real Kotlin Android apps with
mainand with this branch:I checked each retargeted edge against the source:
tearDown → closeand fiveprobe.probe(…)calls: from an unrelated class / the interface toHttpProbe.FlightSdkPortthe property is declared as.control.execute()calls: from a self-edge / a sibling capability to the declared…Control.gate.begin(): from an unrelatedStatsPollGateto the declaredStartupFallbackGate.In app B the nested
HardwareLock.Leaseproperty now resolves by type, 0.9 instead of 0.65, and to the same target as before.Tests
New
__tests__/kotlin-property-receiver.test.tscovers four cases:lateinitproperty with an interface + implementation;Outer.Innertype, with two classes sharing the inner name.On current
mainthe four original cases already pass: thelateinitproperty, the primary-constructor property (nullable included), the property initialized by a constructor call, and the nestedOuter.Innertype.mainhas read class-level member declarations for Kotlin since #2171, so those four are kept here as regression guards. What this PR still adds is everything else: of the 17 cases inkotlin-property-receiver.test.ts, 12 fail onmain(ed199e6) and 5 pass (those four plus the control where a receiver not found up to a library base class keeps the name-only resolution). The full suite passes (4722 passed),build:kernelandtscare clean.I didn't bump
EXTRACTION_VERSION: existing Kotlin indexes pick this up onindex -f. Happy to add the bump if you'd rather.Follow-ups in this PR
Two more commits close the gaps the first one exposed on the same apps:
fun interface(c3aeada). tree-sitter-kotlin has no functional interfaces.fun interface Reply { … }parsed as a broken function, and when a doc comment followed it, error recovery swallowed the next declaration too. On the app above, aninterface ControlSessionEventswent missing (its methods surfaced as top-level functions), and so did aclass DirectRouteSelectorwith its members.preParsenow blanks thefunof afun interfacedeclaration: three spaces for three letters, so offsets hold.val lease = HardwareLock.tryBegin()andprivate val schema = Checker.load()are typed by the callee's declared return type, read from its signature. A nested return type keeps its owner.Regex,Properties, a call onExecutors) now gets no edge instead of the name-only guess. That guess producedregex.find()→ an unrelatedfind,socket.connect()→ a projectconnect, andx.close()→ itself.Across the three apps (
main→ this PR):Every removed edge I checked was a self-loop or a call on a library type.
The tests now also cover a call-bound property and local, a
fun interfacefollowed by a doc comment and an interface, and library receivers (Regex.find,Executors…shutdown). Onmainthose three fail. The full suite passes (4725 passed), andkernel-kotlin-parityis green withCODEGRAPH_KERNEL_EXPECT=1.Third follow-up: remaining receiver shapes
On the same apps I went through what was still name-guessed and fixed each shape:
valand companion properties. Avalproperty is indexed as aconstantand a companion-object property as avariable; the lookup only tookfield.var mode = Mode.ONis typed by its enum. Project enums only:Limits.MAXis an Int.Thread { … }counts as a constructor call.val old = current ?: return,current!!, andrequireNotNull(x)take the aliased value's type.listOf,lazy,threadand the like return a library type. Any other library function leaves the type unknown, so it keeps the old behaviour.Final numbers against
main: call edges through a receiver that are still name-guessed (confidence ≤ 0.7), and call edges resolved by type (≥ 0.9):Every removed edge I checked was either a self-loop or a call on a library type bound to a same-named project method or a test fake:
thread.join(),process.destroy(),webSocket.send(),input.read(),text.isEmpty(). Tests: 11 cases inkotlin-property-receiver.test.ts. The full suite passes (4729).Fourth follow-up: receiver chains (8c4dc2b)
a.b.method(),this.a.method()anda?.b?.c()could not be typed at all. For a chained receiver, the Kotlin extractor kept only the bare method name, in both the wasm extractor and the kernel.engine.pump.drain); anything else still emits the bare name. A chain block was added totorture.kt, and kernel parity holds.matchKotlinReceiverChaintypes the first segment with the receiver logic above (thisis the enclosing class, a capitalized segment is an object or companion, an enum entry takes its enum's type). It then walks each later segment as a property declared in the previous type's class.fun Context.dp()), which keeps the old resolution.Session<Ice>(…),mutableListOf<T>()) now types its variable.On app A, every call edge whose confidence is ≤ 0.7 (receiver or not): 642 → 523; calls resolved by type: 1656 → 1750.
ArrayDeque,LinkedBlockingQueue,mutableListOf<…>) andScheduledFuture.cancelhad been bound to same-named project methods, including self-edges.runtime.session.isCurrentPeer→WebRtcPlaneSession::isCurrentPeer,reconnectPolicy.reset→SignalingReconnectPolicy::reset.Tests: 14 cases in
kotlin-property-receiver.test.ts, with the kernel on and off. The full suite passes (4732).Fifth follow-up: inherited properties and captured outer locals (b04da73)
On the apps above this changed 7 call edges, all correct:
stop()calls reached a single implementation (DjiTelemetrySource) by name, and now reach the declared interfaceTelemetrySource;lateinit vars used inside an anonymousControlEndpoint.The inherited-property walk didn't fire on these apps, which have no calls through an inherited property, so it is covered only by the test cases.
🤖 Generated with Claude Code