• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 24834367427

23 Apr 2026 12:09PM UTC coverage: 78.541% (+0.2%) from 78.391%
24834367427

Pull #1893

github

web-flow
Merge ab101147d into 715603479
Pull Request #1893: Error Handling and Stringify Changes

3126 of 4197 branches covered (74.48%)

Branch coverage included in aggregate %.

801 of 975 new or added lines in 76 files covered. (82.15%)

20 existing lines in 11 files now uncovered.

7056 of 8767 relevant lines covered (80.48%)

173930.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.74
/src/utils/rttc.ts
1
import type es from 'estree';
2
import {
3
  InvalidCallbackError,
4
  InvalidNumberParameterError,
5
  InvalidParameterTypeError,
6
  type InvalidNumberParameterErrorOptions,
7
} from '../errors/rttcErrors';
8
import { RuntimeSourceError } from '../errors/base';
9
import { Chapter } from '../langs';
10
import type { Node, Value } from '../types';
11

12
const LHS = ' on left hand side of operation';
72✔
13
const RHS = ' on right hand side of operation';
72✔
14

15
/**
16
 * Error type to be thrown by runtime type checking functions. This is usually caused by a user
17
 * trying to do something that would work in Javascript (like `'1' - 1`) but is forbidden
18
 * in Source.
19
 */
20
export class RuntimeTypeError extends RuntimeSourceError<Node> {
21
  constructor(
22
    node: Node,
23
    public readonly side: string,
842✔
24
    public readonly expected: string,
842✔
25
    public readonly got: string,
842✔
26
    public readonly chapter: Chapter = Chapter.SOURCE_4,
842✔
27
  ) {
28
    super(node);
842✔
29
  }
30

31
  public override explain() {
32
    const displayGot =
33
      this.got === 'array' ? (this.chapter <= 2 ? 'pair' : 'compound data') : this.got;
1,671!
34
    return `Expected ${this.expected}${this.side}, got ${displayGot}.`;
1,671✔
35
  }
36
}
37

38
type TypeOfConstants =
39
  | 'array'
40
  | 'boolean'
41
  | 'bigint'
42
  | 'function'
43
  | 'number'
44
  | 'null'
45
  | 'object'
46
  | 'string'
47
  | 'undefined';
48

49
/**
50
 * A wrapper around the typeof operator to account for `null` and arrays.
51
 */
52
export function typeOf(v: boolean): 'boolean';
53
export function typeOf(v: bigint): 'bigint';
54
export function typeOf(v: number): 'number';
55
export function typeOf(v: string): 'string';
56
export function typeOf(v: (...args: any[]) => any): 'function';
57
export function typeOf(v: null): 'null';
58
export function typeOf(v: unknown[]): 'array';
59
export function typeOf(v: object): 'object';
60
export function typeOf(v: undefined): 'undefined';
61
export function typeOf(v: unknown): TypeOfConstants;
62
export function typeOf(v: unknown) {
63
  if (v === null) {
42,689,328✔
64
    return 'null';
274✔
65
  } else if (Array.isArray(v)) {
42,689,054✔
66
    return 'array';
543✔
67
  } else {
68
    return typeof v;
42,688,511✔
69
  }
70
}
71

72
/**
73
 * Returns `true` if the provided value is a `number`.
74
 */
75
export function isNumber(v: unknown): v is number {
76
  return typeOf(v) === 'number';
42,687,533✔
77
}
78

79
/**
80
 * Returns `true` is the provided value is a `number` and also a valid
81
 * index for an array.\
82
 * See section 4 of https://2ality.com/2012/12/arrays.html
83
 */
84
export function isArrayIndex(v: unknown): v is number {
85
  // v >>> 0 === v checks that v is a valid unsigned 32-bit int
86
  return isNumber(v) && v >>> 0 === v && v < 2 ** 32 - 1;
146✔
87
}
88

89
export function isArray(v: unknown): v is unknown[] {
90
  return typeOf(v) === 'array';
215✔
91
}
92

93
export function isString(v: unknown): v is string {
94
  return typeOf(v) === 'string';
507✔
95
}
96

97
export function isBool(v: unknown): v is boolean {
98
  return v === true || v === false;
24,582,013✔
99
}
100

101
export function isObject(v: Value): v is object {
102
  return typeOf(v) === 'object';
231✔
103
}
104

105
/**
106
 * Checks that the given unary expression has a valid type, i.e `!` is used with booleans
107
 * and `-` and `+` are used with numbers.
108
 */
