Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 82 additions & 8 deletions src/decode/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class ItemEncoder {
function createMap(
{ scopes, ranges, names = [] }: {
scopes?: (string | null)[];
ranges?: string;
ranges?: string | string[];
names?: string[];
},
): SourceMapJson {
Expand All @@ -62,7 +62,7 @@ function createMap(
mappings: "",
sources: new Array(scopes?.length ?? 0).fill(null),
scopes,
ranges,
ranges: typeof ranges === "string" ? [ranges] : ranges,
names,
};
}
Expand Down Expand Up @@ -428,12 +428,12 @@ describe("decode", () => {
.build();
const map = encode(info);

assertExists(map.ranges);
assertExists(map.ranges?.[0]);

const parts = map.ranges.split(",");
const parts = map.ranges[0].split(",");
parts[0] += encodeUnsigned(42);
parts[0] += encodeSigned(-16);
map.ranges = parts.join(",");
map.ranges[0] = parts.join(",");

assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false });
});
Expand All @@ -447,12 +447,12 @@ describe("decode", () => {
}).endRange(10, 0).build();
const map = encode(info);

assertExists(map.ranges);
assertExists(map.ranges?.[0]);

const parts = map.ranges.split(",");
const parts = map.ranges[0].split(",");
parts[1] += encodeUnsigned(42);
parts[1] += encodeSigned(-16);
map.ranges = parts.join(",");
map.ranges[0] = parts.join(",");

assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false });
});
Expand Down Expand Up @@ -821,6 +821,80 @@ describe("decode", () => {
});
});

it("decodes multiple range segments and resets state per segment", () => {
const map1 = encode(
new ScopeInfoBuilder()
.addNullSource()
.startSource()
.startScope(0, 0, { key: "s1" })
.endScope(10, 0)
.endSource()
.startRange(0, 5, { scopeKey: "s1" })
.endRange(2, 10)
.build(),
);
const map2 = encode(
new ScopeInfoBuilder()
.addNullSource()
.startSource()
.startScope(0, 0, { key: "s1" })
.endScope(10, 0)
.endSource()
.startRange(2, 10, { scopeKey: "s1" })
.endRange(4, 20)
.build(),
);

const combinedMap: SourceMapJson = {
version: 3,
mappings: "",
sources: [null, null],
scopes: map1.scopes,
ranges: [...(map1.ranges ?? []), ...(map2.ranges ?? [])],
names: map1.names,
};

const info = decode(combinedMap, { mode: DecodeMode.STRICT });

assertEquals(info.ranges.length, 2);
assertEquals(info.ranges[0].start, { line: 0, column: 5 });
assertEquals(info.ranges[0].end, { line: 2, column: 10 });
assertStrictEquals(info.ranges[0].originalScope, info.scopes[1]?.[0]);
assertEquals(info.ranges[1].start, { line: 2, column: 10 });
assertEquals(info.ranges[1].end, { line: 4, column: 20 });
assertStrictEquals(info.ranges[1].originalScope, info.scopes[1]?.[0]);
});

it("throws in strict mode when a range segment starts before the previous segment ended", () => {
const seg1 = encode(
new ScopeInfoBuilder().startRange(0, 10).endRange(2, 20).build(),
).ranges![0];
const seg2 = encode(
new ScopeInfoBuilder().startRange(2, 15).endRange(3, 0).build(),
).ranges![0];
const map = createMap({ ranges: [seg1, seg2] });

assertThrows(
() => decode(map, { mode: DecodeMode.STRICT }),
Error,
"Range segment starts before the previous segment ended!",
);
});

it("tolerates overlapping range segments in lax mode", () => {
const seg1 = encode(
new ScopeInfoBuilder().startRange(0, 10).endRange(2, 20).build(),
).ranges![0];
const seg2 = encode(
new ScopeInfoBuilder().startRange(2, 15).endRange(3, 0).build(),
).ranges![0];
const map = createMap({ ranges: [seg1, seg2] });

const info = decode(map, { mode: DecodeMode.LAX });

assertEquals(info.ranges.length, 2);
});

