diff --git a/src/codec.ts b/src/codec.ts index 6e8fbe1..b7833ae 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -53,5 +53,8 @@ export interface GeneratedRangeStartItem { flags: number; line?: number; column: number; - definitionIdx?: number; + definition?: { + sourceIdx: number; + scopeIdx: number; + }; } diff --git a/src/decode/decode.test.ts b/src/decode/decode.test.ts index a2a8421..a819cb1 100644 --- a/src/decode/decode.test.ts +++ b/src/decode/decode.test.ts @@ -410,14 +410,78 @@ describe("decode", () => { assertEquals(info.ranges[0]?.values, [""]); }); + it("handles trailing VLQs in GENERATED_RANGE_START items", () => { + const info = new ScopeInfoBuilder().startRange(0, 0).endRange(10, 0) + .build(); + const map = encode(info); + + assertExists(map.scopes); + + const parts = map.scopes.split(","); + parts[0] += encodeUnsigned(42); + parts[0] += encodeSigned(-16); + map.scopes = parts.join(","); + + assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false }); + }); + + it("handles trailing VLQs in GENERATED_RANGE_CALL_SITE items", () => { + const info = new ScopeInfoBuilder().startSource().startScope(0, 0, { + key: "fn", + }).endScope(10, 0).endSource().startRange(0, 0, { + scopeKey: "fn", + callSite: { sourceIndex: 0, line: 2, column: 4 }, + }).endRange(10, 0).build(); + const map = encode(info); + + assertExists(map.scopes); + + const parts = map.scopes.split(","); + parts[3] += encodeUnsigned(42); + parts[3] += encodeSigned(-16); + map.scopes = parts.join(","); + + assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false }); + }); + + it("throws if GENERATED_RANGE_START.definition has an invalid source index in strict mode", () => { + const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs( + Tag.GENERATED_RANGE_START, + GeneratedRangeFlags.HAS_DEFINITION, + 0, + ).addSignedVLQs(1).addUnsignedVLQs(0).finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 2).finishItem(); + const map = createMap(encoder.encode(), []); + + assertThrows(() => decode(map, { mode: DecodeMode.STRICT })); + }); + + it("ignores if GENERATED_RANGE_START.definition has an invalid source index in lax mode", () => { + const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs( + Tag.GENERATED_RANGE_START, + GeneratedRangeFlags.HAS_DEFINITION, + 0, + ).addSignedVLQs(1).addUnsignedVLQs(0).finishItem(); + encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 2).finishItem(); + const map = createMap(encoder.encode(), []); + + const info = decode(map, { mode: DecodeMode.LAX }); + + assertExists(info.ranges[0]); + assertStrictEquals(info.ranges[0].originalScope, undefined); + }); + it("throws if GENERATED_RANGE_START.definition is not a valid original scope in strict mode", () => { const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_START, 0, 0, 0).finishItem(); + encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_END, 1, 0).finishItem(); encoder.addUnsignedVLQs( Tag.GENERATED_RANGE_START, GeneratedRangeFlags.HAS_DEFINITION, 0, - 1, - ).finishItem(); + ).addSignedVLQs(0, 1).finishItem(); encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 2).finishItem(); const map = createMap(encoder.encode(), []); @@ -426,12 +490,13 @@ describe("decode", () => { it("ignores if GENERATED_RANGE_START.definition is not a valid original scope in lax mode", () => { const encoder = new ItemEncoder(); + encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_START, 0, 0, 0).finishItem(); + encoder.addUnsignedVLQs(Tag.ORIGINAL_SCOPE_END, 1, 0).finishItem(); encoder.addUnsignedVLQs( Tag.GENERATED_RANGE_START, GeneratedRangeFlags.HAS_DEFINITION, 0, - 1, - ).finishItem(); + ).addSignedVLQs(0, 1).finishItem(); encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_END, 2).finishItem(); const map = createMap(encoder.encode(), []); @@ -441,6 +506,34 @@ describe("decode", () => { assertStrictEquals(info.ranges[0].originalScope, undefined); }); + it("decodes cross-source and same-source GENERATED_RANGE_START.definition indices", () => { + const scopes = new ScopeInfoBuilder() + .startSource() + .startScope(0, 0, { key: "s0_root" }) + .startScope(1, 0, { key: "s0_child" }) + .endScope(2, 0) + .endScope(3, 0) + .endSource() + .startSource() + .startScope(0, 0, { key: "s1_root" }) + .startScope(1, 0, { key: "s1_child" }) + .endScope(2, 0) + .endScope(3, 0) + .endSource() + .startRange(0, 0, { scopeKey: "s1_child" }) + .startRange(0, 2, { scopeKey: "s1_root" }) + .endRange(0, 4) + .startRange(0, 5, { scopeKey: "s0_child" }) + .endRange(0, 8) + .endRange(0, 10) + .build(); + + const map = encode(scopes); + const decoded = decode(map, { mode: DecodeMode.STRICT }); + + assertEquals(decoded, { ...scopes, hasVariableAndBindingInfo: false }); + }); + it("throws for free GENERATED_RANGE_CALL_SITE items in strict mode", () => { const encoder = new ItemEncoder(); encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_CALL_SITE); @@ -492,8 +585,7 @@ describe("decode", () => { Tag.GENERATED_RANGE_START, GeneratedRangeFlags.HAS_DEFINITION, 0, - 0, - ).addSignedVLQs(0).finishItem(); + ).addSignedVLQs(0, 0).finishItem(); // Initial binding for the variable is "bar" (index 2). encoder.addUnsignedVLQs(Tag.GENERATED_RANGE_BINDINGS, 2).finishItem(); diff --git a/src/decode/decode.ts b/src/decode/decode.ts index 6339401..ccd84e3 100644 --- a/src/decode/decode.ts +++ b/src/decode/decode.ts @@ -116,6 +116,7 @@ const DEFAULT_SCOPE_STATE = { const DEFAULT_RANGE_STATE = { line: 0, column: 0, + defSourceIdx: 0, defScopeIdx: 0, }; @@ -133,7 +134,8 @@ class Decoder { readonly #scopeStack: OriginalScope[] = []; readonly #rangeStack: GeneratedRange[] = []; - #flatOriginalScopes: OriginalScope[] = []; + #flatOriginalScopes: (OriginalScope[] | null)[] = []; + #currentFlatScopes: OriginalScope[] = []; #subRangeBindingsForRange = new Map< GeneratedRange, Map @@ -161,6 +163,7 @@ class Decoder { switch (tag) { case Tag.EMPTY: { this.#scopes.push(null); + this.#flatOriginalScopes.push(null); break; } case Tag.ORIGINAL_SCOPE_START: { @@ -205,15 +208,20 @@ class Decoder { : undefined; const column = iter.nextUnsignedVLQ(); - const definitionIdx = flags & GeneratedRangeFlags.HAS_DEFINITION - ? iter.nextSignedVLQ() - : undefined; + let definition: { sourceIdx: number; scopeIdx: number } | undefined; + if (flags & GeneratedRangeFlags.HAS_DEFINITION) { + const sourceIdx = iter.nextSignedVLQ(); + const scopeIdx = sourceIdx === 0 + ? iter.nextSignedVLQ() + : iter.nextUnsignedVLQ(); + definition = { sourceIdx, scopeIdx }; + } this.#handleGeneratedRangeStartItem({ flags, line, column, - definitionIdx, + definition, }); break; } @@ -301,6 +309,7 @@ class Decoder { this.#scopes = []; this.#ranges = []; this.#flatOriginalScopes = []; + this.#currentFlatScopes = []; this.#seenOriginalScopeVariables = false; this.#seenGeneratedRangeBindings = false; @@ -340,7 +349,7 @@ class Decoder { ); this.#scopeStack.push(scope); - this.#flatOriginalScopes.push(scope); + this.#currentFlatScopes.push(scope); } #handleOriginalScopeVariablesItem(variableIdxs: number[]) { @@ -385,6 +394,8 @@ class Decoder { parent.children.push(scope); } else { this.#scopes.push([scope]); + this.#flatOriginalScopes.push(this.#currentFlatScopes); + this.#currentFlatScopes = []; this.#scopeState.line = 0; this.#scopeState.column = 0; } @@ -415,16 +426,20 @@ class Decoder { children: [], }; - if (item.definitionIdx !== undefined) { - this.#rangeState.defScopeIdx += item.definitionIdx; - if ( - this.#rangeState.defScopeIdx < 0 || - this.#rangeState.defScopeIdx >= this.#flatOriginalScopes.length - ) { + if (item.definition !== undefined) { + this.#rangeState.defSourceIdx += item.definition.sourceIdx; + if (item.definition.sourceIdx !== 0) { + this.#rangeState.defScopeIdx = 0; + } + this.#rangeState.defScopeIdx += item.definition.scopeIdx; + + const originalScope = this + .#flatOriginalScopes[this.#rangeState.defSourceIdx] + ?.[this.#rangeState.defScopeIdx]; + if (!originalScope) { this.#throwInStrictMode("Invalid definition scope index"); } else { - range.originalScope = - this.#flatOriginalScopes[this.#rangeState.defScopeIdx]; + range.originalScope = originalScope; } } diff --git a/src/encode/encoder.ts b/src/encode/encoder.ts index 30e6397..6c8c914 100644 --- a/src/encode/encoder.ts +++ b/src/encode/encoder.ts @@ -22,6 +22,7 @@ const DEFAULT_SCOPE_STATE = { const DEFAULT_RANGE_STATE = { line: 0, column: 0, + defSourceIdx: 0, defScopeIdx: 0, }; @@ -38,7 +39,11 @@ export class Encoder { #encodedItems: string[] = []; #currentItem: string = ""; - #scopeToCount = new Map(); + #scopeToLocation = new Map< + OriginalScope, + { sourceIdx: number; scopeIdx: number } + >(); + #currentSourceIdx = 0; #scopeCounter = 0; constructor(info: ScopeInfo, names: string[]) { @@ -52,11 +57,13 @@ export class Encoder { encode(): string { this.#encodedItems = []; - this.#info.scopes.forEach((scopes) => { + this.#info.scopes.forEach((scopes, sourceIdx) => { if (scopes === null) { this.#encodedItems.push(EncodedTag.EMPTY); return; } + this.#currentSourceIdx = sourceIdx; + this.#scopeCounter = 0; this.#scopeState.line = 0; this.#scopeState.column = 0; scopes.forEach((scope) => this.#encodeOriginalScope(scope)); @@ -111,7 +118,10 @@ export class Encoder { if (encodedKind !== undefined) this.#encodeSigned(encodedKind); this.#finishItem(); - this.#scopeToCount.set(scope, this.#scopeCounter++); + this.#scopeToLocation.set(scope, { + sourceIdx: this.#currentSourceIdx, + scopeIdx: this.#scopeCounter++, + }); } #encodeOriginalScopeVariables(scope: OriginalScope) { @@ -168,17 +178,23 @@ export class Encoder { this.#rangeState.line = line; this.#rangeState.column = column; - let encodedDefinition; + let encodedDefSourceIdx: number | undefined; + let encodedDefScopeIdx: number | undefined; if (range.originalScope) { - const definitionIdx = this.#scopeToCount.get(range.originalScope); - if (definitionIdx === undefined) { + const location = this.#scopeToLocation.get(range.originalScope); + if (location === undefined) { throw new Error("Unknown OriginalScope for definition!"); } flags |= GeneratedRangeFlags.HAS_DEFINITION; - encodedDefinition = definitionIdx - this.#rangeState.defScopeIdx; - this.#rangeState.defScopeIdx = definitionIdx; + encodedDefSourceIdx = location.sourceIdx - this.#rangeState.defSourceIdx; + this.#rangeState.defSourceIdx = location.sourceIdx; + + encodedDefScopeIdx = encodedDefSourceIdx === 0 + ? location.scopeIdx - this.#rangeState.defScopeIdx + : location.scopeIdx; + this.#rangeState.defScopeIdx = location.scopeIdx; } if (range.isStackFrame) flags |= GeneratedRangeFlags.IS_STACK_FRAME; @@ -187,7 +203,16 @@ export class Encoder { this.#encodeTag(EncodedTag.GENERATED_RANGE_START).#encodeUnsigned(flags); if (encodedLine > 0) this.#encodeUnsigned(encodedLine); this.#encodeUnsigned(encodedColumn); - if (encodedDefinition !== undefined) this.#encodeSigned(encodedDefinition); + if ( + encodedDefSourceIdx !== undefined && encodedDefScopeIdx !== undefined + ) { + this.#encodeSigned(encodedDefSourceIdx); + if (encodedDefSourceIdx === 0) { + this.#encodeSigned(encodedDefScopeIdx); + } else { + this.#encodeUnsigned(encodedDefScopeIdx); + } + } this.#finishItem(); }