fix(ts/js): scope a variable's initializer walk to the variable (#693 for TS/JS)

Two defects in one place — extractVariable's JS-family branch.

1. The initializer walk ran with only the FILE on the node stack, so
   `const cfg = loadConfig()` recorded the FILE as loadConfig's caller. That is
   literally the leak #693 described and #744 fixed for Go; the closing note
   there said "JS/TS didn't have this gap", which holds only for the
   `const f = () => …` shape (an arrow value delegates to extractFunction and
   gets its own node).

2. Object literals were excluded from the walk outright, on the grounds that
   their function-valued members are extracted individually below — but that
   only happens for EXPORTED consts. `const obj = { handler: () => target() }`
   therefore contributed nothing at all: no member node, and no call edge to
   anything.

Fix: walk the initializer with the declared symbol pushed on the stack, and
skip only the shapes whose members really are extracted one-by-one (exported
object-of-functions, RTK endpoints, Pinia setup, Vue store collections) —
walking those too would double-count each member arrow's calls.

Two follow-on repairs the change surfaced:

- CFML `<cfscript>` bodies are delegated to a separate extractor as if they
  were a whole module, so a `var x = helper()` local inside a `<cffunction>`
  minted a top-level variable node and the walk attributed `helper` to it.
  Those snippet-top-level non-callables are locals of the enclosing function;
  their refs now redirect to it, like the snippet's file-attributed refs
  already did.
- codegraph_explore stopped listing a synthesized dynamic-dispatch link once
  the same pair also had a static call edge: it asked getCallers/getCallees,
  which return one row per NEIGHBOUR (the #1086 de-dup), so the static edge
  hid the heuristic one. It now asks the edges directly. This is exactly the
  RTK thunk case — `dispatch(innerThunk(n))` inside a thunk initializer is now
  a real static edge, which is the point, but the synthesized hop must still
  show up in the summary.

Measured on three independent TypeScript trees (codegraph's own src/, evcc-ng,
gv-grx — 499 files): 0 lost nodes/edges/refs, 780 refs re-attributed from the
file node to the declaring constant, 228 genuinely new refs from object
literals, node and edge counts unchanged. Kernel parity holds (324/324
non-deferred files byte-identical).
This commit is contained in:
danusha2345
2026-08-05 22:44:33 +03:00
parent 468f623745
commit a10a7fa6f7
7 changed files with 113 additions and 27 deletions
+19 -11
View File
@@ -398,18 +398,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),
}
}
}