Kotlin: receiver-typed member calls, and park them for cross-repo merges - #3389
Kotlin: receiver-typed member calls, and park them for cross-repo merges#3389xiongjianxu wants to merge 2 commits into
Conversation
The shared cross-file pass skips member calls, so `greeter.greet()` on a receiver whose class is declared in another file produced no edge at all — the Kotlin twin of the Swift gap in Graphify-Labs#1356. Kotlin had no receiver typing to fall back on: the engine exported no per-file type table for it, and the call site stamped no receiver. The extractor now builds `kotlin_type_table` from the four places a Kotlin name gets a declared type — a primary-constructor parameter, a property, a function parameter, and a local `val`/`var` binding (annotated, or constructed with a capitalized head). First binding wins, so a parameter named like a property cannot redirect the property's own calls. Two-segment `recv.method()` chains now stamp `member_receiver`, and every Kotlin raw_call carries `lang="kotlin"`. `_resolve_kotlin_member_calls` reads the table, takes the single class or object declaring that type, and emits the `calls` edge to its member. A capitalized receiver is the type itself (`Registry.register()`), which is exact and stays EXTRACTED; a table-typed one is INFERRED. Kotlin builtins are excluded so a local `class Regex` cannot answer for `kotlin.text.Regex`. Kotlin is excluded from the capitalized-receiver deferral: `Foo.bar()` resolves in-file today and the receiver type is only usable once the bare name misses locally, which already leaves the target unresolved. Three-segment chains stay with the fully-qualified pass (Graphify-Labs#2550).
A Kotlin receiver typed to a class this build declares nowhere is a call into another repository, not a mistake. The resolver held the receiver type and dropped the call, so graph.json — the only artifact merge-graphs and global add read — recorded nothing and no merge-time pass could recover it. Those calls are now parked on the caller node by name, and the merge pass binds them when the type resolves to exactly one declaration in another repo. A Kotlin entry accepts a `.java` declaration as well: the JVM classpath is one namespace, and a Kotlin module over a Java library is the common Android shape.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds Kotlin cross-file member-call resolution: _resolve_kotlin_member_calls looks up a receiver's declared type in the per-file kotlin_type_table (or treats a capitalized receiver as the type for companion/object/static calls), then emits a calls edge to the single class/object declaring that method — EXTRACTED when the type is named in source, INFERRED when pulled from the table. Bails on ambiguous multi-definition types and skips Kotlin/Java/global builtin types; a receiver typed to a class declared nowhere in the corpus is parked on the caller for a later merge rather than dropped. Extends the kotlin cross-repo suffix set to include .java so Kotlin calls can bind against Java declarations, and registers the new resolver alongside the existing qualified-call pass.
Worth a look
- Kotlin overloads are resolved to an arbitrary method —
graphify/extract.py:4467· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Existing non-call edge suppresses required call edge —
graphify/extract.py:4496· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _kotlin_constructor_type returns None after first non-matching call_expression head —
graphify/extractors/engine.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Kotlin local shadowing is ignored —
graphify/extractors/engine.py:887· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2167 functions depend on the 524 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
_extract_generic()— 18 callers, 27 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
extract_objc()— 27 callers, 9 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 44 more — each is listed as a finding
Verification — 2167 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2002 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 51 more finding(s) on lines outside this diff (see the check run).
| "line": "L2"}] | ||
|
|
||
|
|
||
| def test_a_kotlin_call_binds_to_a_java_declaration(): |
There was a problem hiding this comment.
test_a_kotlin_call_binds_to_a_java_declaration()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #3388. Two cherry-pickable commits: the single-repo resolver, then the park that
lets a merged graph finish what one build cannot.
Commit 1 — resolve Kotlin member calls through the receiver's declared type
Kotlin had no receiver typing at all, so
greeter.greet()across files produced no edge.This builds the per-file table (
kotlin_type_table) from every place a receiver's type iswritten, and adds
_resolve_kotlin_member_calls, which takes the single class/objectdeclaring that type and emits
callsto its member — EXTRACTED when the receiver namesthe type in source (
Registry.register()), INFERRED when the type came from the table.Table sources, all four needed:
class App(private val greeter: Greeter)declares no property, so nothing else in the walk ever names the typeprivate val greeter: Greeter = Greeter()fun run(greeter: Greeter)val greeter = Greeter()— the type is only implied by the capitalized call headDesign calls:
member_receiveris stamped only for the 2-segment navigation case, and Kotlin isexcluded from the capitalized-receiver deferral. A call either resolves in-file or
falls through to
raw_calls; deferring would move today's in-fileFoo.bar()hits intoraw_callsand regress them. Not deferring keeps in-file behaviour byte-identical andonly enriches the
raw_callsentry on an in-file miss — which is the only situationwhere the receiver type is of any use. A
>= 3-segment chain is an FQN and still goesto
_resolve_kotlin_qualified_calls(Kotlin: fully-qualified call expressions produce no calls edge (same-package control isolates the qualified form) #2550); the broaderlang="kotlin"tag cannotpoach its calls, since that pass requires
qualified_prefix.Without it
fun other(greeter: Other)clobbers the class's ownprivate val greeter: Greeterand redirects the property's calls toOther.greet. Thetable is flat per file, so a parameter shadowing a property has to lose.
callsonly — noreferencesfallback to the type node when no member matches(Swift has one). Smaller blast radius, and such an edge adds nothing the type-reference
walk already emits.
Kotlin imports a class name into scope rather than a module alias, so a capitalized
receiver genuinely is a type, not a namespace.
Commit 2 — park what this build cannot answer (#3152)
A receiver typed to a class with zero declarations in this corpus is parked on the
caller as a
metadata.unresolved_callsentry (names only, never node ids — those arerewritten by the #1529 remap and again by repo prefixing).
> 1declarations staysdropped: local ambiguity is not something merging can narrow. The merge pass then binds
the entry when exactly one declaration in another repo answers it.
_LANG_SUFFIXES["kotlin"]is{.kt, .kts, .java}. The JVM classpath is one namespace, soa Kotlin module calling a Java library in another repo is a genuine member call; excluding
.javawould drop the most common Android two-repo shape. The reverse (javaaccepting.kt) is left alone here — adding it could make an existing Java↔Java pair ambiguous andso remove an edge that lands today.
Precision costs, measured
share a name collapse to one entry. First-binding-wins makes the property the winner,
which is the safer of the two; the loser's calls go unresolved rather than mis-bound.
Log.d(),Build.VERSION) are in neither_KOTLIN_BUILTIN_TYPESnor_JAVA_BUILTIN_TYPES, so they park. In a merge they can bindto a same-named class another repo declares — the single-definition guard limits the
damage but does not rule it out.
Verification
tests/test_kotlin_receiver_member_calls.py— 10 cases, one per type source plus thenegatives that must stay unresolved (untyped
Anyreceiver, two same-named classes, abuiltin
Regexshadowed by a local class, FQN still reaching Kotlin: fully-qualified call expressions produce no calls edge (same-package control isolates the qualified form) #2550's pass). 6 of the 10fail on
v8.tests/test_cross_repo_member_calls.py— akotlin-primary-constructorarm in theper-language park→merge test, plus unit cases pinning that a Kotlin call binds to a Java
declaration and does not bind to a Swift one.
ruff check graphify testsclean.