fix(js,ts): a call or new in a parameter's default value is the function's call (#2802)

A default runs inside the function each time a call leaves the argument
out, but both extractors pushed the function and walked only its body,
so `function renderWithNote(note = buildNote())` recorded nothing for
the call: no edge and no unresolved reference. Arrows, methods,
constructor parameter properties, class-field arrows, object-literal
methods and destructured parameters lost theirs the same way. Only an
anonymous arrow inside a body was walked whole, params included.

The TS extractor (visitParameterDefaults) and the kernel's TS/JS walker
(visit_parameter_defaults) now hand each default value, and a computed
key in a destructured parameter, to the body walker, with the scope the
body gets on the stack: the function for a named function, method or
component, the enclosing scope for an anonymous function or an
object-literal method. It runs before the body walk at every site that
walks only a body, including a class field's arrow, a forwardRef/memo
render function and a styled .attrs() function. Patterns are recursed
through; the names a parameter binds, its type annotation and a bodiless
signature are not walked. TS/JS only: a Python default runs once, when
the def does, and is left as it was.

Validation: the new test fails 13 of 14 on main 5d81d292 and passes on
both engines, LF and CRLF; the kernel parity fixtures gained a defaults
section. A/B against main on 14 TS/JS repos: 118 edges added, none
removed or retargeted, each a call or `new` written in a default (nuxt
45, mostly kit utilities' `nuxt: Nuxt = useNuxt()`, webdriverio 30,
memfs 17, jest 7, vben 5, social-app 5, vite 4, outline 3, nitro 2;
insomnia, t3-turbo, vue3-antdv-admin, pathe and c12 none). Seven ride
resolution the body already gets: six Vue/Nuxt auto-import self-links
and one member call on memfs's `export default createProcess()`. The
flask and gin control dumps are byte-identical. kernel-parity.mjs over
the 14 repos: 15,408 files byte-parity, 0 with diffs. Index time,
median of 8 interleaved runs on vite, nuxt, webdriverio and insomnia:
+1.6% total, within noise (the last 5 rounds alone: -1.7%). A CLI sync
that adds defaults, adds a file and gives an unchanged file's default
its callee matches a fresh index.

Fixes #2588

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 14:52:16 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5d81d292fa
commit d82a4adc9d
6 changed files with 436 additions and 6 deletions
+77 -6
View File
@@ -45,7 +45,9 @@ impl<'t> Walker<'t> {
if name == "<anonymous>" {
// Still walk the body: module wrappers hold named inner functions
// and calls that would otherwise be lost (#528).
if let Some(body) = body_of(node) {
let body = body_of(node);
self.visit_parameter_defaults(node, body);
if let Some(body) = body {
self.visit_function_body(body);
}
return;
@@ -68,12 +70,73 @@ impl<'t> Walker<'t> {
self.extract_decorators_for(node, row);
self.stack.push(Scope { row, kind: "function", name });
if let Some(body) = body_of(node) {
let body = body_of(node);
self.visit_parameter_defaults(node, body);
if let Some(body) = body {
self.visit_function_body(body);
}
self.stack.pop();
}
/// A parameter's default value is code the function runs, each time a
/// call leaves that argument out (#2588). Each default, and a computed key
/// in a destructured parameter, is walked as the body is, with the same
/// scope on the stack; the names a parameter binds and its type annotation
/// are not. A function with no body never runs. Mirrors
/// TreeSitterExtractor.visitParameterDefaults.
fn visit_parameter_defaults(&mut self, func: Node<'t>, body: Option<Node<'t>>) {
let Some(body) = body else { return };
// A class field holds its function: `onPick = (item = first()) => {…}`.
let owner = if matches!(func.kind(), "public_field_definition" | "field_definition") {
body.parent()
} else {
Some(func)
};
if let Some(params) = owner.and_then(|o| o.child_by_field_name("parameters")) {
self.visit_pattern_defaults(params);
}
}
/// The defaults in a parameter list or a destructuring pattern, in source
/// order. Mirrors TreeSitterExtractor.visitPatternDefaults.
fn visit_pattern_defaults(&mut self, node: Node<'t>) {
stack_guard!();
match node.kind() {
"formal_parameters" | "object_pattern" | "array_pattern" | "rest_pattern" => {
for i in 0..node.named_child_count() {
if let Some(child) = node.named_child(i) {
self.visit_pattern_defaults(child);
}
}
}
"required_parameter" | "optional_parameter" => {
if let Some(pattern) = node.child_by_field_name("pattern") {
self.visit_pattern_defaults(pattern);
}
if let Some(value) = node.child_by_field_name("value") {
self.visit_function_body(value);
}
}
"assignment_pattern" | "object_assignment_pattern" => {
if let Some(left) = node.child_by_field_name("left") {
self.visit_pattern_defaults(left);
}
if let Some(right) = node.child_by_field_name("right") {
self.visit_function_body(right);
}
}
"pair_pattern" => {
if let Some(key) = node.child_by_field_name("key").filter(|k| k.kind() == "computed_property_name") {
self.visit_function_body(key);
}
if let Some(value) = node.child_by_field_name("value") {
self.visit_pattern_defaults(value);
}
}
_ => {}
}
}
/// The property a CommonJS export assignment binds a function to —
/// `exports.NAME = <node>` / `module.exports.NAME = <node>` — or None for
/// any other assignment. The node must be the assignment's whole
@@ -174,7 +237,9 @@ impl<'t> Walker<'t> {
self.stack.push(Scope { row, kind: "component", name: name.to_string() });
match inner_fn {
Some(inner) => {
if let Some(body) = body_of(inner) {
let body = body_of(inner);
self.visit_parameter_defaults(inner, body);
if let Some(body) = body {
self.visit_function_body(body);
}
}
@@ -246,7 +311,9 @@ impl<'t> Walker<'t> {
} else if matches!(arg.kind(), "arrow_function" | "function_expression") {
// A style or `.attrs()` function runs as the component's
// code: its body is walked, the function never named.
if let Some(body) = body_of(arg) {
let body = body_of(arg);
self.visit_parameter_defaults(arg, body);
if let Some(body) = body {
self.visit_function_body(body);
}
} else {
@@ -331,7 +398,9 @@ impl<'t> Walker<'t> {
// Object-literal methods are ephemeral: walk the body only.
if let Some(parent) = node.parent() {
if matches!(parent.kind(), "object" | "object_expression") {
if let Some(body) = body_of(node) {
let body = body_of(node);
self.visit_parameter_defaults(node, body);
if let Some(body) = body {
self.visit_function_body(body);
}
return;
@@ -358,7 +427,9 @@ impl<'t> Walker<'t> {
self.extract_decorators_for(node, row);
self.stack.push(Scope { row, kind: "method", name });
if let Some(body) = body_of(node) {
let body = body_of(node);
self.visit_parameter_defaults(node, body);
if let Some(body) = body {
self.visit_function_body(body);
}
self.stack.pop();