Repository navigation
Description
Version: codegraph 1.6.2 (darwin-arm64), fresh codegraph init -y .
Related: #750 / #752 fixed Foo.factory().bar() for Kotlin. This is the neighbouring shape where the factory is an unqualified call (a same-class private helper, implicit this), plus its local-variable form. The helper declares its return type explicitly, so the information #752 relies on is there.
Repro
// src/main/kotlin/demo/domain/Repo.kt
package demo.domain
interface Repo {
fun pending(id: Long): Int
}// src/main/kotlin/demo/data/RealRepo.kt
package demo.data
import demo.domain.Repo
class RealRepo : Repo {
override fun pending(id: Long): Int = 1
}// src/test/kotlin/demo/testutil/FakeRepo.kt
package demo.testutil
import demo.domain.Repo
class FakeRepo : Repo {
override fun pending(id: Long): Int = 2
}// src/test/kotlin/demo/data/RealRepoTest.kt
package demo.data
class RealRepoTest {
private fun repository(): RealRepo = RealRepo()
fun viaConstructor() {
val repo = RealRepo()
repo.pending(1)
}
fun viaHelperLocal() {
val repo = repository()
repo.pending(1)
}
fun viaHelperChain() {
repository().pending(1)
}
}codegraph init -y . && codegraph callees <fn>:
| Function | Call | Expected | Actual |
|---|---|---|---|
viaConstructor |
val repo = RealRepo(); repo.pending(1) |
RealRepo::pending |
RealRepo::pending ✅ |
viaHelperLocal |
val repo = repository(); repo.pending(1) |
RealRepo::pending |
Repo::pending (interface) |
viaHelperChain |
repository().pending(1) |
RealRepo::pending |
FakeRepo::pending ❌ |
- The wrong edge needs the decoy in a different package. With all three classes in one package, both helper calls land on the interface method instead (imprecise, but not wrong).
- Same results with the calls inside a lambda (
runTest { … }),for/ifblocks, a block-bodied helper, andsuspendfunctions.
Impact
callers RealRepo::pending misses the helper-based call sites, and callers FakeRepo::pending gains a call it never receives. So impact / affected on a repository implementation under-reports its tests and points at the fake. This is the common Android test setup: a private repository() builder in the test class, and a shared fake in a testutil package.
In a larger private Kotlin codebase (~1.1k Kotlin files) the local-variable form went further: val repo = repository(); for (…) { repo.m(…) } inside runTest { } produced no calls edge to m at all (the test's callees listed repository but not m). I couldn't reduce that variant: the toy project above resolves it to the interface. Treat it as an extra data point.
Expected
Infer the receiver type from the helper's declared return type for unqualified calls and for val x = helper() locals, the same way it already works for Foo.factory() (#752) and constructor-initialised locals. Per #750's invariant, when the type can't be validated, emit no edge rather than one to a same-named method on an unrelated class.
Happy to turn the repro into a fixture PR if that helps.
Activity
fix(kotlin): repository().pending() links the method of the type repo…
colbymchenry commentedon Oct 23, 2026
Thanks for the clear report and the four-file example, @mindcipherrr.
#2822 is merged to
mainand fixes both forms. Withprivate fun repository(): RealRepo, bothrepository().pending(1)andval repo = repository(); repo.pending(1)now link toRealRepo.pending. That holds whether the helper is declared in the class, inherited from a base class or declared at the top level. When the declared type has no such method, the call now links to nothing instead of a same-named method elsewhere, and types from outside your project are still matched by name as before.It ships in the next release; re-index your Kotlin project to pick it up. If the
val repo = repository()local insiderunTest { for (…) { … } }in your larger codebase still shows no edge after that, a small snippet with that shape would help us track it down.fix(kotlin): repository().pending() links the method of the type repo…
fix(rust): kotlin repository().pending() links the method of the type…