mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-20 16:05:00 +08:00
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:
+101
-19
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user