diff --git a/README.md b/README.md index e5d1749..ac8296b 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,14 @@ The library also contains a builder that makes creating structured scope informa import { ScopeInfoBuilder } from "@chrome-devtools/source-map-scopes-codec"; const scopeInformation = new ScopeInfoBuilder() - .startScope(0, 0, { kind: "Global" }) - .startScope(5, 10) - .setScopeKind("Function") // Same as passing 'kind' to 'startScope'. - .setScopeName("foo") // Same as passing 'name' to 'startScope'. - .endScope(10, 5) - .endScope(11, 1) + .startSource() + .startScope(0, 0, { kind: "Global" }) + .startScope(5, 10) + .setScopeKind("Function") // Same as passing 'kind' to 'startScope'. + .setScopeName("foo") // Same as passing 'name' to 'startScope'. + .endScope(10, 5) + .endScope(11, 1) + .endSource() .startRange(0, 0, { scope: 0 }) .startRange(0, 10) .setRangeScopeDefinition(1) // Same as passing 'scope' to 'startRange'. diff --git a/src/builder/builder.test.ts b/src/builder/builder.test.ts index d0ccd43..6347a06 100644 --- a/src/builder/builder.test.ts +++ b/src/builder/builder.test.ts @@ -17,79 +17,105 @@ describe("ScopeInfoBuilder", () => { builder = new ScopeInfoBuilder(); }); - it("adds null OriginalScopes", () => { - const info = builder.addNullScope().addNullScope().build(); + it("adds null sources", () => { + const info = builder.addNullSource().addNullSource().build(); assertEquals(info.scopes, [null, null]); }); it("builds simple OriginalScopes", () => { - const info = builder.startScope(0, 0).endScope(5, 10).build(); + const info = builder.startSource().startScope(0, 0).endScope(5, 10) + .endSource().build(); - assertEquals(info.scopes[0]?.start, { line: 0, column: 0 }); - assertEquals(info.scopes[0]?.end, { line: 5, column: 10 }); + assertEquals(info.scopes[0]?.[0]?.start, { line: 0, column: 0 }); + assertEquals(info.scopes[0]?.[0]?.end, { line: 5, column: 10 }); }); - it("builds a simple nested OriginalScope", () => { - const info = builder.startScope(0, 0).startScope(5, 0).endScope(10, 0) - .endScope(15, 0).build(); - - assertStrictEquals(info.scopes[0]?.children.length, 1); - assertEquals(info.scopes[0].children[0].start, { line: 5, column: 0 }); - assertEquals(info.scopes[0].children[0].end, { line: 10, column: 0 }); + it("builds multiple root OriginalScopes in a single source", () => { + const info = builder.startSource() + .startScope(0, 0).endScope(5, 10) + .startScope(6, 0).endScope(10, 5) + .endSource().build(); + + assertStrictEquals(info.scopes[0]?.length, 2); + assertEquals(info.scopes[0][0].start, { line: 0, column: 0 }); + assertEquals(info.scopes[0][0].end, { line: 5, column: 10 }); + assertEquals(info.scopes[0][1].start, { line: 6, column: 0 }); + assertEquals(info.scopes[0][1].end, { line: 10, column: 5 }); + }); - assertStrictEquals(info.scopes[0], info.scopes[0].children[0].parent); + it("builds a simple nested OriginalScope", () => { + const info = builder.startSource().startScope(0, 0).startScope(5, 0) + .endScope(10, 0) + .endScope(15, 0).endSource().build(); + + assertStrictEquals(info.scopes[0]?.[0]?.children.length, 1); + assertEquals(info.scopes[0][0].children[0].start, { line: 5, column: 0 }); + assertEquals(info.scopes[0][0].children[0].end, { line: 10, column: 0 }); + + assertStrictEquals( + info.scopes[0][0], + info.scopes[0][0].children[0].parent, + ); }); describe("startScope", () => { it("can set the name via option", () => { - const info = builder.startScope(0, 0, { name: "foo" }).endScope(5, 0) - .build(); + const info = builder.startSource().startScope(0, 0, { name: "foo" }) + .endScope(5, 0) + .endSource().build(); - assertStrictEquals(info.scopes[0]?.name, "foo"); + assertStrictEquals(info.scopes[0]?.[0]?.name, "foo"); }); it("can set kind via option", () => { - const info = builder.startScope(0, 0, { kind: "Global" }).endScope(10, 0) - .build(); + const info = builder.startSource().startScope(0, 0, { kind: "Global" }) + .endScope(10, 0) + .endSource().build(); - assertStrictEquals(info.scopes[0]?.kind, "Global"); + assertStrictEquals(info.scopes[0]?.[0]?.kind, "Global"); }); it("can set isStackFrame via option", () => { - const info = builder.startScope(0, 0, { isStackFrame: true }).endScope( + const info = builder.startSource().startScope(0, 0, { + isStackFrame: true, + }).endScope( 10, 0, - ).build(); + ).endSource().build(); - assertStrictEquals(info.scopes[0]?.isStackFrame, true); + assertStrictEquals(info.scopes[0]?.[0]?.isStackFrame, true); }); it("can set variables via option", () => { - const info = builder.startScope(0, 0, { variables: ["a", "b"] }).endScope( + const info = builder.startSource().startScope(0, 0, { + variables: ["a", "b"], + }).endScope( 10, 0, - ).build(); + ).endSource().build(); - assertEquals(info.scopes[0]?.variables, ["a", "b"]); + assertEquals(info.scopes[0]?.[0]?.variables, ["a", "b"]); }); it("copies the variables passed via options", () => { const variables = ["a", "b"]; - const info = builder.startScope(0, 0, { variables }).endScope(10, 0) - .build(); + const info = builder.startSource().startScope(0, 0, { variables }) + .endScope(10, 0) + .endSource().build(); variables.push("c"); - assertEquals(info.scopes[0]?.variables, ["a", "b"]); + assertEquals(info.scopes[0]?.[0]?.variables, ["a", "b"]); }); }); describe("setScopeName", () => { it("sets the name", () => { - const info = builder.startScope(0, 0).setScopeName("foo").endScope(5, 0) - .build(); + const info = builder.startSource().startScope(0, 0).setScopeName("foo") + .endScope(5, 0) + .endSource().build(); - assertStrictEquals(info.scopes[0]?.name, "foo"); + assertStrictEquals(info.scopes[0]?.[0]?.name, "foo"); }); it("does nothing when no scope is open", () => { @@ -99,12 +125,13 @@ describe("ScopeInfoBuilder", () => { describe("setScopeKind", () => { it("sets the kind", () => { - const info = builder.startScope(0, 0).setScopeKind("Global").endScope( - 20, - 0, - ).build(); + const info = builder.startSource().startScope(0, 0).setScopeKind("Global") + .endScope( + 20, + 0, + ).endSource().build(); - assertStrictEquals(info.scopes[0]?.kind, "Global"); + assertStrictEquals(info.scopes[0]?.[0]?.kind, "Global"); }); it("does nothing when no scope is open", () => { @@ -114,31 +141,38 @@ describe("ScopeInfoBuilder", () => { describe("setScopeStackFrame", () => { it("sets the isStackFrame flag", () => { - const info = builder.startScope(0, 0).setScopeStackFrame(true).endScope( + const info = builder.startSource().startScope(0, 0).setScopeStackFrame( + true, + ).endScope( 10, 0, ) - .build(); + .endSource().build(); - assertStrictEquals(info.scopes[0]?.isStackFrame, true); + assertStrictEquals(info.scopes[0]?.[0]?.isStackFrame, true); }); }); describe("setScopeVariables", () => { it("sets variables", () => { - const info = builder.startScope(0, 0).setScopeVariables(["a", "b"]) - .endScope(10, 0).build(); + const info = builder.startSource().startScope(0, 0).setScopeVariables([ + "a", + "b", + ]) + .endScope(10, 0).endSource().build(); - assertEquals(info.scopes[0]?.variables, ["a", "b"]); + assertEquals(info.scopes[0]?.[0]?.variables, ["a", "b"]); }); it("creates a copy of the variables", () => { const variables = ["a", "b"]; - const info = builder.startScope(0, 0).setScopeVariables(variables) - .endScope(10, 0).build(); + const info = builder.startSource().startScope(0, 0).setScopeVariables( + variables, + ) + .endScope(10, 0).endSource().build(); variables.push("c"); - assertEquals(info.scopes[0]?.variables, ["a", "b"]); + assertEquals(info.scopes[0]?.[0]?.variables, ["a", "b"]); }); }); @@ -168,20 +202,24 @@ describe("ScopeInfoBuilder", () => { describe("startRange", () => { it("sets the definition scope when it's provided as a number", () => { - const info = builder.startScope(0, 0, { key: 0 }).endScope(10, 0) + const info = builder.startSource().startScope(0, 0, { key: 0 }).endScope( + 10, + 0, + ).endSource() .startRange(0, 0, { scopeKey: 0, }).endRange(0, 10).build(); - assertStrictEquals(info.scopes[0], info.ranges[0].originalScope); + assertStrictEquals(info.scopes[0]?.[0], info.ranges[0].originalScope); }); it("sets the definition scope when it's provided directly", () => { - const scope = builder.startScope(0, 0).endScope(10, 0).lastScope(); + const scope = builder.startSource().startScope(0, 0).endScope(10, 0) + .endSource().lastScope(); const info = builder.startRange(0, 0, { scope: scope! }).endRange(0, 10) .build(); - assertStrictEquals(info.scopes[0], info.ranges[0].originalScope); + assertStrictEquals(info.scopes[0]?.[0], info.ranges[0].originalScope); assertStrictEquals(info.ranges[0].originalScope, scope); }); @@ -225,27 +263,30 @@ describe("ScopeInfoBuilder", () => { describe("setRangeDefinitionScope", () => { it("sets the definition scope when it's provided directly", () => { - const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0).endScope(10, 0) + .endSource().lastScope()!; const info = builder.startRange(0, 0).setRangeDefinitionScope(scope) .endRange(0, 10).build(); - assertStrictEquals(info.scopes[0], info.ranges[0].originalScope); + assertStrictEquals(info.scopes[0]?.[0], info.ranges[0].originalScope); assertStrictEquals(info.ranges[0].originalScope, scope); }); it("does nothing when no range is on the stack", () => { - const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0).endScope(10, 0) + .endSource().lastScope()!; builder.setRangeDefinitionScope(scope); }); }); describe("setRangeDefinitionScopeKey", () => { it("sets the definition scope when it's provided directly", () => { - builder.startScope(0, 0, { key: "my key" }).endScope(10, 0); + builder.startSource().startScope(0, 0, { key: "my key" }).endScope(10, 0) + .endSource(); const info = builder.startRange(0, 0).setRangeDefinitionScopeKey("my key") .endRange(0, 10).build(); - assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]); + assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]?.[0]); }); it("does nothing when no range is on the stack", () => { @@ -361,17 +402,21 @@ describe("ScopeInfoBuilder", () => { describe("scope key", () => { it("can set the scope key via options", () => { - builder.startScope(0, 0, { key: "my custom key" }).endScope(10, 0); + builder.startSource().startScope(0, 0, { key: "my custom key" }).endScope( + 10, + 0, + ).endSource(); builder.startRange(0, 0, { scopeKey: "my custom key" }).endRange(0, 10); const info = builder.build(); - assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]); + assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]?.[0]); }); }); describe("build", () => { it("resets accumulated state so a reused builder does not leak across builds", () => { - builder.startScope(0, 0, { key: 0 }).endScope(10, 0); + builder.startSource().startScope(0, 0, { key: 0 }).endScope(10, 0) + .endSource(); builder.startRange(0, 0, { scopeKey: 0 }).endRange(0, 10); builder.build(); diff --git a/src/builder/builder.ts b/src/builder/builder.ts index 4647194..d5120fb 100644 --- a/src/builder/builder.ts +++ b/src/builder/builder.ts @@ -21,9 +21,10 @@ import type { * `end*` without a matching `start*`. */ export class ScopeInfoBuilder { - #scopes: (OriginalScope | null)[] = []; + #scopes: (OriginalScope[] | null)[] = []; #ranges: GeneratedRange[] = []; + #currentSourceScopes: OriginalScope[] | null = null; #scopeStack: OriginalScope[] = []; #rangeStack: GeneratedRange[] = []; @@ -31,11 +32,24 @@ export class ScopeInfoBuilder { #keyToScope = new Map(); #lastScope: OriginalScope | null = null; - addNullScope(): this { + addNullSource(): this { this.#scopes.push(null); return this; } + startSource(): this { + this.#currentSourceScopes = []; + return this; + } + + endSource(): this { + if (this.#currentSourceScopes) { + this.#scopes.push(this.#currentSourceScopes); + this.#currentSourceScopes = null; + } + return this; + } + startScope( line: number, column: number, @@ -100,7 +114,7 @@ export class ScopeInfoBuilder { scope.end = { line, column }; if (this.#scopeStack.length === 0) { - this.#scopes.push(scope); + this.#currentSourceScopes?.push(scope); } else { this.#scopeStack.at(-1)!.children.push(scope); } @@ -227,6 +241,7 @@ export class ScopeInfoBuilder { this.#scopes = []; this.#ranges = []; + this.#currentSourceScopes = null; this.#knownScopes.clear(); this.#keyToScope.clear(); this.#lastScope = null; @@ -234,6 +249,10 @@ export class ScopeInfoBuilder { return info; } + protected get currentSourceScopes(): ReadonlyArray | null { + return this.#currentSourceScopes; + } + protected get scopeStack(): ReadonlyArray { return this.#scopeStack; } diff --git a/src/builder/safe_builder.test.ts b/src/builder/safe_builder.test.ts index 6d9fecb..1bfe319 100644 --- a/src/builder/safe_builder.test.ts +++ b/src/builder/safe_builder.test.ts @@ -13,8 +13,14 @@ describe("SafeScopeInfoBuilder", () => { builder = new SafeScopeInfoBuilder(); }); + it("throws when trying to build the info without closing a source", () => { + builder.startSource(); + + assertThrows(() => builder.build()); + }); + it("throws when trying to build the info without closing OriginalScopes", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.build()); }); @@ -25,19 +31,47 @@ describe("SafeScopeInfoBuilder", () => { assertThrows(() => builder.build()); }); - it("throws when trying to add a null scope with open OriginalScopes", () => { - builder.startScope(0, 0); + it("throws when trying to add a null source with an open source", () => { + builder.startSource(); + + assertThrows(() => builder.addNullSource()); + }); + + it("throws when trying to add a null source with open OriginalScopes", () => { + builder.startSource().startScope(0, 0); - assertThrows(() => builder.addNullScope()); + assertThrows(() => builder.addNullSource()); }); - it("throws when trying t add a null scope with open GeneratedRanges", () => { + it("throws when trying to add a null source with open GeneratedRanges", () => { builder.startRange(0, 0); - assertThrows(() => builder.addNullScope()); + assertThrows(() => builder.addNullSource()); + }); + + describe("startSource / endSource", () => { + it("throws when trying to start a source while another source is open", () => { + builder.startSource(); + + assertThrows(() => builder.startSource()); + }); + + it("throws when trying to end a source when no source is open", () => { + assertThrows(() => builder.endSource()); + }); + + it("throws when trying to end a source while a scope is open", () => { + builder.startSource().startScope(0, 0); + + assertThrows(() => builder.endSource()); + }); }); describe("startScope", () => { + it("throws when trying to start a scope without an open source", () => { + assertThrows(() => builder.startScope(0, 0)); + }); + it("throws when trying to start a scope while building a range", () => { builder.startRange(0, 0); @@ -45,19 +79,25 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws when trying to start a scope that precedes the current scope", () => { - builder.startScope(10, 0); + builder.startSource().startScope(10, 0); assertThrows(() => builder.startScope(5, 0)); }); it("throws when trying to start a scope that overlaps with the preceding sibling scope", () => { - builder.startScope(0, 0).startScope(5, 0).endScope(10, 0); + builder.startSource().startScope(0, 0).startScope(5, 0).endScope(10, 0); + + assertThrows(() => builder.startScope(7, 0)); + }); + + it("throws when trying to start a root scope that overlaps with the preceding root scope in the same source", () => { + builder.startSource().startScope(0, 0).endScope(10, 0); assertThrows(() => builder.startScope(7, 0)); }); it("allows starting a scope on the preceding scope' end", () => { - builder.startScope(0, 0).endScope(10, 5); + builder.startSource().startScope(0, 0).endScope(10, 5); builder.startScope(10, 5); }); @@ -117,21 +157,27 @@ describe("SafeScopeInfoBuilder", () => { }); it("allows scopes with zero length", () => { - builder.startScope(10, 0); + builder.startSource().startScope(10, 0); builder.endScope(10, 0); }); it("throws when scope end precedes scope start", () => { - builder.startScope(10, 0); + builder.startSource().startScope(10, 0); assertThrows(() => builder.endScope(5, 0)); }); }); describe("startRange", () => { + it("throws when trying to start a range while a source is open", () => { + builder.startSource(); + + assertThrows(() => builder.startRange(0, 0)); + }); + it("throws when trying to start a range while building a scope", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.startRange(0, 0)); }); @@ -177,8 +223,10 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws when 'values' length does not match OriginalScope.variables length (via scope)", () => { - const scope = builder.startScope(0, 0, { variables: ["foo", "bar"] }) - .endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0, { + variables: ["foo", "bar"], + }) + .endScope(10, 0).endSource().lastScope()!; assertThrows(() => builder.startRange(0, 0, { scope, values: ["a", null, "b"] }) @@ -186,8 +234,11 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws when 'values' length does not match OriginalScope.variables length (via scopeKey)", () => { - builder.startScope(0, 0, { variables: ["foo", "bar"], key: "my key" }) - .endScope(10, 0); + builder.startSource().startScope(0, 0, { + variables: ["foo", "bar"], + key: "my key", + }) + .endScope(10, 0).endSource(); assertThrows(() => builder.startRange(0, 0, { @@ -200,13 +251,14 @@ describe("SafeScopeInfoBuilder", () => { describe("setRangeDefinitionScope", () => { it("throws when no range is open", () => { - const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0).endScope(10, 0) + .endSource().lastScope()!; assertThrows(() => builder.setRangeDefinitionScope(scope)); }); it("throws while building a scope", () => { - const scope = builder.startScope(0, 0).currentScope()!; + const scope = builder.startSource().startScope(0, 0).currentScope()!; assertThrows(() => builder.setRangeDefinitionScope(scope)); }); @@ -230,7 +282,7 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws while building a scope", () => { - builder.startScope(0, 0, { key: "my key" }); + builder.startSource().startScope(0, 0, { key: "my key" }); assertThrows(() => builder.setRangeDefinitionScopeKey("my key")); }); @@ -254,7 +306,7 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws while building a scope", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.setRangeStackFrame(true)); }); @@ -266,7 +318,7 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws while building a scope", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.setRangeHidden(true)); }); @@ -278,7 +330,7 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws while building a scope", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.setRangeValues(["a", null])); }); @@ -290,8 +342,10 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws when 'values' length does not match OriginalScope.variables length (via scope)", () => { - const scope = builder.startScope(0, 0, { variables: ["foo", "bar"] }) - .endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0, { + variables: ["foo", "bar"], + }) + .endScope(10, 0).endSource().lastScope()!; builder.startRange(0, 0, { scope }); assertThrows(() => builder.setRangeValues(["a", null, "b"])); @@ -310,7 +364,7 @@ describe("SafeScopeInfoBuilder", () => { }); it("throws while building a scope", () => { - builder.startScope(0, 0); + builder.startSource().startScope(0, 0); assertThrows(() => builder.setRangeCallSite({ @@ -341,8 +395,10 @@ describe("SafeScopeInfoBuilder", () => { describe("sub-range bindings", () => { beforeEach(() => { - builder.startScope(0, 0, { key: "test-scope", variables: ["foo"] }) - .endScope(20, 0); + builder.startSource() + .startScope(0, 0, { key: "test-scope", variables: ["foo"] }) + .endScope(20, 0) + .endSource(); }); it("allows empty sub-range bindings", () => { diff --git a/src/builder/safe_builder.ts b/src/builder/safe_builder.ts index 6ced411..840ed0b 100644 --- a/src/builder/safe_builder.ts +++ b/src/builder/safe_builder.ts @@ -17,11 +17,30 @@ import { ScopeInfoBuilder, type ScopeKey } from "./builder.ts"; * nested and don't partially overlap. */ export class SafeScopeInfoBuilder extends ScopeInfoBuilder { - override addNullScope(): this { - this.#verifyEmptyScopeStack("add null scope"); - this.#verifyEmptyRangeStack("add null scope"); + override addNullSource(): this { + this.#verifyNoOpenSource("add null source"); + this.#verifyEmptyScopeStack("add null source"); + this.#verifyEmptyRangeStack("add null source"); - super.addNullScope(); + super.addNullSource(); + return this; + } + + override startSource(): this { + this.#verifyNoOpenSource("start source"); + this.#verifyEmptyScopeStack("start source"); + this.#verifyEmptyRangeStack("start source"); + + super.startSource(); + return this; + } + + override endSource(): this { + this.#verifyOpenSource("end source"); + this.#verifyEmptyScopeStack("end source"); + this.#verifyEmptyRangeStack("end source"); + + super.endSource(); return this; } @@ -36,6 +55,7 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder { key?: ScopeKey; }, ): this { + this.#verifyOpenSource("start scope"); this.#verifyEmptyRangeStack("start scope"); const parent = this.scopeStack.at(-1); @@ -46,7 +66,9 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder { ); } - const precedingSibling = parent?.children.at(-1); + const precedingSibling = parent + ? parent.children.at(-1) + : this.currentSourceScopes?.at(-1); if ( precedingSibling && comparePositions(precedingSibling.end, { line, column }) > 0 @@ -124,6 +146,7 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder { callSite?: OriginalPosition; }, ): this { + this.#verifyNoOpenSource("startRange"); this.#verifyEmptyScopeStack("starRange"); const parent = this.rangeStack.at(-1); @@ -280,11 +303,24 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder { "Can't build ScopeInfo while an OriginalScope is unclosed.", ); } + this.#verifyNoOpenSource("build ScopeInfo"); this.#verifyEmptyRangeStack("build ScopeInfo"); return super.build(); } + #verifyNoOpenSource(op: string): void { + if (this.currentSourceScopes !== null) { + throw new Error(`Can't ${op} while a source is unclosed.`); + } + } + + #verifyOpenSource(op: string): void { + if (this.currentSourceScopes === null) { + throw new Error(`Can't ${op} without an open source.`); + } + } + #verifyEmptyScopeStack(op: string): void { if (this.scopeStack.length > 0) { throw new Error(`Can't ${op} while a OriginalScope is unclosed.`); diff --git a/src/decode/decode.test.ts b/src/decode/decode.test.ts index 1a6ba0a..a2a8421 100644 --- a/src/decode/decode.test.ts +++ b/src/decode/decode.test.ts @@ -62,7 +62,10 @@ function createMap(scopes: string, names: string[]): SourceMapJson { describe("decode", () => { it("handles unknown items interspersed in an known items", () => { - const info = new ScopeInfoBuilder().startScope(0, 0).endScope(10, 0) + const info = new ScopeInfoBuilder().startSource().startScope(0, 0).endScope( + 10, + 0, + ).endSource() .build(); const map = encode(info); @@ -81,7 +84,10 @@ describe("decode", () => { }); it("handles trailing VLQs in ORIGINAL_SCOPE_START items", () => { - const info = new ScopeInfoBuilder().startScope(0, 0).endScope(10, 0) + const info = new ScopeInfoBuilder().startSource().startScope(0, 0).endScope( + 10, + 0, + ).endSource() .build(); const map = encode(info); @@ -96,7 +102,10 @@ describe("decode", () => { }); it("handles trailing VLQs in ORIGINAL_SCOPE_END items", () => { - const info = new ScopeInfoBuilder().startScope(0, 0).endScope(10, 0) + const info = new ScopeInfoBuilder().startSource().startScope(0, 0).endScope( + 10, + 0, + ).endSource() .build(); const map = encode(info); @@ -123,8 +132,8 @@ describe("decode", () => { const info = decode(map); - assertExists(info.scopes[0]); - assertStrictEquals(info.scopes[0].name, ""); + assertExists(info.scopes[0]?.[0]); + assertStrictEquals(info.scopes[0][0].name, ""); }); it("ignores wrong 'kind' indices in lax mode", () => { @@ -140,8 +149,8 @@ describe("decode", () => { const info = decode(map); - assertExists(info.scopes[0]); - assertStrictEquals(info.scopes[0].kind, ""); + assertExists(info.scopes[0]?.[0]); + assertStrictEquals(info.scopes[0][0].kind, ""); }); it("throws in strict mode when the scopes string ends mid-VLQ", () => { @@ -321,7 +330,7 @@ describe("decode", () => { const info = decode(map, { mode: DecodeMode.LAX }); - assertEquals(info.scopes[0]?.variables, ["foo", ""]); + assertEquals(info.scopes[0]?.[0]?.variables, ["foo", ""]); }); it("ignores if ORIGINAL_SCOPE_VARIABLES indices are out-of-bounds (lower) in lax mode", () => { @@ -334,7 +343,7 @@ describe("decode", () => { const info = decode(map, { mode: DecodeMode.LAX }); - assertEquals(info.scopes[0]?.variables, ["foo", ""]); + assertEquals(info.scopes[0]?.[0]?.variables, ["foo", ""]); }); it("throws if ORIGINAL_SCOPE_START.name is out-of-bounds in strict mode", () => { @@ -623,10 +632,10 @@ describe("decode", () => { describe("hasVariableAndBindingInfo", () => { it("is 'false' when no variables/bindings are present", () => { const map = encode( - new ScopeInfoBuilder().startScope(0, 0, { + new ScopeInfoBuilder().startSource().startScope(0, 0, { isStackFrame: true, key: "fn", - }).endScope(10, 0).startRange(0, 0, { + }).endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "fn", isStackFrame: true, }).endRange(0, 10).build(), @@ -639,11 +648,11 @@ describe("decode", () => { it("is 'false' when only variables are present", () => { const map = encode( - new ScopeInfoBuilder().startScope(0, 0, { + new ScopeInfoBuilder().startSource().startScope(0, 0, { isStackFrame: true, key: "fn", variables: ["foo", "bar"], - }).endScope(10, 0).startRange(0, 0, { + }).endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "fn", isStackFrame: true, }).endRange(0, 10).build(), @@ -656,11 +665,11 @@ describe("decode", () => { it("is 'true' when variables/bindings are present", () => { const map = encode( - new ScopeInfoBuilder().startScope(0, 0, { + new ScopeInfoBuilder().startSource().startScope(0, 0, { isStackFrame: true, key: "fn", variables: ["foo", "bar"], - }).endScope(10, 0).startRange(0, 0, { + }).endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "fn", isStackFrame: true, values: ["n", "m"], diff --git a/src/decode/decode.ts b/src/decode/decode.ts index bf477cb..6339401 100644 --- a/src/decode/decode.ts +++ b/src/decode/decode.ts @@ -124,7 +124,7 @@ class Decoder { readonly #names: string[]; readonly #mode: DecodeMode; - #scopes: (OriginalScope | null)[] = []; + #scopes: (OriginalScope[] | null)[] = []; #ranges: GeneratedRange[] = []; readonly #scopeState = { ...DEFAULT_SCOPE_STATE }; @@ -384,7 +384,7 @@ class Decoder { scope.parent = parent; parent.children.push(scope); } else { - this.#scopes.push(scope); + this.#scopes.push([scope]); this.#scopeState.line = 0; this.#scopeState.column = 0; } diff --git a/src/encode/encode.test.ts b/src/encode/encode.test.ts index 610569e..73fd2e4 100644 --- a/src/encode/encode.test.ts +++ b/src/encode/encode.test.ts @@ -45,21 +45,24 @@ describe("encode", () => { assertStrictEquals(encode(info, map), map); }); - it("encodes null OriginalScopes correctly", () => { - const info = builder.addNullScope().addNullScope().addNullScope().build(); + it("encodes null sources correctly", () => { + const info = builder.addNullSource().addNullSource().addNullSource() + .build(); assertStrictEquals(encode(info).scopes, "A,A,A"); }); it("throws when a child scope' start is not nested properly within its parent", () => { - const info = builder.startScope(10, 0).startScope(0, 0).endScope(20, 0) - .endScope(30, 0).build(); + const info = builder.startSource().startScope(10, 0).startScope(0, 0) + .endScope(20, 0) + .endScope(30, 0).endSource().build(); assertThrows(() => encode(info)); }); it("throws when a scopes' end precedes the scopes' start", () => { - const info = builder.startScope(10, 0).endScope(0, 0).build(); + const info = builder.startSource().startScope(10, 0).endScope(0, 0) + .endSource().build(); assertThrows(() => encode(info)); }); @@ -78,7 +81,8 @@ describe("encode", () => { }); it("throws when a ranges' definition scope is not known to the encoder", () => { - const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!; + const scope = builder.startSource().startScope(0, 0).endScope(10, 0) + .endSource().lastScope()!; const info = builder.startRange(0, 10).endRange(0, 20).build(); // Set the range's definition as a copy of `scope`. @@ -97,10 +101,10 @@ describe("encode", () => { }); it("throws when range bindings don't match with scope variables", () => { - const info = builder.startScope(0, 0, { + const info = builder.startSource().startScope(0, 0, { key: "key", variables: ["foo", "bar"], - }).endScope(10, 0).startRange(0, 0, { + }).endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "key", values: ["a", null, "b"], }).endRange(0, 10).build(); @@ -109,8 +113,11 @@ describe("encode", () => { }); it("throws when sub-range bindings are not sorted", () => { - const info = builder.startScope(0, 0, { key: "key", variables: ["a"] }) - .endScope(10, 0).startRange(0, 0, { + const info = builder.startSource().startScope(0, 0, { + key: "key", + variables: ["a"], + }) + .endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "key", values: [[{ from: { line: 5, column: 0 }, @@ -125,8 +132,11 @@ describe("encode", () => { }); it("throws when sub-range bindings have a gap", () => { - const info = builder.startScope(0, 0, { key: "key", variables: ["a"] }) - .endScope(10, 0).startRange(0, 0, { + const info = builder.startSource().startScope(0, 0, { + key: "key", + variables: ["a"], + }) + .endScope(10, 0).endSource().startRange(0, 0, { scopeKey: "key", values: [[{ from: { line: 0, column: 0 }, diff --git a/src/encode/encoder.ts b/src/encode/encoder.ts index a0c2010..30e6397 100644 --- a/src/encode/encoder.ts +++ b/src/encode/encoder.ts @@ -52,10 +52,14 @@ export class Encoder { encode(): string { this.#encodedItems = []; - this.#info.scopes.forEach((scope) => { + this.#info.scopes.forEach((scopes) => { + if (scopes === null) { + this.#encodedItems.push(EncodedTag.EMPTY); + return; + } this.#scopeState.line = 0; this.#scopeState.column = 0; - this.#encodeOriginalScope(scope); + scopes.forEach((scope) => this.#encodeOriginalScope(scope)); }); this.#info.ranges.forEach((range) => { this.#encodeGeneratedRange(range); @@ -64,12 +68,7 @@ export class Encoder { return this.#encodedItems.join(","); } - #encodeOriginalScope(scope: OriginalScope | null): void { - if (scope === null) { - this.#encodedItems.push(EncodedTag.EMPTY); - return; - } - + #encodeOriginalScope(scope: OriginalScope): void { this.#encodeOriginalScopeStart(scope); this.#encodeOriginalScopeVariables(scope); scope.children.forEach((child) => this.#encodeOriginalScope(child)); diff --git a/src/roundtrip.test.ts b/src/roundtrip.test.ts index a3eafa4..f65f351 100644 --- a/src/roundtrip.test.ts +++ b/src/roundtrip.test.ts @@ -26,76 +26,96 @@ describe("round trip", () => { builder = new ScopeInfoBuilder(); }); - it("handles null OriginalScopes", () => { - builder.addNullScope().addNullScope().addNullScope(); + it("handles null sources", () => { + builder.addNullSource().addNullSource().addNullSource(); assertCodec(builder.build()); }); it("handles a single top-level OriginalScope", () => { - builder.startScope(0, 0).endScope(10, 1); + builder.startSource().startScope(0, 0).endScope(10, 1).endSource(); assertCodec(builder.build()); }); it("handles two simple top-level OriginalScopes", () => { - builder.startScope(0, 0).endScope(10, 1); - builder.startScope(0, 0).endScope(15, 1); + builder.startSource().startScope(0, 0).endScope(10, 1).endSource(); + builder.startSource().startScope(0, 0).endScope(15, 1).endSource(); assertCodec(builder.build()); }); it("handles a simple nested OriginalScope", () => { - builder.startScope(0, 0).startScope(5, 1).endScope(10, 1).endScope(15, 0); + builder.startSource().startScope(0, 0).startScope(5, 1).endScope(10, 1) + .endScope(15, 0).endSource(); assertCodec(builder.build()); }); it("handles multiple children of a top-level scope", () => { - builder.startScope(0, 0).startScope(5, 1).endScope(10, 1).startScope(15, 0) - .endScope(20, 0).endScope(25, 1); + builder.startSource().startScope(0, 0).startScope(5, 1).endScope(10, 1) + .startScope(15, 0) + .endScope(20, 0).endScope(25, 1).endSource(); assertCodec(builder.build()); }); it("handles scopes that start on the same line", () => { - builder.startScope(0, 5).startScope(0, 10).endScope(10, 5).endScope(10, 10); + builder.startSource().startScope(0, 5).startScope(0, 10).endScope(10, 5) + .endScope(10, 10).endSource(); assertCodec(builder.build()); }); it("handles scope names", () => { - builder.startScope(0, 0, { name: "foo" }).startScope(10, 0, { name: "bar" }) - .endScope(20, 0).endScope(30, 0); + builder.startSource().startScope(0, 0, { name: "foo" }).startScope(10, 0, { + name: "bar", + }) + .endScope(20, 0).endScope(30, 0).endSource(); assertCodec(builder.build()); }); it("handles scope kinds", () => { - builder.startScope(0, 0, { kind: "Global" }).startScope(10, 0, { - kind: "Function", - }).endScope(20, 0).endScope(30, 0); + builder.startSource().startScope(0, 0, { kind: "Global" }).startScope( + 10, + 0, + { + kind: "Function", + }, + ).endScope(20, 0).endScope(30, 0).endSource(); assertCodec(builder.build()); }); it("handles names/kinds across multiple top-level scopes", () => { - builder.startScope(0, 0, { kind: "Global" }).startScope(10, 5, { - kind: "Function", - name: "foo", - }) - .endScope(20, 0).endScope(30, 0); - builder.startScope(0, 0, { kind: "Global" }).startScope(10, 5, { - kind: "Function", - name: "bar", - }) - .endScope(20, 0).endScope(30, 0); + builder.startSource().startScope(0, 0, { kind: "Global" }).startScope( + 10, + 5, + { + kind: "Function", + name: "foo", + }, + ) + .endScope(20, 0).endScope(30, 0).endSource(); + builder.startSource().startScope(0, 0, { kind: "Global" }).startScope( + 10, + 5, + { + kind: "Function", + name: "bar", + }, + ) + .endScope(20, 0).endScope(30, 0).endSource(); assertCodec(builder.build()); }); it("handles isStackFrame flag on scopes", () => { - builder.startScope(0, 0, { isStackFrame: true }).endScope(10, 0); + builder.startSource().startScope(0, 0, { isStackFrame: true }).endScope( + 10, + 0, + ).endSource(); assertCodec(builder.build()); }); @@ -125,18 +145,21 @@ describe("round trip", () => { }); it("handles a single GeneratedRange with a definition scope", () => { - builder.startScope(0, 0, { key: 0 }).endScope(10, 0).startRange(0, 0, { - scopeKey: 0, - }) + builder.startSource().startScope(0, 0, { key: 0 }).endScope(10, 0) + .endSource().startRange(0, 0, { + scopeKey: 0, + }) .endRange(0, 10); assertCodec(builder.build()); }); it("handles multiple GeneratedRanges with different definition scopes", () => { - builder.startScope(0, 0, { key: 0 }).endScope(10, 0).startScope(0, 0, { - key: 1, - }).endScope(20, 0) + builder.startSource().startScope(0, 0, { key: 0 }).endScope(10, 0) + .endSource() + .startSource().startScope(0, 0, { + key: 1, + }).endScope(20, 0).endSource() .startRange(0, 0) .startRange(0, 10, { scopeKey: 0 }) .endRange(0, 40) @@ -160,17 +183,21 @@ describe("round trip", () => { }); it("handles OriginalScope variables", () => { - builder.startScope(0, 0, { variables: ["foo", "bar"] }).startScope(10, 0, { - variables: ["local1", "local2"], - }).endScope(20, 0).endScope(30, 0); + builder.startSource().startScope(0, 0, { variables: ["foo", "bar"] }) + .startScope(10, 0, { + variables: ["local1", "local2"], + }).endScope(20, 0).endScope(30, 0).endSource(); assertCodec(builder.build()); }); it("handles value bindings expressions", () => { - builder.startScope(0, 0, { variables: ["foo", "bar"], key: "outer" }) + builder.startSource().startScope(0, 0, { + variables: ["foo", "bar"], + key: "outer", + }) .startScope(10, 0, { variables: ["local1", "local2"], key: "inner" }) - .endScope(20, 0).endScope(30, 0) + .endScope(20, 0).endScope(30, 0).endSource() .startRange(0, 0, { scopeKey: "outer", values: ["f", "b"] }).startRange( 0, 10, @@ -181,9 +208,13 @@ describe("round trip", () => { }); it("handles callSites for inlined ranges", () => { - builder.startScope(0, 0, { key: "global" }).startScope(10, 0, { - key: "function", - }).endScope(20, 0).endScope(30, 0) + builder.startSource().startScope(0, 0, { key: "global" }).startScope( + 10, + 0, + { + key: "function", + }, + ).endScope(20, 0).endScope(30, 0).endSource() .startRange(0, 0, { scopeKey: "global" }) .startRange(0, 10, { scopeKey: "function", @@ -206,10 +237,10 @@ describe("round trip", () => { }); it("handles sub-range bindings", () => { - builder.startScope(0, 0, { + builder.startSource().startScope(0, 0, { key: "scope", variables: ["v1", "v2", "v3", "v4"], - }).endScope(10, 0); + }).endScope(10, 0).endSource(); builder.startRange(0, 0, { scopeKey: "scope", @@ -273,7 +304,8 @@ describe("round trip", () => { // Regression test for issue #1. it("handles sub-ranges correctly when the range has children", () => { - builder.startScope(0, 0, { variables: ["x"], key: "root" }).endScope(1, 19) + builder.startSource().startScope(0, 0, { variables: ["x"], key: "root" }) + .endScope(1, 19).endSource() .startRange(0, 0, { scopeKey: "root", values: [[{ diff --git a/src/scopes.ts b/src/scopes.ts index 5e5aee4..71dd1f2 100644 --- a/src/scopes.ts +++ b/src/scopes.ts @@ -7,9 +7,9 @@ */ export interface ScopeInfo { /** - * The length of {@linkcode scopes} must match the length of "sources" in the source map JSON. Each entry describes the scope tree of the corresponding source file. + * The length of {@linkcode scopes} must match the length of "sources" in the source map JSON. Each entry describes the scope trees of the corresponding source file. */ - scopes: (OriginalScope | null)[]; + scopes: (OriginalScope[] | null)[]; /** * The range tree of the generated bundle. Multiple top-level ranges are allowed but must not overlap source position wise.