describe("hasVariableAndBindingInfo", () => {
it("is 'false' when no variables/bindings are present", () => {
const map = encode(
Expand Down
36 changes: 30 additions & 6 deletions src/decode/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
SourceMapJson,
SubRangeBinding,
} from "../scopes.ts";
import { comparePositions } from "../util.ts";
import { TokenIterator } from "../vlq.ts";

/**
Expand Down Expand Up @@ -81,7 +82,7 @@ function decodeMap(

return new Decoder(
sourceMap.scopes ?? [],
sourceMap.ranges ?? "",
sourceMap.ranges ?? [],
sourceMap.names,
options,
).decode();
Expand Down Expand Up @@ -127,9 +128,10 @@ const DEFAULT_RANGE_STATE = {

class Decoder {
readonly #encodedScopes: (string | null)[];
readonly #encodedRanges: string;
readonly #encodedRanges: string[];
readonly #names: string[];
readonly #mode: DecodeMode;
readonly #generatedOffset: Position;

#scopes: (OriginalScope[] | null)[] = [];
#ranges: GeneratedRange[] = [];
Expand All @@ -153,16 +155,15 @@ class Decoder {

constructor(
scopes: (string | null)[],
ranges: string,
ranges: string[],
names: string[],
options: DecodeOptions,
) {
this.#encodedScopes = scopes;
this.#encodedRanges = ranges;
this.#names = names;
this.#mode = options.mode;
this.#rangeState.line = options.generatedOffset.line;
this.#rangeState.column = options.generatedOffset.column;
this.#generatedOffset = options.generatedOffset;
}

decode(): DecodedScopeInfo {
Expand All @@ -175,7 +176,9 @@ class Decoder {
this.#decodeScopes(encodedScope);
}

this.#decodeRanges(this.#encodedRanges);
for (const encodedRange of this.#encodedRanges) {
this.#decodeRanges(encodedRange);
}

const info = {
scopes: this.#scopes,
Expand Down Expand Up @@ -270,6 +273,12 @@ class Decoder {
}

#decodeRanges(encodedRanges: string) {
Object.assign(this.#rangeState, DEFAULT_RANGE_STATE);
this.#rangeState.line = this.#generatedOffset.line;
this.#rangeState.column = this.#generatedOffset.column;
const prevLastRange = this.#ranges.at(-1);
const prevRangesLength = this.#ranges.length;

const iter = new TokenIterator(
encodedRanges,
this.#mode === DecodeMode.STRICT,
Expand Down Expand Up @@ -369,6 +378,21 @@ class Decoder {
this.#throwInStrictMode(
"Encountered GENERATED_RANGE_START without matching END!",
);
this.#rangeStack.length = 0;
}

if (
prevLastRange &&
this.#ranges.length > prevRangesLength &&
comparePositions(
this.#ranges[prevRangesLength].start,
prevLastRange.end,
) <
0
) {
this.#throwInStrictMode(
"Range segment starts before the previous segment ended!",
);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/encode/encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ describe("encode", () => {
const info = builder.addNullSource().addNullSource().addNullSource()
.build();

assertEquals(encode(info).scopes, [null, null, null]);
const encoded = encode(info);
assertEquals(encoded.scopes, [null, null, null]);
assertEquals(encoded.ranges, []);
});

it("throws when a subsequent root scope precedes the previous root scope in the same source", () => {
Expand Down
6 changes: 4 additions & 2 deletions src/encode/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class Encoder {
}
}

encode(): { scopes: (string | null)[]; ranges: string } {
encode(): { scopes: (string | null)[]; ranges: string[] } {
const encodedScopes = this.#info.scopes.map((scopes, sourceIdx) => {
if (scopes === null) {
return null;
Expand All @@ -75,7 +75,9 @@ export class Encoder {

return {
scopes: encodedScopes,
ranges: this.#encodedItems.join(","),
ranges: this.#encodedItems.length > 0
? [this.#encodedItems.join(",")]
: [],
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,5 @@ export interface SourceMapJson {
mappings: string;
names?: string[];
scopes?: (string | null)[];
ranges?: string;
ranges?: string[];
}
Loading