109
export function checkUnaryExpression(
110
  node: Node,
111
  operator: '!',
112
  value: unknown,
113
  chapter?: Chapter,
114
): asserts value is boolean;
115
export function checkUnaryExpression(
116
  node: Node,
117
  operator: '-' | '+',
118
  value: unknown,
119
  chapter?: Chapter,
120
): asserts value is number;
121
export function checkUnaryExpression(
122
  node: Node,
123
  operator: es.UnaryOperator,
124
  value: unknown,
125
  chapter?: Chapter,
126
): asserts value is number | boolean;
127
export function checkUnaryExpression(
128
  node: Node,
129
  operator: es.UnaryOperator,
130
  value: unknown,
131
  chapter: Chapter = Chapter.SOURCE_4,
91✔
132
) {
133
  if ((operator === '+' || operator === '-') && !isNumber(value)) {
91✔
134
    throw new RuntimeTypeError(node, '', 'number', typeOf(value), chapter);
16✔
135
  } else if (operator === '!' && !isBool(value)) {
75✔
136
    throw new RuntimeTypeError(node, '', 'boolean', typeOf(value), chapter);
8✔
137
  }
138
}
139

140
/**
141
 * Checks that the given binary expression has a valid type:
142
 * 1. `+` can be used with both numbers or both strings
143
 * 2. `-`, `/`, `%`, `*`, `>`, `>=`, `<`, `<=` can only be used with numbers
144
 * 3. `||` and `&&` can only be used with booleans
145
 * 4. Equality operators can be used with any values
146
 */
147
export function checkBinaryExpression(
148
  node: Node,
149
  operator: '+',
150
  chapter: Chapter,
151
  values: [unknown, unknown],
152
): asserts values is [string, string] | [number, number];
153
export function checkBinaryExpression(
154
  node: Node,
155
  operator: '-' | '*' | '/' | '<' | '<=' | '>' | '>=' | '%',
156
  chapter: Chapter,
157
  values: [unknown, unknown],
158
): asserts values is [number, number];
159
export function checkBinaryExpression(
160
  node: Node,
161
  operator: '===' | '!==',
162
  chapter: Chapter.SOURCE_1 | Chapter.SOURCE_2,
163
  values: [unknown, unknown],
164
): asserts values is [string, string] | [number, number];
165
export function checkBinaryExpression<T>(
166
  node: Node,
167
  operator: '===' | '!==',
168
  chapter: Exclude<Chapter, Chapter.SOURCE_1 | Chapter.SOURCE_2>,
169
  values: [unknown, unknown],
170
): asserts values is [T, T];
171
export function checkBinaryExpression(
172
  node: Node,
173
  operator: es.BinaryOperator,
174
  chapter: Chapter,
175
  values: [unknown, unknown],
176
): asserts values is [any, any];
177
export function checkBinaryExpression(
178
  node: Node,
179
  operator: es.BinaryOperator,
180
  chapter: Chapter,
181
  [left, right]: [unknown, unknown],
182
) {
183
  switch (operator) {
21,594,414!
184
    case '-':
185
    case '*':
186
    case '/':
187
    case '%': {
188
      if (!isNumber(left)) {
444,242✔
189
        throw new RuntimeTypeError(node, LHS, 'number', typeOf(left), chapter);
289✔
190
      } else if (!isNumber(right)) {
443,953✔
191
        throw new RuntimeTypeError(node, RHS, 'number', typeOf(right), chapter);
33✔
192
      } else {
193
        return;
443,920✔
194
      }
195
    }
196
    case '+':
197
    case '<':
198
    case '<=':
199
    case '>':
200
    case '>=':
201
    case '!==':
202
    case '===': {
203
      if (chapter > 2 && (operator === '===' || operator === '!==')) {
21,150,172✔
204
        return;
250,413✔
205
      }
206
      if (isNumber(left)) {
20,899,759✔
207
        if (!isNumber(right))
20,899,359✔
208
          throw new RuntimeTypeError(node, RHS, 'number', typeOf(right), chapter);
43✔
209
      } else if (isString(left)) {
400✔
210
        if (!isString(right))
85✔
211
          throw new RuntimeTypeError(node, RHS, 'string', typeOf(right), chapter);
44✔
212
      } else {
213
        throw new RuntimeTypeError(node, LHS, 'string or number', typeOf(left), chapter);
315✔
214
      }
215
      return;
20,899,357✔
216
    }
217
    default:
218
      return;
×
219
  }
220
}
221

222
/**
223
 * Checks that the given if statement's test has a boolean type
224
 */
