fix(extraction): a function in an initializer is referenced once, by what the initializer declares (#2743)

PRs #693 and #1802 walk a declaration's initializer under the symbol it declares,
so `const routes = { home }` references `home` from `routes`. The
dispatcher's declaration-wide function-as-value scan then took the same
values again for the enclosing file (or class). Flush-time dedup keys on
the source, so both stayed: a second reference to the same function from
the same spot.

Each initializer walk now records what it walked, and the scan skips it.
PR #2738 added that mechanism for component wrappers, and C#, Scala and Dart
already had their own. The newly covered paths:
- TS/JS module-level constants and variables, owned object literals
  included (#2300), and TS/JS class properties;
- Python assignments, Go var/const specs, Rust const/static items;
- Java fields, and Kotlin properties (walkInitializer takes the property's
  id, so their walk is unchanged).
Mirrored in the kernel: tsjs, python, go, rustlang, java and kotlin.

References no initializer walk takes are unchanged: a module-level call
argument `register(fn)`, C-family tables, Rust's `static CB: fn() = handler`
(the declaration's own value, which the scan still reads).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 11:41:45 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent ae0fc631e0
commit 51be28d15b
17 changed files with 233 additions and 79 deletions
+16 -5
View File
@@ -428,8 +428,10 @@ impl<'t> Walker<'t> {
} else if matches!(kind, "var_declaration" | "short_var_declaration" | "const_declaration")
&& !self.inside_class_like()
{
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
// The initializers extract_variable walked under their specs'
// symbols took their own candidates; the scan covers the rest.
let walked = self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "import_declaration" {
self.extract_import(node);
@@ -678,7 +680,10 @@ impl<'t> Walker<'t> {
}
/// extractVariable's Go branch: var/const specs + short_var_declaration.
fn extract_variable(&mut self, node: Node<'t>) {
/// Returns the initializers it walked under their specs' symbols, for the
/// declaration scan to skip.
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
let mut walked = Vec::new();
let docstring = self.docstring_of(node);
let is_const_decl = node.kind() == "const_declaration";
@@ -726,6 +731,7 @@ impl<'t> Walker<'t> {
self.stack.push(Scope { row, kind: "variable", name });
self.visit_function_body(value_field);
self.stack.pop();
walked.push(value_field.id());
} else {
self.visit_function_body(value_field);
}
@@ -756,6 +762,7 @@ impl<'t> Walker<'t> {
}
}
}
walked
}
/// extractImport's Go branch: one import node + ref per import_spec.
@@ -1241,11 +1248,15 @@ impl<'t> Walker<'t> {
}
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
if depth > 0
&& matches!(
node.kind(),
@@ -1258,7 +1269,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
+18 -7
View File
@@ -507,12 +507,14 @@ impl<'t> Walker<'t> {
self.extract_enum(node);
skip_children = true;
} else if kind == "field_declaration" && self.inside_class_like() {
self.extract_field(node);
self.scan_fn_ref_subtree(node, 0);
// The initializers extract_field walked under their fields took
// their own candidates; the scan covers the rest.
let walked = self.extract_field(node);
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "local_variable_declaration" && !self.inside_class_like() {
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
self.scan_fn_ref_subtree(node, 0, &[]);
skip_children = true;
} else if kind == "import_declaration" {
self.extract_import(node);
@@ -726,8 +728,11 @@ impl<'t> Walker<'t> {
self.stack.pop();
}
/// extractField — each declarator becomes a field/constant node.
fn extract_field(&mut self, node: Node<'t>) {
/// extractField — each declarator becomes a field/constant node. Returns
/// the initializers walked under their fields, for the declaration scan to
/// skip.
fn extract_field(&mut self, node: Node<'t>) -> Vec<usize> {
let mut walked = Vec::new();
let docstring = preceding_docstring(node, self.src);
let visibility = self.visibility_of(node);
let is_static = Some(self.is_static(node));
@@ -786,6 +791,7 @@ impl<'t> Walker<'t> {
self.stack.push(Scope { row, kind: field_kind, name: name.clone() });
self.visit_function_body(value);
self.stack.pop();
walked.push(value.id());
}
}
}
@@ -805,6 +811,7 @@ impl<'t> Walker<'t> {
);
}
}
walked
}
/// extractVariable's generic fallback (top-level locals — rare in Java).
@@ -1230,11 +1237,15 @@ impl<'t> Walker<'t> {
}
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
// (functionTypes is empty for Java; lambda_expression halts the scan)
if depth > 0 && matches!(node.kind(), "lambda_literal" | "lambda_expression") {
return;
@@ -1242,7 +1253,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
+24 -13
View File
@@ -733,7 +733,10 @@ impl<'t> Walker<'t> {
})
}
fn try_visit_hook(&mut self, node: Node<'t>) -> bool {
/// The Kotlin visitNode hook: `Some` when it consumed the node, holding the
/// initializers it walked as such (`walkInitializer`), which the caller's
/// fn-ref scan of the node skips.
fn try_visit_hook(&mut self, node: Node<'t>) -> Option<Vec<usize>> {
// 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
@@ -741,10 +744,10 @@ impl<'t> Walker<'t> {
if matches!(node.kind(), "getter" | "setter") {
return accessor_owner(node)
.and_then(|owner| self.property_kind(owner))
.is_some();
.map(|_| Vec::new());
}
if node.kind() != "property_declaration" {
return false;
return None;
}
let var_decl = (0..node.named_child_count())
.filter_map(|i| node.named_child(i))
@@ -764,11 +767,11 @@ impl<'t> Walker<'t> {
for init in property_initializers(node) {
self.visit_function_body(init);
}
return true;
return Some(Vec::new());
};
let name = self.text(name_node).to_string();
if name.is_empty() {
return false;
return None;
}
let Some(kind) = self.property_kind(node) else {
// A local — no node is minted, but the initializer is still code.
@@ -778,7 +781,7 @@ impl<'t> Walker<'t> {
for init in property_initializers(node) {
self.visit_function_body(init);
}
return true;
return Some(Vec::new());
};
// The `type`-field signature read is dead (zero fields) → signature
// undefined; NO docstring/visibility/isStatic — the modifiers merge in
@@ -790,26 +793,30 @@ impl<'t> Walker<'t> {
// 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.
// used to attribute to the enclosing class. The initializers are
// walked as such: the fn-ref scan skips them, or it would take each
// function value again for the enclosing file or class.
let mut walked = Vec::new();
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);
walked.push(init.id());
}
for acc in following_accessors(node) {
self.visit_function_body(acc);
}
self.stack.pop();
}
true
Some(walked)
}
// --- the dispatcher (visitNode, Kotlin-relevant branches) -----------------------
fn visit_node(&mut self, node: Node<'t>) {
stack_guard!();
if self.try_visit_hook(node) {
self.scan_fn_ref_subtree(node, 0);
if let Some(walked) = self.try_visit_hook(node) {
self.scan_fn_ref_subtree(node, 0, &walked);
return;
}
@@ -856,7 +863,7 @@ impl<'t> Walker<'t> {
// Hook-declined destructuring: extractField/extractVariable both
// find no matching children for kotlin — NOTHING minted, RHS
// invisible; candidates-only scan.
self.scan_fn_ref_subtree(node, 0);
self.scan_fn_ref_subtree(node, 0, &[]);
skip_children = true;
} else if kind == "import_header" {
self.extract_import(node);
@@ -1574,11 +1581,15 @@ impl<'t> Walker<'t> {
});
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
// Halts at functionTypes (function_declaration) + the fixed list —
// lambda_literal halts (no captures inside `by lazy { }` under a
// hook-consumed property).
@@ -1594,7 +1605,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
+16 -5
View File
@@ -431,8 +431,10 @@ impl<'t> Walker<'t> {
self.extract_class(node);
skip_children = true;
} else if kind == "assignment" && !self.inside_class_like() {
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
// The right-hand side extract_variable walked under the assigned
// name took its own candidates; the scan covers the rest.
let walked = self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "import_statement" || kind == "import_from_statement" {
self.extract_import(node);
@@ -564,7 +566,9 @@ impl<'t> Walker<'t> {
}
/// extractVariable's python branch: `left = right` at module scope.
fn extract_variable(&mut self, node: Node<'t>) {
/// Returns the right-hand side when it was walked under the assigned
/// name, for the declaration scan to skip.
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
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));
@@ -589,16 +593,19 @@ impl<'t> Walker<'t> {
// 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.
let mut walked = Vec::new();
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();
walked.push(right.id());
}
None => self.visit_function_body(right),
}
}
walked
}
fn extract_import(&mut self, node: Node<'t>) {
@@ -955,11 +962,15 @@ impl<'t> Walker<'t> {
}
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
// Halts at functionTypes ∪ the fixed arrow/lambda list — python's
// `lambda` kind is NOT in that list (mirrored).
if depth > 0
@@ -974,7 +985,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
+17 -7
View File
@@ -500,8 +500,10 @@ impl<'t> Walker<'t> {
// Inside a class-like scope the gate fails and the else-ladder
// falls through with children VISITED — a trait const's value
// expression emits calls refs from the trait node.
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
// The initializer extract_variable walked under the declared
// symbol took its own candidates; the scan covers the rest.
let walked = self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0, &walked);
skip_children = true;
} else if kind == "use_declaration" {
self.extract_import(node);
@@ -721,9 +723,10 @@ impl<'t> Walker<'t> {
/// extractVariable's generic fallback: kind is ALWAYS `variable` (no
/// isConst hook), every direct `identifier` child mints a node positioned
/// at the CHILD, docstring shared, isExported present-false, no signature,
/// and the initializer value is never body-walked.
fn extract_variable(&mut self, node: Node<'t>) {
/// at the CHILD, docstring shared, isExported present-false, no signature.
/// The initializer is walked under the declared symbol (#693), and returned
/// then for the declaration scan to skip.
fn extract_variable(&mut self, node: Node<'t>) -> Vec<usize> {
let docstring = preceding_docstring(node, self.src);
let name_field = node.child_by_field_name("name");
// An impl's associated const is its type's (`Limits::MAX`), as its methods are.
@@ -759,16 +762,19 @@ impl<'t> Walker<'t> {
// `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.
let mut walked = Vec::new();
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();
walked.push(value.id());
}
None => self.visit_function_body(value),
}
}
walked
}
/// extractImport via the rust hook: import node named by the ROOT module +
@@ -1430,11 +1436,15 @@ impl<'t> Walker<'t> {
}
}
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32, walked: &[usize]) {
stack_guard!();
if depth > 12 {
return;
}
// Subtrees the body walker has already been through.
if walked.contains(&node.id()) {
return;
}
if depth > 0
&& matches!(
node.kind(),
@@ -1447,7 +1457,7 @@ impl<'t> Walker<'t> {
self.maybe_capture_fn_refs(node);
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.scan_fn_ref_subtree(c, depth + 1);
self.scan_fn_ref_subtree(c, depth + 1, walked);
}
}
}
+7 -3
View File
@@ -530,9 +530,11 @@ impl<'t> Walker<'t> {
}
}
/// Returns the initializers it walked under a component wrapper, whose
/// fn-ref candidates that walk already took (the caller's scan of the
/// declaration skips them). Mirrors TreeSitterExtractor.extractVariable.
/// Returns the initializers it walked under the symbol they declare — a
/// constant's or a variable's (#693), a component wrapper's — whose fn-ref
/// candidates that walk already took for the symbol (the caller's scan of
/// the declaration skips them; it would take each again for the file).
/// Mirrors TreeSitterExtractor.extractVariable.
pub(super) fn extract_variable(&mut self, node: Node<'t>) -> Option<HashSet<usize>> {
let mut walked: Option<HashSet<usize>> = None;
let is_const = self.is_const_decl(node);
@@ -619,6 +621,7 @@ impl<'t> Walker<'t> {
// the ownedObject branch of TreeSitterExtractor.extractVariable.
if let (Some(row), Some(obj)) = (var_row, self.owned_object_value(value)) {
self.extract_owned_object_members(obj, row, kind, &name, true, false);
walked.get_or_insert_with(HashSet::new).insert(obj.id());
continue;
}
@@ -678,6 +681,7 @@ impl<'t> Walker<'t> {
self.stack.push(Scope { row, kind, name: name.clone() });
self.visit_function_body(v);
self.stack.pop();
walked.get_or_insert_with(HashSet::new).insert(v.id());
}
None => self.visit_function_body(v),
}
+8 -3
View File
@@ -700,12 +700,16 @@ impl<'t> Walker<'t> {
{
if classify_ts_class_member(node) == Member::Property {
let prop = self.extract_property(node);
// The initializer walked under the property took its own
// candidates; the scan covers the rest of the field.
let mut walked: Option<HashSet<usize>> = None;
if let (Some((row, name)), Some(value)) = (prop, node.child_by_field_name("value")) {
self.stack.push(Scope { row, kind: "property", name });
self.visit_function_body(value);
self.stack.pop();
walked = Some(HashSet::from([value.id()]));
}
self.scan_fn_ref_subtree(node, 0, None);
self.scan_fn_ref_subtree(node, 0, walked.as_ref());
} else {
self.extract_method(node);
}
@@ -719,8 +723,9 @@ impl<'t> Walker<'t> {
} else if self.variant.is_ts() && kind == "type_alias_declaration" {
skip_children = self.extract_type_alias(node);
} else if is_variable_type(kind) && !self.inside_class_like() {
// A component wrapper's initializer extract_variable walked under
// the component captured its own candidates; the scan skips it.
// An initializer extract_variable walked under the symbol it
// declares (a constant's, a component wrapper's) captured its own
// candidates; the scan skips it.
let walked = self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0, walked.as_ref());
skip_children = true;