mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(javascript): retain uncertainty for missing source map mappings
Share the required mappings field contract and retain readable declarations across malformed indexed leaves while marking graph coverage partial. Refs #5469.
This commit is contained in:
@@ -13,6 +13,7 @@ import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
|
||||
import {
|
||||
flattenSourceMapLeaves,
|
||||
sourceMapJsonText,
|
||||
sourceMapMappingsSchema,
|
||||
} from "../../domain/sourceMapEnvelope.js";
|
||||
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import { failedJavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
||||
@@ -603,6 +604,7 @@ const collectSourceMapOriginals = (
|
||||
maps: readonly Readonly<Record<string, unknown>>[],
|
||||
): JavaScriptSourceMapObservation => {
|
||||
const sources: JavaScriptSourceMapOriginal[] = [];
|
||||
let limitation: string | null = null;
|
||||
let declarationIndex = 0;
|
||||
for (const map of maps) {
|
||||
const names = map.sources;
|
||||
@@ -645,7 +647,10 @@ const collectSourceMapOriginals = (
|
||||
content_sha256: content === null ? null : sha256(content),
|
||||
});
|
||||
}
|
||||
if (!sourceMapMappingsSchema.safeParse(map.mappings).success)
|
||||
limitation ??= "Source map has no mappings string.";
|
||||
}
|
||||
if (limitation !== null) return invalidSourceMap(file, limitation, sources);
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
@@ -689,11 +694,12 @@ const unavailableSourceMap = (
|
||||
const invalidSourceMap = (
|
||||
file: JavaScriptArtifactFile,
|
||||
limitation: string,
|
||||
sources: readonly JavaScriptSourceMapOriginal[] = [],
|
||||
): JavaScriptSourceMapObservation => ({
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
status: "invalid",
|
||||
sources: [],
|
||||
sources,
|
||||
limitation,
|
||||
});
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export type JavaScriptSourceMapObservation = JavaScriptStructuredObservation &
|
||||
}
|
||||
| {
|
||||
readonly status: "invalid";
|
||||
readonly sources: readonly [];
|
||||
readonly sources: readonly JavaScriptSourceMapOriginal[];
|
||||
readonly limitation: string;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Artifact source declarations do not require generated-position decoding. */
|
||||
type SourceMapRecord = Readonly<Record<string, unknown>>;
|
||||
|
||||
/** ECMA-426 requires a string mapping payload on every regular map leaf. */
|
||||
export const sourceMapMappingsSchema = z.string();
|
||||
|
||||
/**
|
||||
* Return the JSON parsing view of retained source-map text. ECMA-426 permits
|
||||
* HTTP responses to start with `)]}'`; the first line is transport framing.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
||||
import { sourceMapJsonText } from "../../domain/sourceMapEnvelope.js";
|
||||
import {
|
||||
sourceMapJsonText,
|
||||
sourceMapMappingsSchema,
|
||||
} from "../../domain/sourceMapEnvelope.js";
|
||||
import type { EncodedSourceMapXInput } from "@jridgewell/trace-mapping";
|
||||
|
||||
const leafSchema = z.object({
|
||||
@@ -10,7 +13,7 @@ const leafSchema = z.object({
|
||||
sources: z.array(z.string().nullable()),
|
||||
sourcesContent: z.array(z.string().nullable()).nullable().optional(),
|
||||
names: z.array(z.string()).default([]),
|
||||
mappings: z.string().regex(/^[A-Za-z0-9+/,;]*$/u),
|
||||
mappings: sourceMapMappingsSchema.regex(/^[A-Za-z0-9+/,;]*$/u),
|
||||
ignoreList: z.array(z.number().int().min(0)).optional(),
|
||||
x_google_ignoreList: z.array(z.number().int().min(0)).optional(),
|
||||
});
|
||||
|
||||
@@ -9,6 +9,98 @@ import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"keeps missing source-map mappings partial while retaining explicit declarations through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-source-map-mappings-");
|
||||
const original = "export const answer: number = 42;";
|
||||
const cases = [
|
||||
{
|
||||
directory: "missing",
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
},
|
||||
expectedCoverage: "partial",
|
||||
limitation: "Source map has no mappings string.",
|
||||
},
|
||||
{
|
||||
directory: "empty",
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
mappings: "",
|
||||
},
|
||||
expectedCoverage: "complete",
|
||||
limitation: null,
|
||||
},
|
||||
{
|
||||
directory: "mapped",
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
},
|
||||
expectedCoverage: "complete",
|
||||
limitation: null,
|
||||
},
|
||||
] as const;
|
||||
for (const { directory, map, expectedCoverage, limitation } of cases) {
|
||||
const app = join(root, directory);
|
||||
const mapText = JSON.stringify(map);
|
||||
await writeFixtureFiles(app, { "app.js.map": mapText });
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, app);
|
||||
const mapDigest = createHash("sha256").update(mapText).digest("hex");
|
||||
const source = graph.nodes.find(
|
||||
(node) =>
|
||||
node.kind === "source-module" &&
|
||||
node.identity.strategy === "source-map-original" &&
|
||||
node.identity.source_map_sha256 === mapDigest,
|
||||
);
|
||||
|
||||
expect(graph.coverage.status).toBe(expectedCoverage);
|
||||
expect(source).toMatchObject({
|
||||
identity: {
|
||||
strategy: "source-map-original",
|
||||
original_source: "original.ts",
|
||||
source_sha256: createHash("sha256").update(original).digest("hex"),
|
||||
},
|
||||
observations: [
|
||||
expect.objectContaining({
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: mapDigest }),
|
||||
limitations: limitation === null ? [] : [limitation],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
if (limitation !== null)
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "unknown",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
operation: "parse-local-source-map",
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: mapDigest }),
|
||||
limitations: [limitation],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"recovers retained prefixed source-map originals and byte identity through %s",
|
||||
async (surface, { cli }) => {
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
|
||||
const analyzeMaps = async (maps: Readonly<Record<string, string>>) => {
|
||||
const root = await createTestTempDirectory("rea-app-source-map-mappings-");
|
||||
await Promise.all(
|
||||
Object.entries(maps).map(([path, text]) =>
|
||||
writeFile(join(root, path), text),
|
||||
),
|
||||
);
|
||||
const result = await analyzeJavaScriptApplication({
|
||||
input_path: root,
|
||||
format: "directory",
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
return analysis.graph;
|
||||
};
|
||||
|
||||
it("marks missing regular-map mappings unavailable and accepts empty or populated strings", async () => {
|
||||
const original = "export const answer: number = 42;";
|
||||
const maps = {
|
||||
"missing.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
}),
|
||||
"empty.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
mappings: "",
|
||||
}),
|
||||
"mapped.js.map": JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["original.ts"],
|
||||
sourcesContent: [original],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
};
|
||||
const graph = await analyzeMaps(maps);
|
||||
const missingDigest = createHash("sha256")
|
||||
.update(maps["missing.js.map"])
|
||||
.digest("hex");
|
||||
expect(graph.coverage.status).toBe("partial");
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "unknown",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
operation: "parse-local-source-map",
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: missingDigest }),
|
||||
limitations: ["Source map has no mappings string."],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
const originals = graph.nodes.filter(
|
||||
(node) => node.identity.strategy === "source-map-original",
|
||||
);
|
||||
expect(originals).toHaveLength(3);
|
||||
expect(originals).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "source-module",
|
||||
identity: expect.objectContaining({
|
||||
source_map_sha256: missingDigest,
|
||||
original_source: "original.ts",
|
||||
source_sha256: createHash("sha256").update(original).digest("hex"),
|
||||
}),
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: missingDigest }),
|
||||
limitations: ["Source map has no mappings string."],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
for (const mapPath of ["empty.js.map", "mapped.js.map"] as const) {
|
||||
const digest = createHash("sha256").update(maps[mapPath]).digest("hex");
|
||||
expect(originals).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "source-module",
|
||||
identity: expect.objectContaining({ source_map_sha256: digest }),
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
evidence: expect.objectContaining({ limitations: [] }),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains declarations before and after an invalid indexed-map leaf", async () => {
|
||||
const mapText = JSON.stringify({
|
||||
version: 3,
|
||||
sections: [
|
||||
{
|
||||
offset: { line: 0, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["before.ts"],
|
||||
sourcesContent: ["export const before = 1;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 1, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["middle.ts"],
|
||||
sourcesContent: ["export const middle = 2;"],
|
||||
names: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: { line: 2, column: 0 },
|
||||
map: {
|
||||
version: 3,
|
||||
sources: ["after.ts"],
|
||||
sourcesContent: ["export const after = 3;"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const graph = await analyzeMaps({ "indexed.js.map": mapText });
|
||||
const digest = createHash("sha256").update(mapText).digest("hex");
|
||||
expect(graph.coverage.status).toBe("partial");
|
||||
expect(graph.nodes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "unknown",
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
path: "indexed.js.map",
|
||||
operation: "parse-local-source-map",
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: digest }),
|
||||
limitations: ["Source map has no mappings string."],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
const originals = graph.nodes.filter(
|
||||
(node) =>
|
||||
node.identity.strategy === "source-map-original" &&
|
||||
node.identity.source_map_sha256 === digest,
|
||||
);
|
||||
expect(originals).toHaveLength(3);
|
||||
for (const [index, sourceName, content] of [
|
||||
[0, "before.ts", "export const before = 1;"],
|
||||
[1, "middle.ts", "export const middle = 2;"],
|
||||
[2, "after.ts", "export const after = 3;"],
|
||||
] as const) {
|
||||
expect(originals).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "source-module",
|
||||
identity: expect.objectContaining({
|
||||
source_map_sha256: digest,
|
||||
original_source: sourceName,
|
||||
source_sha256: createHash("sha256").update(content).digest("hex"),
|
||||
}),
|
||||
observations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
declaration_index: index,
|
||||
source_name_available: true,
|
||||
content_available: true,
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ sha256: digest }),
|
||||
limitations: ["Source map has no mappings string."],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user