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
+1
View File
@@ -15,6 +15,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In TypeScript, an awaited call with type arguments like `await fetchApi<User>("/me")` or `await api.get<T>(url)` now links to the function it calls. Before, the call was dropped, so typed fetch wrappers looked like they had no callers. (#2586)
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
- In Python, `from app import email` no longer links the importing file to a project variable named `app`, such as a FastAPI or Flask `app = FastAPI()`. (#2584)
@@ -125,3 +125,14 @@ function calleeShapes(handlers, type, fn, ns) {
(fn)();
(ns.fn)();
}
// --- parameter defaults are the function's code (#2588) -----------------------
function defaultsOf(note = legacyHelper(1), { a = arrow(2), b: { c = new Widget() } = {} } = {}, [x = legacyHelper(3), , y = arrow(4)] = [], { [legacyHelper(5)]: keyed } = {}) {
return [note, a, c, x, y, keyed];
}
class DefaultsHolder {
constructor(o = new Widget()) { this.o = o; }
pick(item = legacyHelper(6)) { return item; }
onPick = (item = arrow(7)) => item;
}
const defaultsApi = { load(note = legacyHelper(8)) { return note; } };
export default function (note = arrow(9)) { return note; }
@@ -280,3 +280,18 @@ export function NestedHandlers({ items, onPick }: { items: string[]; onPick: (a:
const [a, b] = [() => 1, () => 2];
return items.map((i) => <button onClick={handleClear} onDoubleClick={() => describe(i)}>{later(i)}{count}{a()}{b()}</button>);
}
// --- parameter defaults are the function's code (#2588) ----------------------
export function defaultsOf(note = formatLabel('x'), { a = parseLabel('a'), b: { c = new FieldDelegator({}, []) } = {} as any }: any = {}, [first = formatLabel('f')]: string[] = [], { [parseLabel('k')]: keyed }: any = {}) { return [note, a, c, first, keyed]; }
export const defaultArrow = (cb = (e: unknown) => formatLabel(String(e)), typed: ReturnType<typeof parseLabel> = parseLabel('t')) => cb(typed);
export class DefaultsHolder {
constructor(private readonly delegate = new FieldDelegator({ send: (m: string) => m }, [])) {}
pick(item = formatLabel('i')) { return item; }
onPick = (item = parseLabel('p')) => item;
wrappedPick = withBatchedUpdates((item = formatLabel('w')) => item);
}
export const DefaultsApi = { load(note = formatLabel('l')) { return note; }, save: (note = parseLabel('s')) => note };
export const DefaultsRef = forwardRef(function DefaultsRef({ size = parseLabel('z') }: any, ref: any) { return <b ref={ref}>{size}</b>; });
export const DefaultsBox = styled.div.attrs((p = formatLabel('b')) => ({ p }))`color: red;`;
export function overloadedDefault(x: string): string;
export function overloadedDefault(x: string = formatLabel('o')): string { return x; }
+269
View File
@@ -0,0 +1,269 @@
/**
* A parameter's default value is code its function runs, each time a call
* leaves that argument out: `function render(note = buildNote())` calls
* buildNote as surely as `const note = buildNote()` in its body would. The
* extractors walked only the body, so a call or `new` in a default was never
* seen (#2588). Each default, and a computed key in a destructured parameter,
* is now walked with the function on the stack — for functions, arrows,
* methods, constructors, class-field arrows, object-literal methods and the
* render functions of React wrappers — in the kernel and the wasm extractor
* alike. The names a parameter binds and its type are not code, and a Python
* default, which runs once when the `def` does, is left as it was.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult, Language } from '../src/types';
const KERNEL_PATH = path.join(
__dirname,
'..',
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
);
const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';
const TS_SOURCE = `import { buildNote, fallback, make, pickKey, logError, Options, Service, throttle } from './lib';
export function plain(note = buildNote()) { return note; }
export const arrow = (note = buildNote()) => note;
export function destr({ n } = buildNote() as any) { return n; }
export function nested({ a = fallback(), b: { c = make() } = {} as any }: any) { return a + c; }
export function computed({ [pickKey()]: v }: any) { return v; }
export function rest(...[first = fallback()]: number[]) { return first; }
export function optional(x?: number, y: number = fallback()) { return (x ?? 0) + y; }
export function typed(x: ReturnType<typeof shape> = buildNote()) { return x; }
export function withNew(opts = new Options()) { return opts; }
export function handler(onError = (e: unknown) => logError(e)) { onError(1); }
export async function* stream(note = buildNote()) { yield note; }
export class K {
constructor(private readonly svc = new Service()) {}
m(note = buildNote()) { return note; }
static s(note = buildNote()) { return note; }
field = (note = buildNote()) => note;
wrapped = throttle((note = fallback()) => note);
}
export const api = { load(note = buildNote()) { return note; }, save: (note = make()) => note };
export function outer() {
function inner(note = buildNote()) { return note; }
return inner();
}
export function over(x: number): number;
export function over(x: number = fallback()): number { return x; }
export function inBody() { return buildNote(); }
`;
const TS_EXPECTED = [
'api::load calls buildNote',
'api::save calls make',
'arrow calls buildNote',
'computed calls pickKey',
'K::constructor instantiates Service',
'destr calls buildNote',
'K::field calls buildNote',
'handler calls logError',
'handler calls onError',
'inBody calls buildNote',
'outer::inner calls buildNote',
'K::m calls buildNote',
'K::s calls buildNote',
'nested calls fallback',
'nested calls make',
'optional calls fallback',
'outer calls inner',
'over calls fallback',
'plain calls buildNote',
'rest calls fallback',
'stream calls buildNote',
// The type annotation's `typeof shape` is a type, not a call.
'typed calls buildNote',
'withNew instantiates Options',
'K::wrapped calls fallback',
];
const JS_SOURCE = `import { buildNote, fallback, pickKey, Options } from './lib';
export function plain(note = buildNote()) { return note; }
export function nested({ a = fallback(), b: { c = buildNote() } = {} }) { return a + c; }
export function arr([x = fallback(), , y = buildNote()] = []) { return x + y; }
export function computed({ [pickKey()]: v }) { return v; }
export const arrow = (note = buildNote()) => note;
export class J {
constructor(o = new Options()) { this.o = o; }
m(note = buildNote()) { return note; }
field = (note = fallback()) => note;
}
export default function (note = buildNote()) { return note; }
export function inBody() { return buildNote(); }
`;
const JS_EXPECTED = [
'J::m calls buildNote',
'arr calls buildNote',
'arr calls fallback',
'arrow calls buildNote',
'computed calls pickKey',
'J::constructor instantiates Options',
'J::field calls fallback',
'inBody calls buildNote',
'nested calls buildNote',
'nested calls fallback',
'plain calls buildNote',
// An anonymous function's code is its enclosing scope's: here, the file's.
'src/plain.js calls buildNote',
];
const TSX_SOURCE = `import { forwardRef, memo } from 'react';
import styled from 'styled-components';
import { buildNote, fallback } from './lib';
export const Button = forwardRef(function Button({ size = fallback() }: any, ref: any) { return <b ref={ref}>{size}</b>; });
export const Card = memo(({ note = buildNote() }: any) => <i>{note}</i>);
export const Box = styled.div.attrs((p = buildNote()) => ({ p }))\`
color: red;
\`;
export const Nested = memo(forwardRef(({ title = buildNote() }: any, ref: any) => <p ref={ref}>{title}</p>));
export function Page({ title = fallback() }: { title?: string }) { return <h1>{title}</h1>; }
setTimeout((n = fallback()) => n, 0);
`;
const TSX_EXPECTED = [
'Box calls buildNote',
'Button calls fallback',
'Card calls buildNote',
'Nested calls buildNote',
'Page calls fallback',
// An anonymous function's code is its enclosing scope's: here, the file's.
'src/ui.tsx calls fallback',
'src/ui.tsx calls setTimeout',
];
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
describe('a call in a parameter default is the function’s call (#2588)', () => {
let savedEnv: Record<string, string | undefined> = {};
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['typescript', 'tsx', 'javascript', 'jsx']);
});
beforeEach(() => {
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
resetKernelForTests();
});
afterEach(() => {
for (const k of ENV_KEYS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
resetKernelForTests();
});
function extract(backend: 'kernel' | 'wasm', file: string, source: string, language: Language): ExtractionResult {
if (backend === 'wasm') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource(file, source, language);
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract(file, source, language);
expect(result, `kernel extraction of ${file}`).not.toBeNull();
return result!;
}
/** `caller kind callee` for every call and `new` the file records, by its caller's qualified name. */
function callsOf(result: ExtractionResult): string[] {
const names = new Map(result.nodes.map((n) => [n.id, n.kind === 'file' ? n.filePath : n.qualifiedName]));
return result.unresolvedReferences
.filter((r) => r.referenceKind === 'calls' || r.referenceKind === 'instantiates')
.map((r) => `${names.get(r.fromNodeId)} ${r.referenceKind} ${r.referenceName}`)
.sort();
}
const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const);
for (const crlf of [false, true]) {
const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s);
const label = crlf ? ' (CRLF)' : '';
it.each(backends)(`in TypeScript functions, arrows, methods, fields and object literals: %s${label}`, (backend) => {
expect(callsOf(extract(backend, 'src/use.ts', eol(TS_SOURCE), 'typescript'))).toEqual([...TS_EXPECTED].sort());
});
it.each(backends)(`in JavaScript, through assignment and destructuring patterns: %s${label}`, (backend) => {
expect(callsOf(extract(backend, 'src/plain.js', eol(JS_SOURCE), 'javascript'))).toEqual([...JS_EXPECTED].sort());
expect(callsOf(extract(backend, 'src/plain.jsx', eol(JS_SOURCE), 'jsx'))).toEqual(
JS_EXPECTED.map((s) => s.replace('src/plain.js ', 'src/plain.jsx ')).sort()
);
});
it.each(backends)(`in a React wrapper's render function and a styled attrs function: %s${label}`, (backend) => {
expect(callsOf(extract(backend, 'src/ui.tsx', eol(TSX_SOURCE), 'tsx'))).toEqual([...TSX_EXPECTED].sort());
});
}
it.runIf(kernelAvailable)('the kernel and the wasm extractor agree on every node, edge and reference', () => {
const canon = (r: ExtractionResult) => ({
nodes: r.nodes.map(({ updatedAt: _u, ...n }) => JSON.stringify(n, Object.keys(n).sort())).sort(),
edges: r.edges.map((e) => JSON.stringify(e, Object.keys(e).sort())).sort(),
refs: r.unresolvedReferences.map((x) => JSON.stringify(x, Object.keys(x).sort())).sort(),
});
for (const [file, source, language] of [
['src/use.ts', TS_SOURCE, 'typescript'],
['src/plain.js', JS_SOURCE, 'javascript'],
['src/ui.tsx', TSX_SOURCE, 'tsx'],
] as const) {
expect(canon(extract('kernel', file, source, language)), file).toEqual(canon(extract('wasm', file, source, language)));
}
});
});
describe('the default value links to what it calls', () => {
let root = '';
let graph: CodeGraph | null = null;
afterEach(() => {
graph?.close();
graph = null;
if (root) fs.rmSync(root, { recursive: true, force: true });
root = '';
});
it('gives buildNote its callers from defaults, and leaves a Python default alone', async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-param-default-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'app' }),
// The issue's reproduction, verbatim.
'src/f.ts':
'export function buildNote() { return { id: 1 }; }\n' +
'export function renderWithNote(note = buildNote()) {\n return note.id;\n}\n' +
'export const arrow = (note = buildNote()) => note.id;\n' +
'export class K { m(note = buildNote()) { return note.id; } }\n' +
'export function destr({ n } = buildNote() as any) { return n; }\n' +
'export function inBody() { const note = buildNote(); return note.id; }\n',
// Python evaluates a default once, when the `def` runs: not the function's call.
'tool/ctl.py': 'def build():\n return 1\n\n\ndef uses_default(x=build()):\n return x\n\n\ndef in_body():\n return build()\n',
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
graph = await CodeGraph.init(root, { index: true });
const callers = (file: string, name: string): string[] => {
const target = graph!.getNodesInFile(file).find((n) => n.name === name)!;
return graph!.getIncomingEdges(target.id)
.filter((e) => e.kind === 'calls')
.map((e) => graph!.getNode(e.source)!.name)
.sort();
};
expect(callers('src/f.ts', 'buildNote')).toEqual(['arrow', 'destr', 'inBody', 'm', 'renderWithNote']);
expect(callers('tool/ctl.py', 'build')).toEqual(['in_body']);
}, 120_000);
});
+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();
+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);
}