225
export function checkIfStatement(
226
  node: Node,
227
  test: unknown,
228
  chapter: Chapter = Chapter.SOURCE_4,
24,581,986✔
229
): asserts test is boolean {
230
  if (!isBool(test)) {
24,581,986✔
231
    throw new RuntimeTypeError(node, ' as condition', 'boolean', typeOf(test), chapter);
10✔
232
  }
233
}
234

235
const MAX_SOURCE_ARRAY_INDEX = 4294967295;
72✔
236
export function checkoutofRange(node: Node, index: Value, chapter: Chapter = Chapter.SOURCE_4) {
5✔
237
  if (index < 0 || index > MAX_SOURCE_ARRAY_INDEX) {
5!
238
    // as per Source 3 spec
NEW
239
    throw new RuntimeTypeError(node, ' in reasonable range', 'index', 'out of range', chapter);
×
240
  }
241
}
242

243
/**
244
 * Check that the given MemberExpression `x[y]` is of the correct type:
245
 * 1. `x` is an array and `y` is a valid array index
246
 * 2. `x` is an object and `y` is a string
247
 */
248
export function checkMemberAccess(
249
  node: Node,
250
  args: [Value, Value],
251
): asserts args is [object, string] | [unknown[], number] {
252
  const [obj, prop] = args;
231✔
253

254
  if (isObject(obj)) {
231✔
255
    if (!isString(prop)) {
22✔
256
      throw new RuntimeTypeError(node, ' as prop', 'string', typeOf(prop));
8✔
257
    }
258
    return;
14✔
259
  } else if (isArray(obj)) {
209✔
260
    if (isArrayIndex(prop)) return;
146✔
261

262
    if (isNumber(prop)) {
11✔
263
      throw new RuntimeTypeError(node, ' as prop', 'array index', 'other number');
3✔
264
    }
265
    throw new RuntimeTypeError(node, ' as prop', 'array index', typeOf(prop));
8✔
266
  }
267

268
  throw new RuntimeTypeError(node, '', 'object or array', typeOf(obj));
63✔
269
}
270

271
export function checkArray(
272
  node: Node,
273
  maybeArray: Value,
274
  chapter: Chapter = Chapter.SOURCE_4,
6✔
275
): asserts maybeArray is unknown[] {
276
  if (!isArray(maybeArray)) {
6✔
277
    throw new RuntimeTypeError(node, '', 'array', typeOf(maybeArray), chapter);
2✔
278
  }
279
}
280

281
type TupleOfLengthHelper<T extends number, U, V extends U[] = []> = V['length'] extends T
282
  ? V
283
  : TupleOfLengthHelper<T, U, [...V, U]>;
284

285
/**
286
 * Utility type that represents a tuple of a specific length
287
 */
288
export type TupleOfLength<T extends number, U = unknown> = TupleOfLengthHelper<T, U>;
289

290
/**
291
 * Type guard for checking that the provided value is a function and that it has the specified number of parameters.
292
 * Of course at runtime parameter types are not checked, so this is only useful when combined with TypeScript types.
293
 */
294
export function isFunctionOfLength<T extends (...args: any[]) => any>(
295
  f: (...args: any) => any,
296
  l: Parameters<T>['length'],
297
): f is T;
298
export function isFunctionOfLength<T extends number>(
299
  f: unknown,
300
  l: T,
301
): f is (...args: TupleOfLength<T>) => unknown;
302
export function isFunctionOfLength(f: unknown, l: number) {
303
  // TODO: Need a variation for rest parameters
304
  return typeof f === 'function' && f.length === l;
12✔
305
}
306

307
/**
308
 * Assertion version of {@link isFunctionOfLength}
309
 *
310
 * @param f Value to validate
311
 * @param l Number of parameters that `f` is expected to have
312
 * @param func_name Function within which the validation is occurring
313
 * @param type_name Optional alias for the function type
314
 * @param param_name Name of the parameter that's being validated
315
 */
316
export function assertFunctionOfLength<T extends (...args: any[]) => any>(
317
  f: (...args: any) => any,
318
  l: Parameters<T>['length'],
319
  func_name: string,
320
  type_name?: string,
321
  param_name?: string,
322
): asserts f is T;
323
export function assertFunctionOfLength<T extends number>(
324
  f: unknown,
325
  l: T,
326
  func_name: string,
327
  type_name?: string,
328
  param_name?: string,
329
): asserts f is (...args: TupleOfLength<T>) => unknown;
330
export function assertFunctionOfLength(
331
  f: unknown,
332
  l: number,
333
  func_name: string,
334
  type_name?: string,
335
  param_name?: string,
336
) {
337
  if (!isFunctionOfLength(f, l)) {
8✔
338
    throw new InvalidCallbackError(type_name ?? l, f, func_name, param_name);
2✔
339
  }
340
}
341

