What happens
A Kotlin member call on a typed receiver produces no edge at all when the receiver's
class is declared in another file, and nothing survives into graph.json when it is
declared in another repository.
Two files, one corpus:
// Greeter.kt
class Greeter {
fun greet() {}
}
// App.kt
class App(private val greeter: Greeter) {
fun run() { greeter.greet() }
}
App.run → Greeter.greet is missing. The shared cross-file pass skips member calls,
and Kotlin exports no receiver type table, so greeter is never typed: the call falls to
raw_calls and no resolver claims it. affected and every reverse-dependency query
under-report by exactly the calls that make a Kotlin call graph a call graph.
Split the same code into two repos and merge:
repo app/ App.kt (as above)
repo lib/ Greeter.kt
merge-graphs adds nothing, because the single-repo build never recorded that App.run
wanted a Greeter.greet it could not find. This is the Kotlin instance of #3152, whose
merge pass can only finish calls a build parked.
Expected
greeter.greet() resolves to Greeter.greet in one corpus (INFERRED — the type came
from a table, not from the call site), and Registry.register() on an object resolves
EXTRACTED, matching the Swift/C#/Java tiering.
- When the receiver's type is declared nowhere in the build, the call is parked so
merge-graphs / global add can bind it to the single declaration another repo owns.
Notes
Every source of a receiver's type has to be read, because Kotlin's idiomatic injection
point (class App(private val greeter: Greeter)) declares no property at all — a
property-only table would miss the most common shape. The sources are: primary
constructor parameters, property declarations, function parameters, and local
val x = T() bindings whose type is only implied by the constructor call.
Ambiguity must stay unresolved rather than be guessed: two classes named Greeter, or a
receiver typed to a builtin that shares a name with a local class (class Regex vs
kotlin.text.Regex), produce no edge.
A paired PR follows.
What happens
A Kotlin member call on a typed receiver produces no edge at all when the receiver's
class is declared in another file, and nothing survives into
graph.jsonwhen it isdeclared in another repository.
Two files, one corpus:
App.run→Greeter.greetis missing. The shared cross-file pass skips member calls,and Kotlin exports no receiver type table, so
greeteris never typed: the call falls toraw_callsand no resolver claims it.affectedand every reverse-dependency queryunder-report by exactly the calls that make a Kotlin call graph a call graph.
Split the same code into two repos and merge:
merge-graphsadds nothing, because the single-repo build never recorded thatApp.runwanted a
Greeter.greetit could not find. This is the Kotlin instance of #3152, whosemerge pass can only finish calls a build parked.
Expected
greeter.greet()resolves toGreeter.greetin one corpus (INFERRED — the type camefrom a table, not from the call site), and
Registry.register()on anobjectresolvesEXTRACTED, matching the Swift/C#/Java tiering.
merge-graphs/global addcan bind it to the single declaration another repo owns.Notes
Every source of a receiver's type has to be read, because Kotlin's idiomatic injection
point (
class App(private val greeter: Greeter)) declares no property at all — aproperty-only table would miss the most common shape. The sources are: primary
constructor parameters, property declarations, function parameters, and local
val x = T()bindings whose type is only implied by the constructor call.Ambiguity must stay unresolved rather than be guessed: two classes named
Greeter, or areceiver typed to a builtin that shares a name with a local class (
class Regexvskotlin.text.Regex), produce no edge.A paired PR follows.