Skip to content

Commit 7f61abe

Browse files
committed
Add sub-range validation and tests in the safe builder
1 parent 96dd44a commit 7f61abe

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

src/builder/safe_builder.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,5 +338,127 @@ describe("SafeScopeInfoBuilder", () => {
338338

339339
assertThrows(() => builder.endRange(5, 0));
340340
});
341+
342+
describe("sub-range bindings", () => {
343+
beforeEach(() => {
344+
builder.startScope(0, 0, { key: "test-scope", variables: ["foo"] })
345+
.endScope(20, 0);
346+
});
347+
348+
it("allows empty sub-range bindings", () => {
349+
builder.startRange(0, 0, { scopeKey: "test-scope", values: [[]] })
350+
.endRange(10, 0);
351+
});
352+
353+
it("allows single item sub-range bindings", () => {
354+
builder.startRange(0, 0, {
355+
scopeKey: "test-scope",
356+
values: [[{
357+
value: "a",
358+
from: { line: 0, column: 0 },
359+
to: { line: 10, column: 0 },
360+
}]],
361+
})
362+
.endRange(10, 0);
363+
});
364+
365+
it("allows multi-item sub-range bindings", () => {
366+
builder.startRange(0, 0, {
367+
scopeKey: "test-scope",
368+
values: [[{
369+
value: "a",
370+
from: { line: 0, column: 0 },
371+
to: { line: 5, column: 0 },
372+
}, {
373+
value: "b",
374+
from: { line: 5, column: 0 },
375+
to: { line: 10, column: 0 },
376+
}]],
377+
})
378+
.endRange(10, 0);
379+
});
380+
381+
it("throws if the first sub-range does not start at the range start", () => {
382+
builder.startRange(0, 0, {
383+
scopeKey: "test-scope",
384+
values: [[{
385+
value: "a",
386+
from: { line: 1, column: 0 },
387+
to: { line: 10, column: 0 },
388+
}]],
389+
});
390+
391+
assertThrows(() => builder.endRange(10, 0));
392+
});
393+
394+
it("throws if the last sub-range does not end at the range end", () => {
395+
builder.startRange(0, 0, {
396+
scopeKey: "test-scope",
397+
values: [[{
398+
value: "a",
399+
from: { line: 0, column: 0 },
400+
to: { line: 8, column: 0 },
401+
}]],
402+
});
403+
404+
assertThrows(() => builder.endRange(10, 0));
405+
});
406+
407+
it("throws if sub-ranges are not sorted", () => {
408+
builder.startRange(0, 0, {
409+
scopeKey: "test-scope",
410+
values: [[{
411+
value: "a",
412+
from: { line: 5, column: 0 },
413+
to: { line: 10, column: 0 },
414+
}, {
415+
value: "b",
416+
from: { line: 0, column: 0 },
417+
to: { line: 5, column: 0 },
418+
}]],
419+
});
420+
421+
assertThrows(() => builder.endRange(10, 0));
422+
});
423+
424+
it("throws if sub-ranges have a gap", () => {
425+
builder.startRange(0, 0, {
426+
scopeKey: "test-scope",
427+
values: [[{
428+
value: "a",
429+
from: { line: 0, column: 0 },
430+
to: { line: 4, column: 0 },
431+
}, {
432+
value: "b",
433+
from: { line: 5, column: 0 },
434+
to: { line: 10, column: 0 },
435+
}]],
436+
});
437+
438+
assertThrows(() => builder.endRange(10, 0));
439+
});
440+
441+
it("throws if a sub-range 'from' does not precede 'to'", () => {
442+
builder.startRange(0, 0, {
443+
scopeKey: "test-scope",
444+
values: [[{
445+
value: "a",
446+
from: { line: 5, column: 0 },
447+
to: { line: 5, column: 0 },
448+
}]],
449+
});
450+
assertThrows(() => builder.endRange(10, 0));
451+
452+
builder.startRange(0, 0, {
453+
scopeKey: "test-scope",
454+
values: [[{
455+
value: "a",
456+
from: { line: 6, column: 0 },
457+
to: { line: 5, column: 0 },
458+
}]],
459+
});
460+
assertThrows(() => builder.endRange(10, 0));
461+
});
462+
});
341463
});
342464
});

src/builder/safe_builder.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
Binding,
77
OriginalPosition,
88
OriginalScope,
9+
Position,
910
ScopeInfo,
1011
} from "../scopes.d.ts";
1112
import { comparePositions } from "../util.ts";
@@ -268,6 +269,7 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
268269
);
269270
}
270271

272+
this.#verifyRangeValues(range, { line, column });
271273
super.endRange(line, column);
272274
return this;
273275
}
@@ -306,4 +308,52 @@ export class SafeScopeInfoBuilder extends ScopeInfoBuilder {
306308
throw new Error(`Can't ${op} while no GeneratedRange is on the stack.`);
307309
}
308310
}
311+
312+
#verifyRangeValues(
313+
range: { start: Position; values: Binding[] },
314+
end: Position,
315+
): void {
316+
for (const value of range.values) {
317+
if (!Array.isArray(value)) {
318+
continue;
319+
}
320+
321+
const subRanges = value;
322+
if (subRanges.length === 0) {
323+
continue;
324+
}
325+
326+
const first = subRanges.at(0)!;
327+
if (comparePositions(first.from, range.start) !== 0) {
328+
throw new Error(
329+
`Sub-range bindings must start at the generated range's start. Expected ${range.start.line}:${range.start.column}, but got ${first.from.line}:${first.from.column}`,
330+
);
331+
}
332+
333+
const last = subRanges.at(-1)!;
334+
if (comparePositions(last.to, end) !== 0) {
335+
throw new Error(
336+
`Sub-range bindings must end at the generated range's end. Expected ${end.line}:${end.column}, but got ${last.to.line}:${last.to.column}`,
337+
);
338+
}
339+
340+
for (let i = 0; i < subRanges.length; ++i) {
341+
const current = subRanges[i];
342+
if (comparePositions(current.from, current.to) >= 0) {
343+
throw new Error(
344+
`Sub-range binding 'from' (${current.from.line}:${current.from.column}) must precede 'to' (${current.to.line}:${current.to.column})`,
345+
);
346+
}
347+
348+
if (i > 0) {
349+
const prev = subRanges[i - 1];
350+
if (comparePositions(prev.to, current.from) !== 0) {
351+
throw new Error(
352+
`Sub-range bindings must be sorted and not overlap. Found gap between ${prev.to.line}:${prev.to.column} and ${current.from.line}:${current.from.column}`,
353+
);
354+
}
355+
}
356+
}
357+
}
358+
}
309359
}

0 commit comments

Comments
 (0)