Share section classification across declaration analysis and mapping parsing. Preserve external URLs, malformed section reasons, section paths and safe sibling declarations without fetching nested maps. Refs #5466. All 50 focused tests passed, including real CLI and stdio MCP regressions; typecheck and lint passed.
520 lines
16 KiB
TypeScript
520 lines
16 KiB
TypeScript
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 in section [1]."],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
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 in section [1].",
|
|
],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
it("keeps external indexed sections unsupported while retaining local siblings", async () => {
|
|
const url = "https://app.test/maps/part.map";
|
|
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 }, url },
|
|
{
|
|
offset: { line: 2, column: 0 },
|
|
url: 42,
|
|
map: {
|
|
version: 3,
|
|
sources: ["ignored.ts"],
|
|
sourcesContent: ["export const ignored = 0;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
{
|
|
offset: { line: 3, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: ["after.ts"],
|
|
sourcesContent: ["export const after = 2;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const graph = await analyzeMaps({ "indexed.js.map": mapText });
|
|
const digest = createHash("sha256").update(mapText).digest("hex");
|
|
const limitation =
|
|
`Indexed source-map section at [2]: External source-map section URL must be a string.; ` +
|
|
`External source-map section URL is not fetched: ${url}`;
|
|
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",
|
|
external_sections: [{ section_path: [1], url }],
|
|
invalid_sections: [
|
|
{
|
|
section_path: [2],
|
|
reason: "External source-map section URL must be a string.",
|
|
},
|
|
],
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
artifact: expect.objectContaining({ sha256: digest }),
|
|
limitations: [limitation],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
for (const [source, content] of [
|
|
["before.ts", "export const before = 1;"],
|
|
["after.ts", "export const after = 2;"],
|
|
] as const) {
|
|
expect(graph.nodes).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "source-module",
|
|
identity: expect.objectContaining({
|
|
source_map_sha256: digest,
|
|
original_source: source,
|
|
source_sha256: createHash("sha256").update(content).digest("hex"),
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
it("marks non-array sections malformed even when the object has regular-map fields", async () => {
|
|
const mapText = JSON.stringify({
|
|
version: 3,
|
|
sections: null,
|
|
sources: ["must-not-be-read.ts"],
|
|
sourcesContent: ["export const value = 1;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
});
|
|
const graph = await analyzeMaps({ "malformed.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: "malformed.js.map",
|
|
operation: "parse-local-source-map",
|
|
invalid_sections: [
|
|
{
|
|
section_path: [],
|
|
reason: "Indexed source-map sections must be an array.",
|
|
},
|
|
],
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
artifact: expect.objectContaining({ sha256: digest }),
|
|
limitations: [
|
|
"Indexed source-map section at []: Indexed source-map sections must be an array.",
|
|
],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
expect(
|
|
graph.nodes.some(
|
|
(node) =>
|
|
node.identity.strategy === "source-map-original" &&
|
|
node.identity.source_map_sha256 === digest,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("retains declarations around a semantically invalid indexed 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, sourcesContent: [], names: [], mappings: "AAAA" },
|
|
},
|
|
{
|
|
offset: { line: 2, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: ["after.ts"],
|
|
sourcesContent: ["export const after = 2;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const graph = await analyzeMaps({ "indexed-invalid-leaf.js.map": mapText });
|
|
const digest = createHash("sha256").update(mapText).digest("hex");
|
|
const limitation = "Source map has no sources array in section [1].";
|
|
|
|
expect(graph.coverage.status).toBe("partial");
|
|
expect(graph.nodes).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "unknown",
|
|
observations: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
path: "indexed-invalid-leaf.js.map",
|
|
operation: "parse-local-source-map",
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
artifact: expect.objectContaining({ sha256: digest }),
|
|
limitations: [limitation],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
for (const [source, section_path, content] of [
|
|
["before.ts", [0], "export const before = 1;"],
|
|
["after.ts", [2], "export const after = 2;"],
|
|
] as const)
|
|
expect(graph.nodes).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "source-module",
|
|
identity: expect.objectContaining({
|
|
source_map_sha256: digest,
|
|
original_source: source,
|
|
source_sha256: createHash("sha256").update(content).digest("hex"),
|
|
}),
|
|
observations: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({ section_path }),
|
|
evidence: expect.objectContaining({ limitations: [limitation] }),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps safe declarations across malformed roots, contents, and source entries", 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,
|
|
sourceRoot: 42,
|
|
sources: ["unknown-root.ts"],
|
|
sourcesContent: ["export const unknownRoot = 2;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
{
|
|
offset: { line: 2, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: ["content.ts"],
|
|
sourcesContent: [42],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
{
|
|
offset: { line: 3, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: [42, "valid-entry.ts"],
|
|
sourcesContent: [null, "export const validEntry = 3;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
{
|
|
offset: { line: 4, column: 0 },
|
|
map: {
|
|
version: 3,
|
|
sources: ["after.ts"],
|
|
sourcesContent: ["export const after = 4;"],
|
|
names: [],
|
|
mappings: "AAAA",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const graph = await analyzeMaps({
|
|
"indexed-malformed-values.js.map": mapText,
|
|
});
|
|
const digest = createHash("sha256").update(mapText).digest("hex");
|
|
const limitation = [
|
|
"Source map sourceRoot must be a string or null in section [1].",
|
|
"Source map sourcesContent must be an array of strings or null entries in section [2].",
|
|
"Source map contains a non-string source name at index 0 in section [3].",
|
|
].join("; ");
|
|
|
|
expect(graph.coverage.status).toBe("partial");
|
|
expect(graph.nodes).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "unknown",
|
|
observations: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
evidence: expect.objectContaining({
|
|
artifact: expect.objectContaining({ sha256: digest }),
|
|
limitations: [limitation],
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
for (const [source, section_path, content, source_index] of [
|
|
["before.ts", [0], "export const before = 1;", 0],
|
|
["content.ts", [2], null, 0],
|
|
["valid-entry.ts", [3], "export const validEntry = 3;", 1],
|
|
["after.ts", [4], "export const after = 4;", 0],
|
|
] as const)
|
|
expect(graph.nodes).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "source-module",
|
|
identity: expect.objectContaining({
|
|
source_map_sha256: digest,
|
|
original_source: source,
|
|
source_sha256:
|
|
content === null
|
|
? null
|
|
: createHash("sha256").update(content).digest("hex"),
|
|
}),
|
|
observations: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({ section_path, source_index }),
|
|
evidence: expect.objectContaining({ limitations: [limitation] }),
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
});
|