diff --git a/CHANGELOG.md b/CHANGELOG.md index 87850b3b9..4e2fecdc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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("/me")` or `await api.get(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()` or `render()`, 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) diff --git a/__tests__/fixtures/kernel-parity/torture.js b/__tests__/fixtures/kernel-parity/torture.js index 3b47ae529..71b8b3fc1 100644 --- a/__tests__/fixtures/kernel-parity/torture.js +++ b/__tests__/fixtures/kernel-parity/torture.js @@ -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; } diff --git a/__tests__/fixtures/kernel-parity/torture.tsx b/__tests__/fixtures/kernel-parity/torture.tsx index 55346e770..cc01414fa 100644 --- a/__tests__/fixtures/kernel-parity/torture.tsx +++ b/__tests__/fixtures/kernel-parity/torture.tsx @@ -280,3 +280,18 @@ export function NestedHandlers({ items, onPick }: { items: string[]; onPick: (a: const [a, b] = [() => 1, () => 2]; return items.map((i) => ); } + +// --- 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 = 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 {size}; }); +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; } diff --git a/__tests__/param-default-calls.test.ts b/__tests__/param-default-calls.test.ts new file mode 100644 index 000000000..4fe922100 --- /dev/null +++ b/__tests__/param-default-calls.test.ts @@ -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 = 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 {size}; }); +export const Card = memo(({ note = buildNote() }: any) => {note}); +export const Box = styled.div.attrs((p = buildNote()) => ({ p }))\` + color: red; +\`; +export const Nested = memo(forwardRef(({ title = buildNote() }: any, ref: any) =>

{title}

)); +export function Page({ title = fallback() }: { title?: string }) { return

{title}

; } +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 = {}; + + 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 = { + '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); +}); diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index abaaa24a1..61b343825 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -45,7 +45,9 @@ impl<'t> Walker<'t> { if name == "" { // 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>) { + 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 = ` / `module.exports.NAME = ` — 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(); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index b440947d4..fb9ae9ef0 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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); }