Squash danusha2345's PR #1511 at d282f9e8 onto main 8c9c4761,
preserving its nine non-merge commits and main's existing Unreleased notes.
Calls in Kotlin, Java, TS/JS, Scala, Rust and Python declaration initializers
now retain the owner established by the upstream regression expectations.
Include the upstream CFML, dynamic-dispatch summary and viewer follow-ups.
Linux fail-to-pass validation (Node 22.19.0, rebuilt dist and native kernel):
- Before: TS load belonged to file:app.ts; Python/Kotlin/Scala/Rust calls
vanished; Java lost the field-lambda, anonymous override and eager calls.
- After: all six languages PASS; 12 native/WASM LF/CRLF parity checks PASS.
- Focused initializer regressions: 10 passed with CODEGRAPH_KERNEL=0 and
10 passed with the kernel enabled; Kotlin's grammar fallback is recorded.
- Related regression suites: 879 passed, 1 skipped across 15 test files.
- Evidence: /workspace/cg-1510-repro/before and /workspace/cg-1510-repro/after
(combined test output: after/vitest.log).
Fixes #1510
Supersedes #1511
Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
This commit is contained in:
co-authored by
Colby McHenry
danusha2345
parent
8c9c4761b0
commit
9181dd1ef3
@@ -759,6 +759,16 @@ impl<'t> Walker<'t> {
|
||||
if let Some(row) = row {
|
||||
self.extract_decorators_for(node, row);
|
||||
self.extract_type_annotations(node, row);
|
||||
// Walk the initializer ATTRIBUTED to the declared field
|
||||
// (#693, the Go fix): the dispatcher only fn-ref-scans this
|
||||
// subtree, so a lambda / method reference / anonymous class
|
||||
// in `private final Runnable r = () -> target();` emitted no
|
||||
// call edge at all.
|
||||
if let Some(value) = decl.child_by_field_name("value") {
|
||||
self.stack.push(Scope { row, kind: field_kind, name: name.clone() });
|
||||
self.visit_function_body(value);
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+147
-24
@@ -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,66 @@ fn strip_js_ws(s: &str) -> String {
|
||||
s.chars().filter(|c| !is_js_space(*c)).collect()
|
||||
}
|
||||
|
||||
/// A property's CODE children: the named child right after the `=` token, a
|
||||
/// `property_delegate` (`by lazy { … }`), and an accessor the grammar nested
|
||||
/// under the declaration (`val x: Int get() = compute()` — written on ONE line;
|
||||
/// an accessor on its own line parses as a SIBLING of the property and is not
|
||||
/// reachable from here). What stays unwalked is the declaration itself —
|
||||
/// modifiers, the `val`/`var` keyword, the name+type, and an extension
|
||||
/// receiver's type and type parameters. (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 matches!(c.kind(), "property_delegate" | "getter" | "setter") {
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -583,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 {
|
||||
@@ -624,24 +682,89 @@ 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
|
||||
// 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.
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -475,14 +475,37 @@ impl<'t> Walker<'t> {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let left = node.child_by_field_name("left").or_else(|| node.named_child(0));
|
||||
let right = node.child_by_field_name("right").or_else(|| node.named_child(1));
|
||||
let Some(left) = left else { return };
|
||||
if !matches!(left.kind(), "identifier" | "constant") {
|
||||
return;
|
||||
let mut assigned: Option<(u32, String)> = None;
|
||||
if let Some(left) = left {
|
||||
if matches!(left.kind(), "identifier" | "constant") {
|
||||
let name = self.text(left).to_string();
|
||||
let signature = right.map(|r| util::init_signature(self.text(r)));
|
||||
// No isConst hook ⇒ always `variable` (UPPER_CASE constants included).
|
||||
let row = self.create_node(
|
||||
"variable",
|
||||
&name,
|
||||
node,
|
||||
Extra { docstring, signature, ..Extra::default() },
|
||||
);
|
||||
if let Some(row) = row {
|
||||
assigned = Some((row, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walk the initializer ATTRIBUTED to the assigned name (#693): a
|
||||
// module-level `app = FastAPI()` / `handler = lambda: run()` dropped
|
||||
// every call on the right-hand side. A tuple target mints no symbol, so
|
||||
// its RHS is walked at the enclosing scope rather than lost.
|
||||
if let Some(right) = right {
|
||||
match assigned {
|
||||
Some((row, name)) => {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(right);
|
||||
self.stack.pop();
|
||||
}
|
||||
None => self.visit_function_body(right),
|
||||
}
|
||||
}
|
||||
let name = self.text(left).to_string();
|
||||
let signature = right.map(|r| util::init_signature(self.text(r)));
|
||||
// No isConst hook ⇒ always `variable` (UPPER_CASE constants included).
|
||||
self.create_node("variable", &name, node, Extra { docstring, signature, ..Extra::default() });
|
||||
}
|
||||
|
||||
fn extract_import(&mut self, node: Node<'t>) {
|
||||
|
||||
@@ -667,6 +667,8 @@ impl<'t> Walker<'t> {
|
||||
/// and the initializer value is never body-walked.
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let name_field = node.child_by_field_name("name");
|
||||
let mut declared: Option<(u32, String)> = None;
|
||||
for i in 0..node.named_child_count() {
|
||||
let Some(child) = node.named_child(i) else { continue };
|
||||
if child.kind() != "identifier" {
|
||||
@@ -674,7 +676,7 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
let name = self.text(child).to_string();
|
||||
if !name.is_empty() {
|
||||
self.create_node(
|
||||
let row = self.create_node(
|
||||
"variable",
|
||||
&name,
|
||||
child,
|
||||
@@ -684,6 +686,26 @@ impl<'t> Walker<'t> {
|
||||
..Extra::default()
|
||||
},
|
||||
);
|
||||
if let (Some(row), Some(nf)) = (row, name_field) {
|
||||
if child.start_byte() == nf.start_byte() {
|
||||
declared = Some((row, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol (#693):
|
||||
// `const N: usize = compute()` and
|
||||
// `static REGISTRY: Lazy<T> = Lazy::new(|| build())` dropped every call
|
||||
// inside the initializer, so a handler table or a lazily-built
|
||||
// singleton linked to nothing.
|
||||
if let Some(value) = node.child_by_field_name("value") {
|
||||
match declared {
|
||||
Some((row, name)) => {
|
||||
self.stack.push(Scope { row, kind: "variable", name });
|
||||
self.visit_function_body(value);
|
||||
self.stack.pop();
|
||||
}
|
||||
None => self.visit_function_body(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,6 +656,18 @@ impl<'t> Walker<'t> {
|
||||
if let (Some(row), Some(t)) = (created, type_node) {
|
||||
self.emit_scala_type_refs(t, row);
|
||||
}
|
||||
// Walk the initializer ATTRIBUTED to the declared symbol
|
||||
// (#693, the Go fix): the hook consumes this subtree and the
|
||||
// dispatcher only fn-ref-scans it, so `val cb = () => target()`
|
||||
// — and even a plain `val x = compute()` — emitted no call edge
|
||||
// at all.
|
||||
if let Some(row) = created {
|
||||
if let Some(value) = node.child_by_field_name("value") {
|
||||
self.stack.push(Scope { row, kind, name: name.clone() });
|
||||
self.visit_body(value);
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
"enum_case_definitions" => {
|
||||
|
||||
@@ -483,18 +483,26 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the initializer for calls — except the object/store shapes
|
||||
// whose members are extracted method-by-method below.
|
||||
// Walk the initializer for calls, ATTRIBUTED to the declared symbol
|
||||
// (#693) — except the object/store shapes whose members are
|
||||
// extracted method-by-method below (walking those too would
|
||||
// double-count each member arrow's calls). Before this the walk ran
|
||||
// with only the FILE on the stack (`const cfg = load()` recorded the
|
||||
// file as load's caller) and object literals were skipped outright.
|
||||
let members_extracted_separately = extract_object_methods
|
||||
|| rtk_endpoints.is_some()
|
||||
|| pinia_setup.is_some()
|
||||
|| !store_collections.is_empty();
|
||||
if let Some(v) = value {
|
||||
let vk = v.kind();
|
||||
if vk != "object"
|
||||
&& vk != "object_expression"
|
||||
&& !(extract_object_methods && vk == "call_expression")
|
||||
&& rtk_endpoints.is_none()
|
||||
&& pinia_setup.is_none()
|
||||
&& store_collections.is_empty()
|
||||
{
|
||||
self.visit_function_body(v);
|
||||
if !members_extracted_separately {
|
||||
match var_row {
|
||||
Some(row) => {
|
||||
self.stack.push(Scope { row, kind, name: name.clone() });
|
||||
self.visit_function_body(v);
|
||||
self.stack.pop();
|
||||
}
|
||||
None => self.visit_function_body(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user