From 58ef8ac3aa803d7b64dd12a5b2a2e8ec24261060 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 20:47:27 -0700 Subject: [PATCH] fix(javascript): analyze source maps with empty source names A source map whose `sources` contains `""` (a legal relative reference, emitted for virtual or generated modules) made the whole application analysis fail: the empty name became the `source-map-original` identity's `original_source`, which must be nonempty. Display it as `""` at the same accumulator seam; the observation's `properties.source` keeps the exact empty name. Co-Authored-By: Claude Opus 5.5 --- ...JavaScriptApplicationEmptyLiterals.test.ts | 34 +++++++++++++++++- .../JavaScriptArtifactGraphAccumulator.ts | 36 +++++++++++++------ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/application/JavaScriptApplicationEmptyLiterals.test.ts b/src/application/JavaScriptApplicationEmptyLiterals.test.ts index dfcd06b76..887da4e54 100644 --- a/src/application/JavaScriptApplicationEmptyLiterals.test.ts +++ b/src/application/JavaScriptApplicationEmptyLiterals.test.ts @@ -7,9 +7,14 @@ import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js"; import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; -const analyzeSource = async (source: string) => { +const analyzeSource = async ( + source: string, + extraFiles: Readonly> = {}, +) => { const inputPath = await createTestTempDirectory("rea-js-empty-literal-"); await writeFile(join(inputPath, "app.js"), `${source}\n`); + for (const [path, text] of Object.entries(extraFiles)) + await writeFile(join(inputPath, path), text); const result = await analyzeJavaScriptApplication({ input_path: inputPath, format: "directory", @@ -71,3 +76,30 @@ it("displays empty literals as nonempty text while retaining exact values", asyn })), ).toEqual([{ label: '""', eventName: "" }]); }); + +it.each([ + { version: 3, sources: [""], names: [], mappings: "AAAA" }, + { + version: 3, + sections: [ + { + offset: { line: 0, column: 0 }, + map: { version: 3, sources: [""], names: [], mappings: "AAAA" }, + }, + ], + }, +])("projects an empty source-map source name: %j", async (map) => { + const output = await analyzeSource("//# sourceMappingURL=app.js.map", { + "app.js.map": JSON.stringify(map), + }); + const originals = output.graph.nodes + .filter(({ kind }) => kind === "source-module") + .map(({ identity, observations }) => ({ + originalSource: + identity.strategy === "source-map-original" + ? identity.original_source + : null, + sources: observations.map(({ properties }) => properties.source), + })); + expect(originals).toEqual([{ originalSource: '""', sources: [""] }]); +}); diff --git a/src/application/JavaScriptArtifactGraphAccumulator.ts b/src/application/JavaScriptArtifactGraphAccumulator.ts index 5d52403d0..85a917877 100644 --- a/src/application/JavaScriptArtifactGraphAccumulator.ts +++ b/src/application/JavaScriptArtifactGraphAccumulator.ts @@ -68,25 +68,25 @@ const observationInput = ( /** Node fields whose display form is normalized before validation. */ interface DisplayableNodeInput { - readonly identity: { readonly strategy: string; readonly key?: string }; + readonly identity: { + readonly strategy: string; + readonly key?: string; + readonly original_source?: string; + }; readonly observations: readonly { readonly label: string | null }[]; } /** - * Static findings can carry legal empty strings, such as `fetch("")` or - * `require("")`. Labels and artifact-local keys must be nonempty, so display - * the empty value as `""`; each observation keeps the exact value in its - * properties. + * Static findings can carry legal empty strings, such as `fetch("")`, + * `require("")`, or a source map's `sources: [""]`. Labels, artifact-local + * keys, and original source names must be nonempty, so display the empty value + * as `""`; each observation keeps the exact value in its properties. */ const displayableNode = ( input: DisplayableNodeInput, ): DisplayableNodeInput => ({ ...input, - identity: - input.identity.strategy === "artifact-local-key" && - input.identity.key !== undefined - ? { ...input.identity, key: javascriptDisplayText(input.identity.key) } - : input.identity, + identity: displayableIdentity(input.identity), observations: input.observations.map((observation) => ({ ...observation, label: @@ -95,3 +95,19 @@ const displayableNode = ( : javascriptDisplayText(observation.label), })), }); + +const displayableIdentity = ( + identity: DisplayableNodeInput["identity"], +): DisplayableNodeInput["identity"] => { + if (identity.strategy === "artifact-local-key" && identity.key !== undefined) + return { ...identity, key: javascriptDisplayText(identity.key) }; + if ( + identity.strategy === "source-map-original" && + identity.original_source !== undefined + ) + return { + ...identity, + original_source: javascriptDisplayText(identity.original_source), + }; + return identity; +};