mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
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:
co-authored by
Claude Opus 5.5
parent
5d81d292fa
commit
d82a4adc9d
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user