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 <noreply@anthropic.com>
This commit is contained in:
Iwakurakaoru0822 2026-10-06 20:47:27 -07:00
parent 11fc3b9815
commit 58ef8ac3aa
2 changed files with 59 additions and 11 deletions

View File

@ -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<Record<string, string>> = {},
) => {
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: [""] }]);
});

View File

@ -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;
};