diff --git a/src/domain/javascript/javascriptSemanticReturns.ts b/src/domain/javascript/javascriptSemanticReturns.ts index bb931fba51..44cbbea5bb 100644 --- a/src/domain/javascript/javascriptSemanticReturns.ts +++ b/src/domain/javascript/javascriptSemanticReturns.ts @@ -131,15 +131,17 @@ const directReturnExpressions = (callable: t.Node): ReturnExpression[] => { return [{ node: callable.body, location: range(callable.body) }]; if (!t.isFunction(callable)) return []; const output: ReturnExpression[] = []; - const visit = (node: t.Node): void => { - if (t.isFunction(node) || t.isClass(node)) return; + const pending = childNodes(callable.body).reverse(); + while (pending.length > 0) { + const node = pending.pop(); + if (node === undefined || t.isFunction(node) || t.isClass(node)) continue; if (t.isReturnStatement(node)) { output.push({ node: node.argument ?? null, location: range(node) }); - return; + continue; } - for (const child of childNodes(node)) visit(child); - }; - for (const child of childNodes(callable.body)) visit(child); + // Reverse children so the explicit stack retains source-tree order. + for (const child of childNodes(node).reverse()) pending.push(child); + } return output; }; diff --git a/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts b/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts new file mode 100644 index 0000000000..32a2d99af7 --- /dev/null +++ b/tests/boundary/filesystem/javascriptApplicationDeepReturns.test.ts @@ -0,0 +1,44 @@ +import { execFile } from "node:child_process"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; + +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("analyzes direct returns after deep source without exhausting the default process stack", async () => { + const root = await createTestTempDirectory("rea-deep-direct-returns-"); + await writeFile( + join(root, "app.js"), + `export function render() { object${".property".repeat(20_000)}; function nested() { return "nested"; } class Inner { method() { return "method"; } } return "ready"; }`, + ); + const service = new URL( + "../../../dist/application/javascript/JavaScriptApplicationService.js", + import.meta.url, + ).href; + const script = ` + import { analyzeJavaScriptApplication } from ${JSON.stringify(service)}; + const result = await analyzeJavaScriptApplication({ input_path: ${JSON.stringify(root)}, format: "directory" }); + if (!result.ok) throw result.error; + const nodes = result.value.normalized_result.semantic_graph.nodes; + console.log(JSON.stringify(nodes.filter(node => node.kind === "return-site").map(node => node.function_node_id))); + `; + const { stdout, stderr } = await promisify(execFile)( + process.execPath, + ["--input-type=module", "-e", script], + { + timeout: 10_000, + // Bound the child heap while retaining Node's default stack size. + env: { + HOME: root, + TMPDIR: root, + NODE_OPTIONS: "--max-old-space-size=768", + }, + }, + ); + expect(stderr).toBe(""); + const owners: unknown = JSON.parse(stdout); + expect(owners).toHaveLength(3); + expect(new Set(Array.isArray(owners) ? owners : []).size).toBe(3); +});