mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 09:55:46 +00:00
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:
1 parent
497037e6e3
commit
2b2e362a00
9 files changed
+972
-105
No files matched your search
@@ -1,9 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
||||
import {
|
||||
classifySourceMapSection,
|
||||
classifySourceMapSections,
|
||||
sourceMapJsonText,
|
||||
sourceMapMappingsSchema,
|
||||
} from "../../domain/sourceMapEnvelope.js";
|
||||
import type { SourceMapSection } from "../../domain/sourceMapEnvelope.js";
|
||||
import type { EncodedSourceMapXInput } from "@jridgewell/trace-mapping";
|
||||
|
||||
const leafSchema = z.object({
|
||||
@@ -28,13 +31,8 @@ const sectionSchema = z.object({
|
||||
column: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
||||
}),
|
||||
map: z.unknown().optional(),
|
||||
url: z.string().optional(),
|
||||
url: z.unknown().optional(),
|
||||
});
|
||||
const indexedSchema = z.object({
|
||||
version: z.literal(3),
|
||||
sections: z.array(sectionSchema),
|
||||
});
|
||||
|
||||
type Offset = { readonly line: number; readonly column: number };
|
||||
interface PendingMap {
|
||||
readonly value: unknown;
|
||||
@@ -126,21 +124,26 @@ const consumeRecords = (budget: { records: number }, count: number): void => {
|
||||
budget.records += count;
|
||||
};
|
||||
|
||||
const addSections = (
|
||||
node: PendingMap,
|
||||
pending: PendingMap[],
|
||||
budget: { records: number },
|
||||
options: SourceMapStructureOptions,
|
||||
): void => {
|
||||
const addSections = ({
|
||||
node,
|
||||
pending,
|
||||
rawSections,
|
||||
budget,
|
||||
options,
|
||||
}: {
|
||||
readonly node: PendingMap;
|
||||
readonly pending: PendingMap[];
|
||||
readonly rawSections: readonly unknown[];
|
||||
readonly budget: { records: number };
|
||||
readonly options: SourceMapStructureOptions;
|
||||
}): void => {
|
||||
options.check?.();
|
||||
const header = mapHeaderSchema.parse(node.value);
|
||||
if (!Array.isArray(header.sections))
|
||||
throw new SourceMapFormatFailure(
|
||||
"format",
|
||||
"Indexed source-map sections must be an array.",
|
||||
);
|
||||
consumeRecords(budget, header.sections.length);
|
||||
const sections = indexedSchema.parse(node.value).sections;
|
||||
consumeRecords(budget, rawSections.length);
|
||||
const sections = z.array(sectionSchema).parse(rawSections);
|
||||
const embeddedMaps: Extract<
|
||||
SourceMapSection,
|
||||
{ readonly kind: "embedded" }
|
||||
>["map"][] = [];
|
||||
for (const [index, section] of sections.entries()) {
|
||||
if ((index & 255) === 0) options.check?.();
|
||||
const previous = sections[index - 1];
|
||||
@@ -149,20 +152,25 @@ const addSections = (
|
||||
"format",
|
||||
"Indexed section offsets must be strictly increasing.",
|
||||
);
|
||||
if (section.url !== undefined)
|
||||
const classification = classifySourceMapSection(section);
|
||||
if (classification.kind === "external")
|
||||
throw new SourceMapFormatFailure(
|
||||
"unsupported",
|
||||
`External source-map section URL is not fetched: ${section.url}`,
|
||||
);
|
||||
if (section.map === undefined)
|
||||
throw new SourceMapFormatFailure(
|
||||
"format",
|
||||
"Indexed section requires an embedded map.",
|
||||
`External source-map section URL is not fetched: ${classification.url}`,
|
||||
);
|
||||
if (classification.kind === "invalid")
|
||||
throw new SourceMapFormatFailure("format", classification.reason);
|
||||
embeddedMaps.push(classification.map);
|
||||
}
|
||||
for (let index = sections.length - 1; index >= 0; index -= 1) {
|
||||
const section = sections[index];
|
||||
if (section === undefined) continue;
|
||||
const map = embeddedMaps[index];
|
||||
if (map === undefined)
|
||||
throw new SourceMapFormatFailure(
|
||||
"format",
|
||||
"Validated indexed section map is unavailable.",
|
||||
);
|
||||
const next = sections[index + 1];
|
||||
const nextOffset =
|
||||
next === undefined ? null : addOffset(node.offset, next.offset);
|
||||
@@ -179,7 +187,7 @@ const addSections = (
|
||||
"Nested source-map section overlaps its enclosing boundary.",
|
||||
);
|
||||
pending.push({
|
||||
value: section.map,
|
||||
value: map,
|
||||
path: [...node.path, index],
|
||||
offset,
|
||||
stop,
|
||||
@@ -319,8 +327,17 @@ export const inspectSourceMapValue = (
|
||||
"limit",
|
||||
"Indexed source-map nesting exceeds the 64-level decoder stack budget.",
|
||||
);
|
||||
if (mapHeaderSchema.parse(node.value).sections !== undefined) {
|
||||
addSections(node, pending, budget, options);
|
||||
const sections = classifySourceMapSections(node.value);
|
||||
if (sections.kind === "invalid")
|
||||
throw new SourceMapFormatFailure("format", sections.reason);
|
||||
if (sections.kind === "indexed") {
|
||||
addSections({
|
||||
node,
|
||||
pending,
|
||||
rawSections: sections.sections,
|
||||
budget,
|
||||
options,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const leaf = inspectLeaf(node, budget, options);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { traceSourceMap } from "./TraceSourceMap.js";
|
||||
import { inspectSourceMap, SourceMapFormatFailure } from "./SourceMapFormat.js";
|
||||
|
||||
const url = "https://app.test/maps/app.js.map?v=2#map";
|
||||
const point = { line: 1, column: 0 };
|
||||
@@ -15,6 +16,76 @@ const leaf = {
|
||||
const trace = (map: unknown, position = point) =>
|
||||
traceSourceMap(JSON.stringify(map), url, position);
|
||||
|
||||
it("classifies external indexed sections as unsupported with their exact URL", () => {
|
||||
const externalUrl = "https://app.test/maps/part.map?revision=7";
|
||||
try {
|
||||
inspectSourceMap(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sections: [{ offset: { line: 0, column: 0 }, url: externalUrl }],
|
||||
}),
|
||||
);
|
||||
throw new Error("Expected external section rejection");
|
||||
} catch (error: unknown) {
|
||||
expect(error).toBeInstanceOf(SourceMapFormatFailure);
|
||||
expect(error).toMatchObject({
|
||||
reason: "unsupported",
|
||||
message: `External source-map section URL is not fetched: ${externalUrl}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the same conservative section classification for URL-plus-map and malformed URLs", () => {
|
||||
const externalUrl = "https://app.test/maps/part.map";
|
||||
const external = () =>
|
||||
inspectSourceMap(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sections: [
|
||||
{ offset: { line: 0, column: 0 }, url: externalUrl, map: leaf },
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(external).toThrow(
|
||||
expect.objectContaining({
|
||||
reason: "unsupported",
|
||||
message: `External source-map section URL is not fetched: ${externalUrl}`,
|
||||
}),
|
||||
);
|
||||
|
||||
const malformed = () =>
|
||||
inspectSourceMap(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sections: [{ offset: { line: 0, column: 0 }, url: 12, map: leaf }],
|
||||
}),
|
||||
);
|
||||
expect(malformed).toThrow(
|
||||
expect.objectContaining({
|
||||
reason: "format",
|
||||
message: "External source-map section URL must be a string.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a present non-array sections member through the shared section-list rule", () => {
|
||||
const malformed = () =>
|
||||
inspectSourceMap(
|
||||
JSON.stringify({
|
||||
version: 3,
|
||||
sections: null,
|
||||
sources: ["ignored.ts"],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
);
|
||||
expect(malformed).toThrow(
|
||||
expect.objectContaining({
|
||||
reason: "format",
|
||||
message: "Indexed source-map sections must be an array.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("traces BOM-prefixed maps with a source-map transport prefix ending in CR", () => {
|
||||
const text = `\uFEFF)]}'transport prefix\r${JSON.stringify(leaf)}`;
|
||||
const report = traceSourceMap(text, url, point);
|
||||
|
||||
Reference in new issue
Block a user