Skip to content

Commit 33baa6f

Browse files
authored
feat!: change vendor extension tag to 'V' (#22)
Implements tc39/ecma426#278. The spec now reserves the 'V' tag (0x15) for vendor extension items instead of '/'. '/' has the VLQ continuation bit set and can't be used as a single character tag. Note that the previous Tag.VENDOR_EXTENSION value 0x63 didn't match '/' either (it encodes as "jD"). Also expands test coverage for vendor extensions: interspersed with known scope and range items in strict mode, multi-character and out-of-bounds name indices, and rejection of the legacy tags.
1 parent de807fe commit 33baa6f

2 files changed

Lines changed: 189 additions & 3 deletions

File tree

‎src/codec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const enum Tag {
1111
GENERATED_RANGE_BINDINGS = 0x6,
1212
GENERATED_RANGE_SUBRANGE_BINDING = 0x7,
1313
GENERATED_RANGE_CALL_SITE = 0x8,
14-
VENDOR_EXTENSION = 0x63,
14+
VENDOR_EXTENSION = 0x15,
1515
}
1616

1717
export const enum EncodedTag {
@@ -23,7 +23,7 @@ export const enum EncodedTag {
2323
GENERATED_RANGE_BINDINGS = "G", // 0x6
2424
GENERATED_RANGE_SUBRANGE_BINDING = "H", // 0x7
2525
GENERATED_RANGE_CALL_SITE = "I", // 0x8
26-
VENDOR_EXTENSION = "/", // 0x63
26+
VENDOR_EXTENSION = "V", // 0x15
2727
}
2828

2929
export const enum OriginalScopeFlags {

‎src/decode/decode.test.ts‎

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import {
1616
import { encodeSigned, encodeUnsigned } from "../vlq.ts";
1717
import { decode, DecodeMode } from "./decode.ts";
1818
import type { IndexSourceMapJson, SourceMapJson } from "../scopes.ts";
19-
import { GeneratedRangeFlags, OriginalScopeFlags, Tag } from "../codec.ts";
19+
import {
20+
EncodedTag,
21+
GeneratedRangeFlags,
22+
OriginalScopeFlags,
23+
Tag,
24+
} from "../codec.ts";
2025

2126
class ItemEncoder {
2227
#encodedItems: string[] = [];
@@ -730,6 +735,7 @@ describe("decode", () => {
730735
encoder.addUnsignedVLQs(Tag.VENDOR_EXTENSION, 0);
731736
encoder.finishItem();
732737
const ext = encoder.encode();
738+
assertEquals(ext, "VA");
733739
const map = createMap({
734740
scopes: [ext],
735741
ranges: ext,
@@ -743,6 +749,186 @@ describe("decode", () => {
743749
});
744750
});
745751

752+
it("ignores vendor extensions with trailing VLQs", () => {
753+
const map = createMap({
754+
scopes: ["VAEFG"],
755+
ranges: "VACD",
756+
names: ["x_ext_item"],
757+
});
758+
759+
assertEquals(decode(map, { mode: DecodeMode.STRICT }), {
760+
scopes: [[]],
761+
ranges: [],
762+
hasVariableAndBindingInfo: false,
763+
});
764+
});
765+
766+
describe("vendor extensions interspersed with known items", () => {
767+
/**
768+
* Inserts a vendor extension item before, between and after every item of
769+
* `encoded`. Every other vendor extension item carries trailing VLQs.
770+
*/
771+
function intersperseVendorExtensions(
772+
encoded: string,
773+
nameIdx: number,
774+
): string {
775+
const plain = EncodedTag.VENDOR_EXTENSION + encodeUnsigned(nameIdx);
776+
const withTrailing = plain + encodeUnsigned(42) + encodeSigned(-7) +
777+
encodeUnsigned(1000);
778+
const items: string[] = [plain];
779+
encoded.split(",").forEach((item, i) => {
780+
items.push(item, i % 2 === 0 ? withTrailing : plain);
781+
});
782+
return items.join(",");
783+
}
784+
785+
function buildInfo() {
786+
return new ScopeInfoBuilder()
787+
.startSource()
788+
.startScope(0, 0, {
789+
key: "fn",
790+
name: "foo",
791+
kind: "function",
792+
isStackFrame: true,
793+
variables: ["a", "b"],
794+
})
795+
.startScope(2, 4, { key: "block", kind: "block", variables: ["c"] })
796+
.endScope(2, 10)
797+
.startScope(2, 12, { kind: "block" })
798+
.endScope(5, 0)
799+
.endScope(20, 0)
800+
.startScope(21, 0, { name: "bar" })
801+
.endScope(30, 0)
802+
.endSource()
803+
.addNullSource()
804+
.startSource()
805+
.startScope(0, 0, { key: "other", variables: ["d"] })
806+
.endScope(5, 0)
807+
.endSource()
808+
.startRange(0, 0, {
809+
scopeKey: "fn",
810+
isStackFrame: true,
811+
values: ["x", null],
812+
})
813+
.startRange(0, 5, {
814+
scopeKey: "block",
815+
values: [[
816+
{
817+
value: "y",
818+
from: { line: 0, column: 5 },
819+
to: { line: 0, column: 8 },
820+
},
821+
{
822+
value: "z",
823+
from: { line: 0, column: 8 },
824+
to: { line: 0, column: 15 },
825+
},
826+
]],
827+
})
828+
.endRange(0, 15)
829+
.startRange(1, 0, {
830+
scopeKey: "other",
831+
isHidden: true,
832+
values: ["w"],
833+
callSite: { sourceIndex: 0, line: 3, column: 2 },
834+
})
835+
.endRange(1, 20)
836+
.endRange(10, 0)
837+
.build();
838+
}
839+
840+
it("ignores them between scope items in strict mode", () => {
841+
const map = encode(buildInfo());
842+
const expected = decode(map, { mode: DecodeMode.STRICT });
843+
assertExists(map.scopes);
844+
assertEquals(map.scopes.length, 3);
845+
846+
map.names!.push("x_vendor");
847+
const nameIdx = map.names!.length - 1;
848+
map.scopes = map.scopes.map((s) =>
849+
s === null ? null : intersperseVendorExtensions(s, nameIdx)
850+
);
851+
852+
assertEquals(decode(map, { mode: DecodeMode.STRICT }), expected);
853+
assert(expected.hasVariableAndBindingInfo);
854+
});
855+
856+
it("ignores them between range items in strict mode", () => {
857+
const map = encode(buildInfo());
858+
const expected = decode(map, { mode: DecodeMode.STRICT });
859+
assertExists(map.ranges);
860+
861+
map.names!.push("x_vendor");
862+
const nameIdx = map.names!.length - 1;
863+
map.ranges = map.ranges.map((r) =>
864+
intersperseVendorExtensions(r, nameIdx)
865+
);
866+
867+
assertEquals(decode(map, { mode: DecodeMode.STRICT }), expected);
868+
assert(expected.hasVariableAndBindingInfo);
869+
});
870+
871+
it("handles multi-character name indices", () => {
872+
const map = encode(buildInfo());
873+
const expected = decode(map, { mode: DecodeMode.STRICT });
874+
assertExists(map.scopes?.[0]);
875+
assertExists(map.ranges?.[0]);
876+
877+
// Pad "names" so the vendor extension name requires a multi-character VLQ.
878+
const nameIdx = 1000;
879+
while (map.names!.length < nameIdx) map.names!.push("pad");
880+
map.names!.push("x_vendor");
881+
assert(encodeUnsigned(nameIdx).length > 1);
882+
883+
map.scopes[0] = intersperseVendorExtensions(map.scopes[0], nameIdx);
884+
map.ranges[0] = intersperseVendorExtensions(map.ranges[0], nameIdx);
885+
886+
assertEquals(decode(map, { mode: DecodeMode.STRICT }), expected);
887+
});
888+
889+
it("ignores out-of-bounds name indices in strict mode", () => {
890+
const map = encode(buildInfo());
891+
const expected = decode(map, { mode: DecodeMode.STRICT });
892+
assertExists(map.scopes?.[0]);
893+
assertExists(map.ranges?.[0]);
894+
895+
const nameIdx = map.names!.length + 500;
896+
map.scopes[0] = intersperseVendorExtensions(map.scopes[0], nameIdx);
897+
map.ranges[0] = intersperseVendorExtensions(map.ranges[0], nameIdx);
898+
899+
assertEquals(decode(map, { mode: DecodeMode.STRICT }), expected);
900+
});
901+
});
902+
903+
describe("legacy vendor extension tags", () => {
904+
// The spec used to reserve "/" for vendor extensions (tc39/ecma426#278
905+
// changed it to "V"). "/" has the continuation bit set, so "/A" decodes
906+
// to tag 31. Older versions of this codec emitted 0x63 ("jD") instead.
907+
const legacyItems: [string, number][] = [["/A", 31], ["jDA", 0x63]];
908+
909+
for (const [item, tag] of legacyItems) {
910+
it(`throws for '${item}' in scopes in strict mode`, () => {
911+
const map = createMap({ scopes: [item], names: ["x_vendor"] });
912+
913+
assertThrows(
914+
() => decode(map, { mode: DecodeMode.STRICT }),
915+
Error,
916+
`Encountered illegal item tag ${tag}`,
917+
);
918+
});
919+
920+
it(`throws for '${item}' in ranges in strict mode`, () => {
921+
const map = createMap({ ranges: item, names: ["x_vendor"] });
922+
923+
assertThrows(
924+
() => decode(map, { mode: DecodeMode.STRICT }),
925+
Error,
926+
`Encountered illegal item tag ${tag}`,
927+
);
928+
});
929+
}
930+
});
931+
746932
it("throws for invalid item tags in strict mode", () => {
747933
const encoder = new ItemEncoder();
748934
encoder.addUnsignedVLQs(42, 1, 2, 3);

0 commit comments

Comments
 (0)