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
+63
View File
@@ -2200,6 +2200,7 @@ export class TreeSitterExtractor {
// descends into this subtree. (#528)
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
this.visitParameterDefaults(node, body);
if (body) {
this.visitFunctionBody(body, '');
}
@@ -2248,12 +2249,70 @@ export class TreeSitterExtractor {
this.nodeStack.push(funcNode.id);
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
this.visitParameterDefaults(node, body);
if (body) {
this.visitFunctionBody(body, funcNode.id);
}
this.nodeStack.pop();
}
/**
* A parameter's default value is code the function runs, each time a call
* leaves that argument out (#2588): `function render(note = buildNote())`
* calls buildNote as surely as `const note = buildNote()` in its body would.
* Each default, and a computed key in a destructured parameter, is walked as
* the body is, with the same scope on the stack, so its calls and `new`
* attribute to the function. The names a parameter binds and its type
* annotation are not walked. TS/JS only: elsewhere a default may run once,
* when the definition does (Python). A function with no body never runs.
* Mirrored in the kernel (tsjs/extractors.rs visit_parameter_defaults).
*/
private visitParameterDefaults(fn: SyntaxNode, body: SyntaxNode | null | undefined): void {
if (!body || !TS_JS_CHAIN_LANGUAGES.has(this.language)) return;
// A class field holds its function: `onPick = (item = first()) => {…}`.
const owner = fn.type === 'public_field_definition' || fn.type === 'field_definition' ? body.parent : fn;
const params = owner ? getChildByField(owner, 'parameters') : null;
if (params) this.visitPatternDefaults(params);
}
/** The defaults in a parameter list or a destructuring pattern, in source order. */
private visitPatternDefaults(node: SyntaxNode): void {
switch (node.type) {
case 'formal_parameters':
case 'object_pattern':
case 'array_pattern':
case 'rest_pattern':
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) this.visitPatternDefaults(child);
}
return;
case 'required_parameter':
case 'optional_parameter': {
const pattern = getChildByField(node, 'pattern');
if (pattern) this.visitPatternDefaults(pattern);
const value = getChildByField(node, 'value');
if (value) this.visitFunctionBody(value, '');
return;
}
case 'assignment_pattern':
case 'object_assignment_pattern': {
const left = getChildByField(node, 'left');
if (left) this.visitPatternDefaults(left);
const right = getChildByField(node, 'right');
if (right) this.visitFunctionBody(right, '');
return;
}
case 'pair_pattern': {
const key = getChildByField(node, 'key');
if (key?.type === 'computed_property_name') this.visitFunctionBody(key, '');
const value = getChildByField(node, 'value');
if (value) this.visitPatternDefaults(value);
return;
}
}
}
/**
* Detect a React component declared via an HOC wrapper whose result is itself a
* component: `forwardRef(...)`, `memo(...)`, `React.forwardRef/memo(...)`, and
@@ -2355,6 +2414,7 @@ export class TreeSitterExtractor {
if (innerFn) {
const body = this.extractor.resolveBody?.(innerFn, this.extractor.bodyField)
?? getChildByField(innerFn, this.extractor.bodyField);
this.visitParameterDefaults(innerFn, body);
if (body) this.visitFunctionBody(body, compNode.id);
} else {
const value = getChildByField(declarator, 'value');
@@ -2422,6 +2482,7 @@ export class TreeSitterExtractor {
// `styled(Box)(({ theme }) => …)` for a function of its own).
const body = this.extractor!.resolveBody?.(arg, this.extractor!.bodyField)
?? getChildByField(arg, this.extractor!.bodyField);
this.visitParameterDefaults(arg, body);
if (body) this.visitFunctionBody(body, '');
} else {
this.visitFunctionBody(arg, '');
@@ -2556,6 +2617,7 @@ export class TreeSitterExtractor {
if (node.parent?.type === 'object' || node.parent?.type === 'object_expression') {
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
this.visitParameterDefaults(node, body);
if (body) {
this.visitFunctionBody(body, '');
}
@@ -2631,6 +2693,7 @@ export class TreeSitterExtractor {
this.nodeStack.push(methodNode.id);
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
this.visitParameterDefaults(node, body);
if (body) {
this.visitFunctionBody(body, methodNode.id);
}