From f746f86fc8d1f70c320b0cf9b095eab8e8eec995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Z=C3=BCnd?= Date: Thu, 29 Jan 2026 07:30:51 +0100 Subject: [PATCH] feat: add 'hasVariableAndBindingInfo' hint when decoding --- src/decode/decode.test.ts | 68 ++++++++++++++++++++++++++++++++++++--- src/decode/decode.ts | 38 ++++++++++++++++------ src/mod.ts | 1 + src/roundtrip.test.ts | 3 +- src/scopes.ts | 13 +++++++- 5 files changed, 108 insertions(+), 15 deletions(-) diff --git a/src/decode/decode.test.ts b/src/decode/decode.test.ts index f6f166a..b555c81 100644 --- a/src/decode/decode.test.ts +++ b/src/decode/decode.test.ts @@ -6,8 +6,10 @@ import { describe, it } from "@std/testing/bdd"; import { ScopeInfoBuilder } from "../builder/builder.ts"; import { encode } from "../encode/encode.ts"; import { + assert, assertEquals, assertExists, + assertFalse, assertStrictEquals, assertThrows, } from "@std/assert"; @@ -75,7 +77,7 @@ describe("decode", () => { encodeUnsigned(256), ]; map.scopes = items.join(","); - assertEquals(decode(map), info); + assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false }); }); it("handles trailing VLQs in ORIGINAL_SCOPE_START items", () => { @@ -90,7 +92,7 @@ describe("decode", () => { parts[0] += encodeSigned(-16); map.scopes = parts.join(","); - assertEquals(decode(map), info); + assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false }); }); it("handles trailing VLQs in ORIGINAL_SCOPE_END items", () => { @@ -105,7 +107,7 @@ describe("decode", () => { parts[1] += encodeSigned(-16); map.scopes = parts.join(","); - assertEquals(decode(map), info); + assertEquals(decode(map), { ...info, hasVariableAndBindingInfo: false }); }); it("ignores wrong 'name' indices in lax mode", () => { @@ -567,6 +569,7 @@ describe("decode", () => { assertEquals(decode(map, { mode: DecodeMode.STRICT }), { scopes: [], ranges: [], + hasVariableAndBindingInfo: false, }); }); @@ -589,6 +592,63 @@ describe("decode", () => { encoder.finishItem(); const map = createMap(encoder.encode(), []); - assertEquals(decode(map), { scopes: [], ranges: [] }); + assertEquals(decode(map), { + scopes: [], + ranges: [], + hasVariableAndBindingInfo: false, + }); + }); + + describe("hasVariableAndBindingInfo", () => { + it("is 'false' when no variables/bindings are present", () => { + const map = encode( + new ScopeInfoBuilder().startScope(0, 0, { + isStackFrame: true, + key: "fn", + }).endScope(10, 0).startRange(0, 0, { + scopeKey: "fn", + isStackFrame: true, + }).endRange(0, 10).build(), + ); + + const { hasVariableAndBindingInfo } = decode(map); + + assertFalse(hasVariableAndBindingInfo); + }); + + it("is 'false' when only variables are present", () => { + const map = encode( + new ScopeInfoBuilder().startScope(0, 0, { + isStackFrame: true, + key: "fn", + variables: ["foo", "bar"], + }).endScope(10, 0).startRange(0, 0, { + scopeKey: "fn", + isStackFrame: true, + }).endRange(0, 10).build(), + ); + + const { hasVariableAndBindingInfo } = decode(map); + + assertFalse(hasVariableAndBindingInfo); + }); + + it("is 'true' when variables/bindings are present", () => { + const map = encode( + new ScopeInfoBuilder().startScope(0, 0, { + isStackFrame: true, + key: "fn", + variables: ["foo", "bar"], + }).endScope(10, 0).startRange(0, 0, { + scopeKey: "fn", + isStackFrame: true, + values: ["n", "m"], + }).endRange(0, 10).build(), + ); + + const { hasVariableAndBindingInfo } = decode(map); + + assert(hasVariableAndBindingInfo); + }); }); }); diff --git a/src/decode/decode.ts b/src/decode/decode.ts index 348ed6c..8121094 100644 --- a/src/decode/decode.ts +++ b/src/decode/decode.ts @@ -10,11 +10,11 @@ import { Tag, } from "../codec.ts"; import type { + DecodedScopeInfo, GeneratedRange, IndexSourceMapJson, OriginalScope, Position, - ScopeInfo, SourceMap, SourceMapJson, SubRangeBinding, @@ -60,7 +60,7 @@ export const DEFAULT_DECODE_OPTIONS: DecodeOptions = { export function decode( sourceMap: SourceMap, options: Partial = DEFAULT_DECODE_OPTIONS, -): ScopeInfo { +): DecodedScopeInfo { const opts = { ...DEFAULT_DECODE_OPTIONS, ...options }; if ("sections" in sourceMap) { return decodeIndexMap(sourceMap, { @@ -74,8 +74,10 @@ export function decode( function decodeMap( sourceMap: SourceMapJson, options: DecodeOptions, -): ScopeInfo { - if (!sourceMap.scopes || !sourceMap.names) return { scopes: [], ranges: [] }; +): DecodedScopeInfo { + if (!sourceMap.scopes || !sourceMap.names) { + return { scopes: [], ranges: [], hasVariableAndBindingInfo: false }; + } return new Decoder(sourceMap.scopes, sourceMap.names, options).decode(); } @@ -83,16 +85,21 @@ function decodeMap( function decodeIndexMap( sourceMap: IndexSourceMapJson, options: DecodeOptions, -): ScopeInfo { - const scopeInfo: ScopeInfo = { scopes: [], ranges: [] }; +): DecodedScopeInfo { + const scopeInfo: DecodedScopeInfo = { + scopes: [], + ranges: [], + hasVariableAndBindingInfo: false, + }; for (const section of sourceMap.sections) { - const { scopes, ranges } = decode(section.map, { + const { scopes, ranges, hasVariableAndBindingInfo } = decode(section.map, { ...options, generatedOffset: section.offset, }); for (const scope of scopes) scopeInfo.scopes.push(scope); for (const range of ranges) scopeInfo.ranges.push(range); + scopeInfo.hasVariableAndBindingInfo ||= hasVariableAndBindingInfo; } return scopeInfo; @@ -132,6 +139,9 @@ class Decoder { Map >(); + #seenOriginalScopeVariables = false; + #seenGeneratedRangeBindings = false; + constructor(scopes: string, names: string[], options: DecodeOptions) { this.#encodedScopes = scopes; this.#names = names; @@ -140,7 +150,7 @@ class Decoder { this.#rangeState.column = options.generatedOffset.column; } - decode(): ScopeInfo { + decode(): DecodedScopeInfo { const iter = new TokenIterator(this.#encodedScopes); while (iter.hasNext()) { @@ -175,6 +185,7 @@ class Decoder { } this.#handleOriginalScopeVariablesItem(variableIdxs); + this.#seenOriginalScopeVariables = true; break; } case Tag.ORIGINAL_SCOPE_END: { @@ -224,6 +235,7 @@ class Decoder { } this.#handleGeneratedRangeBindingsItem(valueIdxs); + this.#seenGeneratedRangeBindings = true; break; } case Tag.GENERATED_RANGE_SUBRANGE_BINDING: { @@ -239,6 +251,7 @@ class Decoder { } this.#recordGeneratedSubRangeBindingItem(variableIndex, bindings); + this.#seenGeneratedRangeBindings = true; break; } case Tag.GENERATED_RANGE_CALL_SITE: { @@ -275,11 +288,18 @@ class Decoder { ); } - const info = { scopes: this.#scopes, ranges: this.#ranges }; + const info = { + scopes: this.#scopes, + ranges: this.#ranges, + hasVariableAndBindingInfo: this.#seenOriginalScopeVariables && + this.#seenGeneratedRangeBindings, + }; this.#scopes = []; this.#ranges = []; this.#flatOriginalScopes = []; + this.#seenOriginalScopeVariables = false; + this.#seenGeneratedRangeBindings = false; return info; } diff --git a/src/mod.ts b/src/mod.ts index 71e4293..99227bb 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -4,6 +4,7 @@ export type { Binding, + DecodedScopeInfo, GeneratedRange, OriginalPosition, OriginalScope, diff --git a/src/roundtrip.test.ts b/src/roundtrip.test.ts index f062ab1..a3eafa4 100644 --- a/src/roundtrip.test.ts +++ b/src/roundtrip.test.ts @@ -15,7 +15,8 @@ import { assertEquals } from "@std/assert"; import { ScopeInfoBuilder } from "./builder/builder.ts"; function assertCodec(scopesInfo: ScopeInfo): void { - assertEquals(decode(encode(scopesInfo)), scopesInfo); + const { scopes, ranges } = decode(encode(scopesInfo)); + assertEquals({ scopes, ranges }, scopesInfo); } describe("round trip", () => { diff --git a/src/scopes.ts b/src/scopes.ts index 1a23e86..5e5aee4 100644 --- a/src/scopes.ts +++ b/src/scopes.ts @@ -3,7 +3,7 @@ // found in the LICENSE file. /** - * The decoded scopes information found in a source map. + * The scopes information found in a source map. */ export interface ScopeInfo { /** @@ -17,6 +17,17 @@ export interface ScopeInfo { ranges: GeneratedRange[]; } +/** + * The scopes information produced by the decoder. + */ +export interface DecodedScopeInfo extends ScopeInfo { + /** + * When the encoded scopes contained variable and binding expression items. + * Note that a value of `true` also indicates "partial" info and does not guarantee comprehensiveness. + */ + hasVariableAndBindingInfo: boolean; +} + /** * A scope in the authored source. */