mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 16:57:09 +08:00
fix(kotlin): walk a property's initializer scoped to the property (#693 for Kotlin)
The Kotlin property hook consumes the whole `property_declaration` subtree,
so the dispatcher only scanned it for function-as-value candidates and never
walked it for calls. Every call inside a property initializer was therefore
dropped from the graph entirely — not misattributed, gone:
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
That is exactly how Android/MSDK callbacks are declared, so anything reached
only through such a callback looked like it had no callers and its blast
radius came back far too small.
Fix: after minting the property node, walk its RHS — the named child after
the `=` token plus a `property_delegate` — through visitFunctionBody with the
property pushed on the scope stack. This is Go's #693/#744 fix ported;
tree-sitter-kotlin exposes no fields at all, hence the `=` anchor instead of
Go's `child_by_field_name("value")`.
Not touched, by design: the `scope == "local"` early return, the
hook-declined destructuring branch, and the declaration's own children
(modifiers, `val`/`var`, name+type, extension receiver, `getter`/`setter`).
Both arms move together — the Rust kernel and the TS extractor — so
kernel-kotlin-parity stays byte-identical; the torture fixture gains the
lambda/SAM/anonymous-object initializer shapes.
Measured on a 113-file Android/Kotlin app: strictly additive, 0 lost nodes /
edges / refs, +7 nodes (anonymous-object overrides that were invisible),
+109 edges (+47 calls, +35 instantiates, +20 references), and the real case
that motivated this — a `CameraFrameListener` field — now shows up as a
caller of the method it invokes.
This commit is contained in:
@@ -42,6 +42,35 @@ function extractKotlinReturnType(node: SyntaxNode, source: string): string | und
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A property's RHS: the named child right after the `=` token, plus a
|
||||
* `property_delegate` (`by lazy { … }`). Everything else a
|
||||
* `property_declaration` can hold is a declaration, not code — modifiers, the
|
||||
* `val`/`var` keyword, the name+type, an extension receiver's type and type
|
||||
* parameters, and `getter`/`setter` — so it stays unwalked. (Go's #693 fix
|
||||
* walks the `value` field for the same reason; tree-sitter-kotlin exposes no
|
||||
* fields at all, hence the `=` anchor.)
|
||||
*/
|
||||
function kotlinPropertyInitializers(node: SyntaxNode): SyntaxNode[] {
|
||||
const out: SyntaxNode[] = [];
|
||||
let afterEq = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (!c) continue;
|
||||
if (!c.isNamed) {
|
||||
if (c.type === '=') afterEq = true;
|
||||
continue;
|
||||
}
|
||||
if (afterEq) {
|
||||
out.push(c);
|
||||
afterEq = false;
|
||||
} else if (c.type === 'property_delegate') {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Check if a node matches the `fun interface` misparse pattern */
|
||||
function isFunInterfaceNode(node: SyntaxNode): boolean {
|
||||
let hasFun = false;
|
||||
@@ -126,7 +155,19 @@ export const kotlinExtractor: LanguageExtractor = {
|
||||
const sig = typeNode
|
||||
? `${isVal ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`
|
||||
: undefined;
|
||||
ctx.createNode(kind, name, node, { signature: sig });
|
||||
const created = ctx.createNode(kind, name, node, { signature: sig });
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol (#693, the Go
|
||||
// fix, ported to Kotlin): the hook consumes this subtree, so without an
|
||||
// explicit walk a lambda / SAM / object initializer
|
||||
// (`private val cb = Runnable { target() }` — the idiomatic Android
|
||||
// callback field) contributed NO call edge at all, and everything reached
|
||||
// only through such a callback looked like it had no callers.
|
||||
const inits = created ? kotlinPropertyInitializers(node) : [];
|
||||
if (created && inits.length > 0) {
|
||||
ctx.pushScope(created.id);
|
||||
for (const init of inits) ctx.visitFunctionBody(init, created.id);
|
||||
ctx.popScope();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user