Repository navigation
Description
Summary
#693 ("CodeGraph does not index call edges from anonymous/lambda functions") was fixed for Go only, by #744. The same gap is still open in Kotlin, Java, TypeScript/JavaScript, Scala, Rust and Python — and in most of them it is worse than in Go: the calls are not merely attributed to the file node, they never reach the graph at all.
The consequence is the one #693 described: a function reached only through a declaration's initializer looks like it has no callers, and impact under-reports its blast radius. On Android/MSDK code, on Scala, and on any Python module that wires itself up at import time, that is most of the wiring.
One correction to #693's closing note, which says JS/TS didn't have this gap: that holds only for const f = () => … (an arrow value delegates to extractFunction and gets its own node). For every other JS/TS initializer shape the gap is real — see the table.
Repro, one file per language
class C {
private val fieldLambda: () -> Unit = { target() } // no caller
private val samField = Runnable { target() } // no caller
private val plain = compute() // no caller
private val delegated by lazy { compute() } // no caller
val sameLine: Int get() = compute() // no caller
init { val cfg = load() } // `load` vanishes
val (a, b) = makePair() // `makePair` vanishes
}
object Holder { val cb: () -> Unit = { hit() } } // no callerclass T {
private final Runnable fieldLambda = () -> target(); // no caller
private final Runnable l = new LocationListener() { … target() … }; // class + method invisible
private final int eager = compute(); // no caller
}const cfg = load(); // caller recorded as the FILE node
const obj = { handler: () => target() }; // nothing at all — no member node, no edge
const list = [() => target()]; // caller recorded as the FILE node
export const ok = { handler: () => target() }; // works (exported object-of-functions path)class C {
val fieldLambda: () => Unit = () => target() // no caller
val direct = compute() // no caller
lazy val lazily = compute() // no caller
}const LEN: usize = compute_len(); // no caller
static REGISTRY: Lazy<Cfg> = Lazy::new(|| build_cfg()); // no callerAPP = compute() # no caller
handler = lambda: target() # no caller
MAPPING = {"a": compute()} # no caller
first, second = compute(), other() # no caller, and no symbol eitherWhere each one goes wrong
| language | mechanism | result |
|---|---|---|
| Kotlin | the property_declaration hook returns true; the dispatcher only runs scanFnRefSubtree |
initializer, delegate and accessor bodies never walked |
| Java | extractField mints the field node and stops |
variable_declarator's value never walked; an anonymous class in a field initializer is never extracted at all |
| TS/JS | extractVariable's walk runs with only the FILE on nodeStack; object literals are excluded from it outright |
calls attributed to the file; non-exported object literals contribute nothing |
| Scala | the val/var hook returns true |
value field never walked |
| Rust | const_item/static_item ride extractVariable's generic fallback |
value field never walked |
| Python | the assignment branch mints the node and stops |
right field never walked |
How much it costs, measured
Each number is one extractor arm (wasm) diffed against upstream main over a real tree, counting refs that appear/disappear/change owner:
| language | corpus | lost | re-attributed | new |
|---|---|---|---|---|
| Kotlin | 113-file Android app (MSDK callbacks) | 0 | 0 | +368 |
| Java | 409-file Android SDK sample tree | 0 | 0 | +651 (+21 nodes: anonymous listener/Parcelable.Creator classes) |
| TS/JS | 499 files across three projects | 0 | 780 (file → declaring constant) | +228 |
| Scala | 32-file SpinalHDL project | 0 | 0 | +812 |
| Rust | 282 files across three projects | 0 | 0 | +86 |
| Python | 799 files across three projects | 0 | 0 | +3057 |
Node and edge counts are unchanged everywhere except Java (+21 nodes, the anonymous classes that were previously invisible).
Two smaller defects found while measuring, not part of this family
- Rust phantom node.
extractVariable's generic fallback mints a node for every directidentifierchild, soconst MAX: u32 = OTHER;producesMAXand a spuriousOTHER. - C# is wider than a field initializer. Beyond
variable_declarator(whose initializer is a bare child, with novaluefield),extractPropertywalks neither the accessor bodies, nor=> expr, nor{ get; } = …. For decompiled/reverse-engineered C#, where logic often lives in property getters, that is the larger share of the recall loss.
Happy to split either of those out into its own issue.
Fix
PR incoming — Kotlin, Java, TS/JS, Scala, Rust and Python, both extractor arms (TS + the Rust kernel), kernel parity preserved per language. C# deliberately left out; it needs its own shape and its own validation.
Activity
colbymchenry commentedon Sep 8, 2026
Fixed on Linux by landing upstream #1511 (@danusha2345) onto current main as Forge PR #1802 (
9664bb7a).Root cause: declaration hooks minted the symbol then stopped (or walked with only the file on the stack), so calls in Kotlin/Java/TS/JS/Scala/Rust/Python initializers vanished or were attributed to the file — #693/#744 covered Go only.
Linux fail to pass (Node 22.19.0, rebuilt dist + native kernel):
load→ file; Python/Kotlin/Scala/Rust initializer calls empty; Java missing fieldLambda/eager/anon.Closing as completed. #1511 superseded by #1802.
fix(extraction): land upstream declaration initializer walks (#1511) (#…