fix(kotlin): close the remaining property-hook holes — init blocks, destructuring, own-line accessors

Three shapes the hook still swallowed after cf582f4/8b2e436. The first two are
outright losses: the code existed in the file and reached the graph nowhere.

    init { val cfg = load() }        // `load` vanished — only the block's
                                     // bare statements survived
    val (a, b) = makePair()          // `makePair` vanished, at class AND
                                     // file scope
    val x: Int                       // walked, but attributed to the
        get() = compute()            // enclosing CLASS, not to `x`

The first two mint no symbol of their own, so they are walked at the ENCLOSING
scope — an `init` block's calls belong to the class, exactly like the block's
other statements. The third is an accessor the grammar makes a following
SIBLING of the property rather than a child (same-line accessors nest, which
8b2e436 already covered); the property now claims its own-line accessors, and
the accessor branch skips what the property claimed. The skip re-derives the
property's kind instead of remembering it across nodes, so a destructured or
local declaration — which mints nothing and therefore claims nothing — keeps
falling through as before. No cross-node state in either arm.

The scope/kind decision moved into one function (`kotlinPropertyKind` /
`property_kind`) now that two branches need it.

Measured on the 113-file Android/Kotlin project, against upstream c65d56c:
0 lost refs, 0 re-attributed, +368 new. Kernel parity unchanged (99/113
byte-identical, same 14 grammar-deferred files as upstream).
This commit is contained in:
danusha2345
2026-08-05 23:24:00 +03:00
parent a86703bc79
commit 8c35774d63
5 changed files with 256 additions and 58 deletions
+101 -19
View File
@@ -117,6 +117,36 @@ fn property_initializers<'t>(node: Node<'t>) -> Vec<Node<'t>> {
out
}
/// Accessors written on their OWN line parse as SIBLINGS of the property, not
/// as children of it (same-line ones nest — see property_initializers). Walking
/// back over any accessors between us and the declaration finds the property an
/// accessor belongs to; None when this accessor stands alone.
fn accessor_owner<'t>(node: Node<'t>) -> Option<Node<'t>> {
let mut p = node.prev_named_sibling();
while let Some(n) = p {
if matches!(n.kind(), "getter" | "setter") {
p = n.prev_named_sibling();
continue;
}
return if n.kind() == "property_declaration" { Some(n) } else { None };
}
None
}
/// The sibling accessors that follow a property declaration, in source order.
fn following_accessors<'t>(node: Node<'t>) -> Vec<Node<'t>> {
let mut out = Vec::new();
let mut n = node.next_named_sibling();
while let Some(c) = n {
if !matches!(c.kind(), "getter" | "setter") {
break;
}
out.push(c);
n = c.next_named_sibling();
}
out
}
struct Scope {
row: u32,
kind: &'static str,
@@ -613,25 +643,23 @@ impl<'t> Walker<'t> {
// --- the visitNode hook (property branch ONLY — fun-interface recovery is
// defer-shielded and not ported) ------------------------------------------------
fn try_visit_hook(&mut self, node: Node<'t>) -> bool {
if node.kind() != "property_declaration" {
return false;
}
/// A property's node kind, or None when the declaration mints no node at
/// all: destructuring, an unreadable name, or a local (inside a function
/// body / `init` block / lambda / accessor). Kind by enclosing scope — a
/// singleton `object` / `companion object` (and a top-level property) holds
/// SHARED values (`val`→constant, `var`→variable, the Scala-object rule; a
/// `const val` is just a val); a class/interface/enum instance `val`/`var`
/// is per-instance state → `field`.
fn property_kind(&self, node: Node<'t>) -> Option<&'static str> {
let var_decl = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "variable_declaration");
let name_node = var_decl.and_then(|vd| {
(0..vd.named_child_count())
.filter_map(|i| vd.named_child(i))
.find(|c| c.kind() == "simple_identifier")
});
let Some(name_node) = name_node else { return false }; // destructuring → decline
let name = self.text(name_node).to_string();
if name.is_empty() {
return false;
.find(|c| c.kind() == "variable_declaration")?;
let name_node = (0..var_decl.named_child_count())
.filter_map(|i| var_decl.named_child(i))
.find(|c| c.kind() == "simple_identifier")?;
if self.text(name_node).is_empty() {
return None;
}
// Scope walk up the parent chain — first match wins.
let mut scope: &str = "const";
let mut p = node.parent();
while let Some(pn) = p {
@@ -654,19 +682,67 @@ impl<'t> Walker<'t> {
p = pn.parent();
}
if scope == "local" {
return true; // a local — extract nothing, subtree still scanned
return None;
}
let binding = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "binding_pattern_kind");
let is_val = binding.map(|b| self.text(b) == "val").unwrap_or(false);
let kind: &'static str = if scope == "instance" {
Some(if scope == "instance" {
"field"
} else if is_val {
"constant"
} else {
"variable"
})
}
fn try_visit_hook(&mut self, node: Node<'t>) -> bool {
// An own-line accessor already walked by its owning property below. The
// ownership test re-derives the property's kind rather than remembering
// it: a destructured or local declaration mints no node, so its
// accessors were NOT consumed and must keep falling through.
if matches!(node.kind(), "getter" | "setter") {
return accessor_owner(node)
.and_then(|owner| self.property_kind(owner))
.is_some();
}
if node.kind() != "property_declaration" {
return false;
}
let var_decl = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.find(|c| c.kind() == "variable_declaration");
let name_node = var_decl.and_then(|vd| {
(0..vd.named_child_count())
.filter_map(|i| vd.named_child(i))
.find(|c| c.kind() == "simple_identifier")
});
// Destructuring (`val (a, b) = makePair()`): NEITHER arm mints a symbol
// for the destructured names — declining just routes the node to
// extractField/extractVariable, which both find nothing for kotlin and
// end in the same fn-ref scan. But the RHS is CODE, and it was vanishing
// whole. Consume the node here and walk it at the ENCLOSING scope (no
// symbol of its own to attribute to).
let Some(name_node) = name_node else {
for init in property_initializers(node) {
self.visit_function_body(init);
}
return true;
};
let name = self.text(name_node).to_string();
if name.is_empty() {
return false;
}
let Some(kind) = self.property_kind(node) else {
// A local — no node is minted, but the initializer is still code.
// Walk it at the ENCLOSING scope: an `init { }` block's
// `val q = load()` is the CLASS calling load, and it used to
// disappear entirely (only the block's bare statements survived).
for init in property_initializers(node) {
self.visit_function_body(init);
}
return true;
};
// The `type`-field signature read is dead (zero fields) → signature
// undefined; NO docstring/visibility/isStatic — the modifiers merge in
@@ -676,11 +752,17 @@ impl<'t> Walker<'t> {
// 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.
// The property also OWNS any accessor written on its own line, which the
// grammar makes a following SIBLING rather than a child; those bodies
// used to attribute to the enclosing class.
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);
}
for acc in following_accessors(node) {
self.visit_function_body(acc);
}
self.stack.pop();
}
true