Skip to content

Commit 0ee16a3

Browse files
committed
Add simple value bindings to builders
1 parent 8fe4aec commit 0ee16a3

File tree

5 files changed

+138
-7
lines changed

5 files changed

+138
-7
lines changed

src/builder/builder.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ describe("ScopeInfoBuilder", () => {
202202

203203
assertStrictEquals(info.ranges[0]?.isHidden, true);
204204
});
205+
206+
it("can set simple values via option", () => {
207+
const info = builder.startRange(0, 0, { values: ["a", null, "b"] })
208+
.endRange(0, 10).build();
209+
210+
assertEquals(info.ranges[0]?.values, ["a", null, "b"]);
211+
});
205212
});
206213

207214
describe("setRangeDefinitionScope", () => {
@@ -256,6 +263,19 @@ describe("ScopeInfoBuilder", () => {
256263
});
257264
});
258265

266+
describe("setRangeValues", () => {
267+
it("sets the values", () => {
268+
const info = builder.startRange(0, 0).setRangeValues(["a", null, null])
269+
.endRange(0, 10).build();
270+
271+
assertEquals(info.ranges[0]?.values, ["a", null, null]);
272+
});
273+
274+
it("does nothing when no range is on the stack", () => {
275+
builder.setRangeValues(["a", null, "b"]);
276+
});
277+
});
278+
259279
describe("endRange", () => {
260280
it("does nothing when no range is open", () => {
261281
builder.endRange(0, 20);

src/builder/builder.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import type { GeneratedRange, OriginalScope, ScopeInfo } from "../scopes.d.ts";
5+
import type {
6+
Binding,
7+
GeneratedRange,
8+
OriginalScope,
9+
ScopeInfo,
10+
} from "../scopes.d.ts";
611

712
/**
813
* Small utility class to build scope and range trees.
@@ -129,14 +134,15 @@ export class ScopeInfoBuilder {
129134
scopeKey?: ScopeKey;
130135
isStackFrame?: boolean;
131136
isHidden?: boolean;
137+
values?: Binding[];
132138
},
133139
): this {
134140
const range: GeneratedRange = {
135141
start: { line, column },
136142
end: { line, column },
137143
isStackFrame: Boolean(options?.isStackFrame),
138144
isHidden: Boolean(options?.isHidden),
139-
values: [],
145+
values: options?.values ?? [],
140146
children: [],
141147
};
142148

@@ -181,6 +187,13 @@ export class ScopeInfoBuilder {
181187
return this;
182188
}
183189

190+
setRangeValues(values: Binding[]): this {
191+
const range = this.#rangeStack.at(-1);
192+
if (range) range.values = values;
193+
194+
return this;
195+
}
196+
184197
endRange(line: number, column: number): this {
185198
const range = this.#rangeStack.pop();
186199
if (!range) return this;
@@ -221,6 +234,10 @@ export class ScopeInfoBuilder {
221234
protected isValidScopeKey(key: ScopeKey): boolean {
222235
return this.#keyToScope.has(key);
223236
}
237+
238+
protected getScopeByValidKey(key: ScopeKey): OriginalScope {
239+
return this.#keyToScope.get(key)!;
240+
}
224241
}
225242

226243
/**

src/builder/safe_builder.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ describe("SafeScopeInfoBuilder", () => {
171171
})
172172
);
173173
});
174+
175+
it("throws when 'values' is provided without a scope", () => {
176+
assertThrows(() => builder.startRange(0, 0, { values: ["a", null] }));
177+
});
178+
179+
it("throws when 'values' length does not match OriginalScope.variables length (via scope)", () => {
180+
const scope = builder.startScope(0, 0, { variables: ["foo", "bar"] })
181+
.endScope(10, 0).lastScope()!;
182+
183+
assertThrows(() =>
184+
builder.startRange(0, 0, { scope, values: ["a", null, "b"] })
185+
);
186+
});
187+
188+
it("throws when 'values' length does not match OriginalScope.variables length (via scopeKey)", () => {
189+
builder.startScope(0, 0, { variables: ["foo", "bar"], key: "my key" })
190+
.endScope(10, 0);
191+
192+
assertThrows(() =>
193+
builder.startRange(0, 0, {
194+
scopeKey: "my key",
195+
values: ["a", null, "b"],
196+
})
197+
);
198+
});
174199
});
175200

176201
describe("setRangeDefinitionScope", () => {
@@ -247,6 +272,32 @@ describe("SafeScopeInfoBuilder", () => {
247272
});
248273
});
249274

275+
describe("setRangeValues", () => {
276+
it("throws when no range is on open", () => {
277+
assertThrows(() => builder.setRangeValues(["a", null]));
278+
});
279+
280+
it("throws while building a scope", () => {
281+
builder.startScope(0, 0);
282+
283+
assertThrows(() => builder.setRangeValues(["a", null]));
284+
});
285+
286+
it("throws when called without setting a scope prior", () => {
287+
builder.startRange(0, 0);
288+
289+
assertThrows(() => builder.setRangeValues(["a", null]));
290+
});
291+
292+
it("throws when 'values' length does not match OriginalScope.variables length (via scope)", () => {
293+
const scope = builder.startScope(0, 0, { variables: ["foo", "bar"] })
294+
.endScope(10, 0).lastScope()!;
295+
builder.startRange(0, 0, { scope });
296+
297+
assertThrows(() => builder.setRangeValues(["a", null, "b"]));
298+
});
299+
});
300+
250301
describe("endRange", () => {
251302
it("throws when the range stack is empty", () => {
252303
assertThrows(() => builder.endRange(5, 0));

src/builder/safe_builder.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

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

@@ -114,6 +114,7 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
114114
scopeKey?: ScopeKey;
115115
isStackFrame?: boolean;
116116
isHidden?: boolean;
117+
values?: Binding[];
117118
},
118119
): this {
119120
this.#verifyEmptyScopeStack("starRange");
@@ -151,6 +152,27 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
151152
);
152153
}
153154

155+
if (
156+
options?.values?.length && options?.scope === undefined &&
157+
options?.scopeKey === undefined
158+
) {
159+
throw new Error("Provided bindings without providing an OriginalScope");
160+
} else if (
161+
options?.values?.length && options?.scope &&
162+
options.values.length !== options.scope.variables.length
163+
) {
164+
throw new Error(
165+
"Provided bindings don't match up with OriginalScope.variables",
166+
);
167+
} else if (options?.values?.length && options?.scopeKey !== undefined) {
168+
const scope = this.getScopeByValidKey(options.scopeKey);
169+
if (options.values.length !== scope.variables.length) {
170+
throw new Error(
171+
"Provided bindings don't match up with OriginalScope.variables",
172+
);
173+
}
174+
}
175+
154176
super.startRange(line, column, options);
155177
return this;
156178
}
@@ -199,6 +221,25 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
199221
return this;
200222
}
201223

224+
override setRangeValues(values: Binding[]): this {
225+
this.#verifyEmptyScopeStack("setRangeValues");
226+
this.#verifyRangePresent("setRangeValues");
227+
228+
const range = this.rangeStack.at(-1)!;
229+
if (!range.originalScope) {
230+
throw new Error(
231+
"Setting an OriginalScope for a range is required before value bindings can be provided!",
232+
);
233+
} else if (range.originalScope.variables.length !== values.length) {
234+
throw new Error(
235+
"Provided bindings don't match up with OriginalScope.variables",
236+
);
237+
}
238+
239+
super.setRangeValues(values);
240+
return this;
241+
}
242+
202243
override endRange(line: number, column: number): this {
203244
this.#verifyEmptyScopeStack("endRange");
204245

src/scopes.d.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,18 @@ export interface GeneratedRange {
9292
*
9393
* 1) A single expression (valid for a full `GeneratedRange`).
9494
*
95-
* 2) `undefined` if this variable is unavailable in the whole range. This can
95+
* 2) `null` if this variable is unavailable in the whole range. This can
9696
* happen e.g. when the variable was optimized out and can't be recomputed.
9797
*
9898
* 3) A list of `SubRangeBinding`s. Used when computing the value requires different
9999
* expressions throughout the `GeneratedRange` or if the variable is unavailable in
100100
* parts of the `GeneratedRange`.
101-
* The "from" of the first `SubRangeBinding` and the "to" of the last `SubRangeBinding`
102-
* are qual to the `GeneratedRange`s "start" and "end" position respectively.
101+
*
102+
* Note: The decoder produces `SubRangeBindings` where the "from" of the first `SubRangeBinding`
103+
* and the "to" of the last `SubRangeBinding` are equal to the `GeneratedRange`s "start" and "end"
104+
* position respectively.
103105
*/
104-
export type Binding = string | undefined | SubRangeBinding[];
106+
export type Binding = string | null | SubRangeBinding[];
105107

106108
export interface SubRangeBinding {
107109
value?: string;

0 commit comments

Comments
 (0)