Skip to content

Commit 1ffb4dd

Browse files
committed
Change how the builder handles definition OriginalScopes
1 parent bacf465 commit 1ffb4dd

File tree

5 files changed

+121
-67
lines changed

5 files changed

+121
-67
lines changed

src/builder/builder.test.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,10 @@ describe("ScopeInfoBuilder", () => {
168168

169169
describe("startRange", () => {
170170
it("sets the definition scope when it's provided as a number", () => {
171-
const info = builder.startScope(0, 0).endScope(10, 0).startRange(0, 0, {
172-
scope: 0,
173-
}).endRange(0, 10).build();
171+
const info = builder.startScope(0, 0, { key: 0 }).endScope(10, 0)
172+
.startRange(0, 0, {
173+
scopeKey: 0,
174+
}).endRange(0, 10).build();
174175

175176
assertStrictEquals(info.scopes[0], info.ranges[0].originalScope);
176177
});
@@ -204,24 +205,32 @@ describe("ScopeInfoBuilder", () => {
204205
});
205206

206207
describe("setRangeDefinitionScope", () => {
207-
it("sets the definition scope when it's provided as a number", () => {
208-
const info = builder.startScope(0, 0).endScope(10, 0).startRange(0, 0)
209-
.setRangeDefinitionScope(0).endRange(0, 10).build();
208+
it("sets the definition scope when it's provided directly", () => {
209+
const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!;
210+
const info = builder.startRange(0, 0).setRangeDefinitionScope(scope)
211+
.endRange(0, 10).build();
210212

211213
assertStrictEquals(info.scopes[0], info.ranges[0].originalScope);
214+
assertStrictEquals(info.ranges[0].originalScope, scope);
212215
});
213216

217+
it("does nothing when no range is on the stack", () => {
218+
const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!;
219+
builder.setRangeDefinitionScope(scope);
220+
});
221+
});
222+
223+
describe("setRangeDefinitionScopeKey", () => {
214224
it("sets the definition scope when it's provided directly", () => {
215-
const scope = builder.startScope(0, 0).endScope(10, 0).lastScope();
216-
const info = builder.startRange(0, 0).setRangeDefinitionScope(scope!)
225+
builder.startScope(0, 0, { key: "my key" }).endScope(10, 0);
226+
const info = builder.startRange(0, 0).setRangeDefinitionScopeKey("my key")
217227
.endRange(0, 10).build();
218228

219-
assertStrictEquals(info.scopes[0], info.ranges[0].originalScope);
220-
assertStrictEquals(info.ranges[0].originalScope, scope);
229+
assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]);
221230
});
222231

223232
it("does nothing when no range is on the stack", () => {
224-
builder.setRangeDefinitionScope(0);
233+
builder.setRangeDefinitionScopeKey("foo");
225234
});
226235
});
227236

@@ -297,4 +306,14 @@ describe("ScopeInfoBuilder", () => {
297306
assertEquals(builder.lastScope()?.start, { line: 10, column: 0 });
298307
});
299308
});
309+
310+
describe("scope key", () => {
311+
it("can set the scope key via options", () => {
312+
builder.startScope(0, 0, { key: "my custom key" }).endScope(10, 0);
313+
builder.startRange(0, 0, { scopeKey: "my custom key" }).endRange(0, 10);
314+
const info = builder.build();
315+
316+
assertStrictEquals(info.ranges[0].originalScope, info.scopes[0]);
317+
});
318+
});
300319
});