342
/**
343
 * Function for checking if the given `obj` is a tuple of the given length.
344
 */
345
export function isTupleOfLength<T extends number, U>(obj: U[], l: T): obj is TupleOfLength<T, U>;
346
export function isTupleOfLength<T extends number>(obj: unknown, l: T): obj is TupleOfLength<T>;
347
export function isTupleOfLength<T extends number>(obj: unknown, l: T): obj is TupleOfLength<T> {
348
  if (!Array.isArray(obj)) return false;
4!
349
  return obj.length === l;
4✔
350
}
351

352
/**
353
 * Assertion version of {@link isTupleOfLength}
354
 */
355
export function assertTupleOfLength<T extends number, U>(
356
  obj: U[],
357
  l: T,
358
  func_name: string,
359
  param_name?: string,
360
): asserts obj is TupleOfLength<T, U>;
361
export function assertTupleOfLength<T extends number>(
362
  obj: unknown,
363
  l: T,
364
  func_name: string,
365
  param_name?: string,
366
): asserts obj is TupleOfLength<T>;
367
export function assertTupleOfLength<T extends number>(
368
  obj: unknown,
369
  l: T,
370
  func_name: string,
371
  param_name?: string,
372
): asserts obj is TupleOfLength<T> {
NEW
373
  if (!isTupleOfLength(obj, l)) {
×
NEW
374
    throw new InvalidParameterTypeError(`tuple of length ${length}`, obj, func_name, param_name);
×
375
  }
376
}
377

378
/**
379
 * Function for checking if a given value is a number and that it also potentially satisfies a bunch of other criteria:
380
 * - Within a given range of [min, max]
381
 * - Is an integer
382
 * - Is not NaN
383
 */
384
export function isNumberWithinRange(
385
  value: unknown,
386
  min?: number,
387
  max?: number,
388
  integer?: boolean,
389
): value is number;
390
export function isNumberWithinRange(
391
  value: unknown,
392
  options: InvalidNumberParameterErrorOptions,
393
): value is number;
394
export function isNumberWithinRange(
395
  value: unknown,
396
  arg0?: InvalidNumberParameterErrorOptions | number,
397
  max?: number,
398
  integer: boolean = true,
63✔
399
): value is number {
400
  let options: InvalidNumberParameterErrorOptions;
401

402
  if (typeof arg0 === 'number' || typeof arg0 === 'undefined') {
63✔
403
    options = {
11✔
404
      min: arg0,
405
      max,
406
      integer,
407
    };
408
  } else {
409
    options = arg0;
52✔
410
    options.integer = arg0.integer ?? true;
52✔
411
  }
412

413
  if (typeof value !== 'number' || Number.isNaN(value)) return false;
63✔
414

415
  if (options.max !== undefined && value > options.max) return false;
52✔
416
  if (options.min !== undefined && value < options.min) return false;
51✔
417

418
  return !options.integer || Number.isInteger(value);
46✔
419
}
420

421
export interface AssertNumberWithinRangeOptions extends InvalidNumberParameterErrorOptions {
422
  func_name: string;
423
  param_name?: string;
424
}
425

426
/**
427
 * Assertion version of {@link isNumberWithinRange}
428
 */
429
export function assertNumberWithinRange(
430
  value: unknown,
431
  func_name: string,
432
  min?: number,
433
  max?: number,
434
  integer?: boolean,
435
  param_name?: string,
436
): asserts value is number;
437
export function assertNumberWithinRange(
438
  value: unknown,
439
  options: AssertNumberWithinRangeOptions,
440
): asserts value is number;
441
export function assertNumberWithinRange(
442
  value: unknown,
443
  arg0: AssertNumberWithinRangeOptions | string,
444
  min?: number,
445
  max?: number,
446
  integer?: boolean,
447
  param_name?: string,
448
): asserts value is number {
449
  let options: AssertNumberWithinRangeOptions;
450

451
  if (typeof arg0 === 'string') {
38✔
452
    options = {
31✔
453
      func_name: arg0,
454
      min,
455
      max,
456
      integer: integer ?? true,
41✔
457
      param_name,
458
    };
459
  } else {
460
    options = arg0;
7✔
461
  }
462

463
  if (!isNumberWithinRange(value, options)) {
38✔
464
    throw new InvalidNumberParameterError(value, options, options.func_name, options.param_name);
12✔
465
  }
466
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc