fix(javascript): retain partial indexed source map facts

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.
This commit is contained in:
morluto committed 2026-10-20 22:18:23 +08:00
1 parent 497037e6e3
commit 2b2e362a00
9 files changed
+972 -105

No files matched your search

@@ -158,7 +158,7 @@ it("retains declarations before and after an invalid indexed-map leaf", async ()
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({ sha256: digest }),
limitations: ["Source map has no mappings string."],
limitations: ["Source map has no mappings string in section [1]."],
}),
}),
]),
@@ -192,7 +192,9 @@ it("retains declarations before and after an invalid indexed-map leaf", async ()
}),
evidence: expect.objectContaining({
artifact: expect.objectContaining({ sha256: digest }),
limitations: ["Source map has no mappings string."],
limitations: [
"Source map has no mappings string in section [1].",
],
}),
}),
]),
@@ -200,3 +202,318 @@ it("retains declarations before and after an invalid indexed-map leaf", async ()
);
}
});
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] }),
}),
]),
}),
);
});