From 5d046b9ad7e962b3fd4ad3d99ff62aa4c2663552 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Fri, 13 Nov 2026 06:16:36 -0800 Subject: [PATCH] fix(javascript): retain wide function fingerprint inventories (#10355) --- .../JavaScriptSemanticGraphBuilder.ts | 6 +- .../semanticFingerprintAccumulation.test.ts | 98 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 tests/boundary/javascript/semanticFingerprintAccumulation.test.ts diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index b6bafed798..37880b30c6 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -250,7 +250,8 @@ export const createJavaScriptSemanticGraphProjection = ( unknownCeiling, unknownsBefore + fileUnknownCeiling, ); - fingerprints.push(...(yield* projectFileSteps(file, ir, state, paths))); + for (const fingerprint of yield* projectFileSteps(file, ir, state, paths)) + fingerprints.push(fingerprint); // Nodes were dropped only if the budget actually blocked creation. A file // that exactly fills its share ends with a zero budget without dropping // anything, so the remaining budget alone cannot decide truncation. @@ -427,7 +428,8 @@ export const createJavaScriptSemanticGraphProjection = ( for (const unknown of newUnknowns) state.unknowns.set(unknown.unknown_id, unknown); for (const root of projection.roots) state.roots.add(root); - fingerprints.push(...projection.fingerprints); + for (const fingerprint of projection.fingerprints) + fingerprints.push(fingerprint); if (projection.nodesTruncated) { truncatedFiles += 1; noteSemanticCeilingReached( diff --git a/tests/boundary/javascript/semanticFingerprintAccumulation.test.ts b/tests/boundary/javascript/semanticFingerprintAccumulation.test.ts new file mode 100644 index 0000000000..7c642f10ef --- /dev/null +++ b/tests/boundary/javascript/semanticFingerprintAccumulation.test.ts @@ -0,0 +1,98 @@ +import { createHash } from "node:crypto"; +import { expect, it } from "vitest"; +import { + createJavaScriptSemanticGraphProjection, + projectJavaScriptSemanticFileSteps, +} from "../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; +import { analyzeJavaScriptSemantics } from "../../../src/domain/javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js"; +import { resolveJavaScriptSemanticEvidence } from "../../support/javascriptSemanticGraphFixture.js"; + +it.each(["direct", "transferred"] as const)( + "retains an admitted source's wide fingerprint list through %s projection", + (mode) => { + // This forked project uses the same main-thread stack as the CLI parent. + // These real callable facts exceed V8's call-argument capacity, but fit + // the caller-selectable one-million-node graph ceiling. + const count = 130_000; + const source = "(()=>{});\n".repeat(count); + const sha256 = createHash("sha256").update(source).digest("hex"); + const file: JavaScriptArtifactFile = { + path: "entry.mjs", + container_sha256: sha256, + sha256, + bytes: Buffer.byteLength(source), + inventory_artifact_id: `art_${sha256}`, + kind: "javascript", + unpacked: false, + text: { included: true, value: source }, + }; + const ir = analyzeJavaScriptSemantics(source); + expect(ir.callables).toHaveLength(count); + expect(ir.functionFingerprints).toHaveLength(count); + const projection = createJavaScriptSemanticGraphProjection({ + totalNodes: count + 1, + fileNodes: count + 1, + }); + if (mode === "direct") projection.projectFile(file, ir); + else { + const steps = projectJavaScriptSemanticFileSteps( + file, + ir, + projection.remainingFileCapacity(), + ); + let step = steps.next(); + while (!step.done) step = steps.next(); + projection.adoptFileProjection(step.value); + } + const graph = projection.finish(sha256, { + graph_id: `jag_${sha256}`, + nodes: [], + }); + const functions = graph.nodes.filter(({ kind }) => kind === "function"); + expect(functions).toHaveLength(count); + expect(graph.fingerprints).toHaveLength(count); + // The graph sorts nodes and fingerprints by their independent digests. + const functionIds = new Set(functions.map(({ node_id }) => node_id)); + expect( + graph.fingerprints.every(({ function_node_id }) => + functionIds.has(function_node_id), + ), + ).toBe(true); + expect( + new Set( + graph.fingerprints.map(({ function_node_id }) => function_node_id), + ).size, + ).toBe(count); + expect( + new Set(graph.fingerprints.map(({ fingerprint_id }) => fingerprint_id)) + .size, + ).toBe(count); + expect(graph.coverage.truncated).toBe(false); + for (const line of [1, count]) { + const callable = functions.find( + ({ identity }) => identity.source_range?.start.line === line, + ); + const fingerprint = graph.fingerprints.find( + ({ function_node_id }) => function_node_id === callable?.node_id, + ); + if (fingerprint === undefined) throw new Error("Missing fingerprint"); + expect( + resolveJavaScriptSemanticEvidence(graph, fingerprint.evidence), + ).toMatchObject({ + state: "observed", + authority: "ast-static-analysis", + artifact: { available: true, sha256 }, + location: { + available: true, + value: { + kind: "source-range", + source: "entry.mjs", + start: { line }, + }, + }, + }); + } + }, + 30_000, +);