src/builder/builder.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ export class ScopeInfoBuilder {
2121
#scopeStack: OriginalScope[] = [];
2222
#rangeStack: GeneratedRange[] = [];
2323

24-
#scopeCounter = 0;
25-
#scopeToCount = new Map<OriginalScope, number>();
26-
#countToScope = new Map<number, OriginalScope>();
24+
#knownScopes = new Set<OriginalScope>();
25+
#keyToScope = new Map<ScopeKey, OriginalScope>();
2726
#lastScope: OriginalScope | null = null;
2827

2928
addNullScope(): this {
@@ -39,6 +38,7 @@ export class ScopeInfoBuilder {
3938
kind?: string;
4039
isStackFrame?: boolean;
4140
variables?: string[];
41+
key?: ScopeKey;
4242
},
4343
): this {
4444
const scope: OriginalScope = {
@@ -56,8 +56,8 @@ export class ScopeInfoBuilder {
5656
scope.parent = this.#scopeStack.at(-1);
5757
}
5858
this.#scopeStack.push(scope);
59-
this.#scopeToCount.set(scope, this.#scopeCounter);
60-
this.#countToScope.set(this.#scopeCounter++, scope);
59+
this.#knownScopes.add(scope);
60+
if (options?.key !== undefined) this.#keyToScope.set(options.key, scope);
6161

6262
return this;
6363
}
@@ -119,14 +119,14 @@ export class ScopeInfoBuilder {
119119

120120
/**
121121
* @param option The definition 'scope' of this range can either be the "OriginalScope" directly
122-
* (produced by this builder) or the scope's number.
123-
* If a scope was started with the n-th call to `startScope` then n is the scope's number.
122+
* (produced by this builder) or the scope's key set while building the scope.
124123
*/
125124
startRange(
126125
line: number,
127126
column: number,
128127
options?: {
129-
scope?: number | OriginalScope;
128+
scope?: OriginalScope;
129+
scopeKey?: ScopeKey;
130130
isStackFrame?: boolean;
131131
isHidden?: boolean;
132132
},
@@ -144,27 +144,26 @@ export class ScopeInfoBuilder {
144144
range.parent = this.#rangeStack.at(-1);
145145
}
146146

147-
if (typeof options?.scope === "number") {
148-
range.originalScope = this.#countToScope.get(options.scope);
149-
} else if (options?.scope !== undefined) {
147+
if (options?.scope !== undefined) {
150148
range.originalScope = options.scope;
149+
} else if (options?.scopeKey !== undefined) {
150+
range.originalScope = this.#keyToScope.get(options.scopeKey);
151151
}
152152

153153
this.#rangeStack.push(range);
154154

155155
return this;
156156
}
157157

158-
setRangeDefinitionScope(scope: number | OriginalScope): this {
158+
setRangeDefinitionScope(scope: OriginalScope): this {
159159
const range = this.#rangeStack.at(-1);
160-
if (!range) return this;
161-
162-
if (typeof scope === "number") {
163-
range.originalScope = this.#countToScope.get(scope);
164-
} else {
165-
range.originalScope = scope;
166-
}
160+
if (range) range.originalScope = scope;
161+
return this;
162+
}
167163

164+
setRangeDefinitionScopeKey(scopeKey: ScopeKey): this {
165+
const range = this.#rangeStack.at(-1);
166+
if (range) range.originalScope = this.#keyToScope.get(scopeKey);
168167
return this;
169168
}
170169

@@ -202,9 +201,7 @@ export class ScopeInfoBuilder {
202201

203202
this.#scopes = [];
204203
this.#ranges = [];
205-
this.#scopeCounter = 0;
206-
this.#scopeToCount.clear();
207-
this.#countToScope.clear();
204+
this.#knownScopes.clear();
208205

209206
return info;
210207
}
@@ -217,11 +214,19 @@ export class ScopeInfoBuilder {
217214
return this.#rangeStack;
218215
}
219216

220-
protected isValidScopeNumber(n: number): boolean {
221-
return this.#countToScope.has(n);
217+
protected isKnownScope(scope: OriginalScope): boolean {
218+
return this.#knownScopes.has(scope);
222219
}
223220

224-
protected isKnownScope(scope: OriginalScope): boolean {
225-
return this.#scopeToCount.has(scope);
221+
protected isValidScopeKey(key: ScopeKey): boolean {
222+
return this.#keyToScope.has(key);
226223
}
227224
}
225+
226+
/**
227+
* Users of the {@link ScopeInfoBuilder} can provide their own keys to uniquely identify a scope,
228+
* and use the key later when building the corresponding range to connect them.
229+
*
230+
* The only requirement for ScopeKey is that it can be used as a key in a `Map`.
231+
*/
232+
export type ScopeKey = unknown;

src/builder/safe_builder.test.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ describe("SafeScopeInfoBuilder", () => {
154154
builder.startRange(10, 5);
155155
});
156156

157-
it("throws when the definition scope doesnt point to a valid scope", () => {
158-
assertThrows(() => builder.startRange(0, 0, { scope: 0 }));
157+
it("throws when the definition scope doesn't point to a valid scope", () => {
158+
assertThrows(() => builder.startRange(0, 0, { scopeKey: 0 }));
159159
});
160160

161161
it("throws when the definition scope is not known to the builder", () => {
@@ -175,17 +175,39 @@ describe("SafeScopeInfoBuilder", () => {
175175

176176
describe("setRangeDefinitionScope", () => {
177177
it("throws when no range is open", () => {
178-
assertThrows(() => builder.setRangeDefinitionScope(0));
178+
const scope = builder.startScope(0, 0).endScope(10, 0).lastScope()!;
179+
180+
assertThrows(() => builder.setRangeDefinitionScope(scope));
179181
});
180182

181183
it("throws while building a scope", () => {
182-
builder.startScope(0, 0);
184+
const scope = builder.startScope(0, 0).currentScope()!;
185+
186+
assertThrows(() => builder.setRangeDefinitionScope(scope));
187+
});
188+
189+
it("throws when the definition scope is not known to the builder", () => {
190+
assertThrows(() =>
191+
builder.startRange(0, 0).setRangeDefinitionScope({
192+
start: { line: 0, column: 0 },
193+
end: { line: 10, column: 10 },
194+
isStackFrame: false,
195+
variables: [],
196+
children: [],
197+
})
198+
);
199+
});
200+
});
183201

184-
assertThrows(() => builder.setRangeDefinitionScope(0));
202+
describe("setRangeDefinitionScopeKey", () => {
203+
it("throws when no range is open", () => {
204+
assertThrows(() => builder.setRangeDefinitionScopeKey("my key"));
185205
});
186206

187-
it("throws when the definition scope doesnt point to a valid scope", () => {
188-
assertThrows(() => builder.startRange(0, 0).setRangeDefinitionScope(0));
207+
it("throws while building a scope", () => {
208+
builder.startScope(0, 0, { key: "my key" });
209+
210+
assertThrows(() => builder.setRangeDefinitionScopeKey("my key"));
189211
});
190212

191213
it("throws when the definition scope is not known to the builder", () => {

src/builder/safe_builder.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import type { OriginalScope, ScopeInfo } from "../scopes.d.ts";
66
import { comparePositions } from "../util.ts";
7-
import { ScopeInfoBuilder } from "./builder.ts";
7+
import { ScopeInfoBuilder, type ScopeKey } from "./builder.ts";
88

99
/**
1010
* Similar to `ScopeInfoBuilder`, but with checks that scopes/ranges are well
@@ -27,6 +27,7 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
2727
kind?: string;
2828
isStackFrame?: boolean;
2929
variables?: string[];
30+
key?: ScopeKey;
3031
},
3132
): this {
3233
this.#verifyEmptyRangeStack("start scope");
@@ -109,7 +110,8 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
109110
line: number,
110111
column: number,
111112
options?: {
112-
scope?: number | OriginalScope;
113+
scope?: OriginalScope;
114+
scopeKey?: ScopeKey;
113115
isStackFrame?: boolean;
114116
isHidden?: boolean;
115117
},
@@ -136,16 +138,14 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
136138
}
137139

138140
if (
139-
typeof options?.scope === "number" &&
140-
!this.isValidScopeNumber(options.scope)
141+
options?.scopeKey !== undefined &&
142+
!this.isValidScopeKey(options.scopeKey)
141143
) {
142144
throw new Error(
143-
`${options.scope} does not reference a valid OriginalScope`,
145+
`${options.scopeKey} does not reference a valid OriginalScope`,
144146
);
145147
}
146-
if (
147-
typeof options?.scope === "object" && !this.isKnownScope(options.scope)
148-
) {
148+
if (options?.scope && !this.isKnownScope(options.scope)) {
149149
throw new Error(
150150
"The provided definition scope was not produced by this builder!",
151151
);
@@ -155,27 +155,31 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
155155
return this;
156156
}
157157

158-
override setRangeDefinitionScope(scope: number | OriginalScope): this {
158+
override setRangeDefinitionScope(scope: OriginalScope): this {
159159
this.#verifyEmptyScopeStack("setRangeDefinitionScope");
160160
this.#verifyRangePresent("setRangeDefinitionScope");
161161

162-
if (
163-
typeof scope === "number" &&
164-
!this.isValidScopeNumber(scope)
165-
) {
162+
if (!this.isKnownScope(scope)) {
166163
throw new Error(
167-
`${scope} does not reference a valid OriginalScope`,
164+
"The provided definition scope was not produced by this builder!",
168165
);
169166
}
170-
if (
171-
typeof scope === "object" && !this.isKnownScope(scope)
172-
) {
167+
168+
super.setRangeDefinitionScope(scope);
169+
return this;
170+
}
171+
172+
override setRangeDefinitionScopeKey(scopeKey: ScopeKey): this {
173+
this.#verifyEmptyScopeStack("setRangeDefinitionScope");
174+
this.#verifyRangePresent("setRangeDefinitionScope");
175+
176+
if (!this.isValidScopeKey(scopeKey)) {
173177
throw new Error(
174-
"The provided definition scope was not produced by this builder!",
178+
`The provided scope key ${scopeKey} is not know nto the builder!`,
175179
);
176180
}
177181

178-
super.setRangeDefinitionScope(scope);
182+
super.setRangeDefinitionScopeKey(scopeKey);
179183
return this;
180184
}
181185

src/roundtrip.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,22 @@ describe("round trip", () => {
103103
});
104104

105105
it("handles a single GeneratedRange with a definition scope", () => {
106-
builder.startScope(0, 0).endScope(10, 0).startRange(0, 0, { scope: 0 })
106+
builder.startScope(0, 0, { key: 0 }).endScope(10, 0).startRange(0, 0, {
107+
scopeKey: 0,
108+
})
107109
.endRange(0, 10);
108110

109111
assertCodec(builder.build());
110112
});
111113

112114
it("handles multiple GeneratedRanges with different definition scopes", () => {
113-
builder.startScope(0, 0).endScope(10, 0).startScope(0, 0).endScope(20, 0)
115+
builder.startScope(0, 0, { key: 0 }).endScope(10, 0).startScope(0, 0, {
116+
key: 1,
117+
}).endScope(20, 0)
114118
.startRange(0, 0)
115-
.startRange(0, 10, { scope: 0 })
119+
.startRange(0, 10, { scopeKey: 0 })
116120
.endRange(0, 40)
117-
.startRange(0, 50, { scope: 1 })
121+
.startRange(0, 50, { scopeKey: 1 })
118122
.endRange(0, 80)
119123
.endRange(0, 100);
120124

0 commit comments

Comments
 (0)