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
+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;