mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-19 23:20:31 +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:
@@ -9,10 +9,10 @@
|
||||
//! is source-order dependent) and extractModifiers (expect/actual platform
|
||||
//! modifiers → the node DECORATORS wire field, on every created node — the
|
||||
//! KMP synthesizer's input). Preserved on purpose: the FIELD_COUNT-0 dead
|
||||
//! cluster (no signatures, ZERO type-annotation refs), hook-consumed property
|
||||
//! initializers emitting nothing, the bodiless-class header re-walk asymmetry,
|
||||
//! enum-entry bodies being invisible, KDoc (`multiline_comment`) never being
|
||||
//! a docstring AND chain-breaking, comment-gluing into import/package extents,
|
||||
//! cluster (no signatures, ZERO type-annotation refs), the bodiless-class
|
||||
//! header re-walk asymmetry, enum-entry bodies being invisible, KDoc
|
||||
//! (`multiline_comment`) never being a docstring AND chain-breaking,
|
||||
//! comment-gluing into import/package extents,
|
||||
//! `@Anno(args)` emitting nothing while `@Anno` emits decorates, zero
|
||||
//! instantiates refs (constructors are capitalized `calls`), the qualified-
|
||||
//! receiver `com::qext` bug, the paren-then-lambda `trailing()` garbage
|
||||
@@ -87,6 +87,34 @@ fn strip_js_ws(s: &str) -> String {
|
||||
s.chars().filter(|c| !is_js_space(*c)).collect()
|
||||
}
|
||||
|
||||
/// 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; this grammar exposes no fields
|
||||
/// at all, hence the `=` anchor.)
|
||||
fn property_initializers<'t>(node: Node<'t>) -> Vec<Node<'t>> {
|
||||
let mut out: Vec<Node<'t>> = Vec::new();
|
||||
let mut after_eq = false;
|
||||
for i in 0..node.child_count() {
|
||||
let Some(c) = node.child(i) else { continue };
|
||||
if !c.is_named() {
|
||||
if c.kind() == "=" {
|
||||
after_eq = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if after_eq {
|
||||
out.push(c);
|
||||
after_eq = false;
|
||||
} else if c.kind() == "property_delegate" {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
struct Scope {
|
||||
row: u32,
|
||||
kind: &'static str,
|
||||
@@ -641,7 +669,18 @@ impl<'t> Walker<'t> {
|
||||
// The `type`-field signature read is dead (zero fields) → signature
|
||||
// undefined; NO docstring/visibility/isStatic — the modifiers merge in
|
||||
// create_node still decorates expect/actual properties.
|
||||
self.create_node(kind, &name, node, Extra::default());
|
||||
let row = self.create_node(kind, &name, node, Extra::default());
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol (#693, the Go
|
||||
// fix, ported): without this the subtree is only fn-ref-scanned, so a
|
||||
// lambda / SAM / object initializer (`val cb = Runnable { target() }` —
|
||||
// the idiomatic Android callback field) contributed NO call edge at all.
|
||||
if let Some(row) = row {
|
||||
self.stack.push(Scope { row, kind, name: name.clone() });
|
||||
for init in property_initializers(node) {
|
||||
self.visit_function_body(init);
|
||||
}
|
||||
self.stack.pop();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user