• 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

90.37
/src/cse-machine/utils.ts
1
import type es from 'estree';
2
import { isFunction } from 'lodash';
3

4
import * as errors from '../errors/errors';
5
import { RuntimeSourceError } from '../errors/base';
6
import type {
7
  Context,
8
  Environment,
9
  Node,
10
  NodeTypeToNode,
11
  StatementSequence,
12
  Value,
13
} from '../types';
14
import * as ast from '../utils/ast/astCreator';
15
import { isIdentifier, isImportDeclaration, isVariableDeclaration } from '../utils/ast/typeGuards';
16
import assert from '../utils/assert';
17
import { extractDeclarations } from '../utils/ast/helpers';
18
import Closure from './closure';
19
import { Continuation, isCallWithCurrentContinuation } from './continuations';
20
import Heap from './heap';
21
import * as instr from './instrCreator';
22
import type { Control } from './interpreter';
23
import {
24
  type AppInstr,
25
  type ControlItem,
26
  type EnvArray,
27
  type Instr,
28
  InstrType,
29
  type InstrTypeToInstr,
30
} from './types';
31

32
/**
33
 * Typeguard for Instr to distinguish between program statements and instructions.
34
 *
35
 * @param command A ControlItem
36
 * @returns true if the ControlItem is an instruction and false otherwise.
37
 */
38
export const isInstr = (command: ControlItem): command is Instr => {
72✔
39
  return 'instrType' in command;
9,078,718✔
40
};
41

42
/**
43
 * Typeguard for Node to distinguish between program statements and instructions.
44
 *
45
 * @param command A ControlItem
46
 * @returns true if the ControlItem is a Node or StatementSequence, false if it is an instruction.
47
 */
48
export const isNode = (command: ControlItem): command is Node => {
72✔
49
  return 'type' in command;
39,817,801✔
50
};
51

52
/**
53
 * Typeguard for esReturnStatement. To verify if a Node is an esReturnStatement.
54
 *
55
 * @param node a Node
56
 * @returns true if node is an esReturnStatement, false otherwise.
57
 */
58
export const isReturnStatement = (node: Node): node is es.ReturnStatement => {
72✔
59
  return node.type === 'ReturnStatement';
30,464✔
60
};
61

62
/**
63
 * Typeguard for esIfStatement. To verify if a Node is an esIfStatement.
64
 *
65
 * @param node a Node
66
 * @returns true if node is an esIfStatement, false otherwise.
67
 */
68
export const isIfStatement = (node: Node): node is es.IfStatement => {
72✔
69
  return node.type === 'IfStatement';
12,115✔
70
};
71

72
/**
73
 * Typeguard for esBlockStatement. To verify if a Node is a block statement.
74
 *
75
 * @param node a Node
76
 * @returns true if node is an esBlockStatement, false otherwise.
77
 */
78
export const isBlockStatement = (node: Node): node is es.BlockStatement => {
72✔
79
  return node.type === 'BlockStatement';
5,299,356✔
80
};
81

82
/**
83
 * Typeguard for StatementSequence. To verify if a ControlItem is a statement sequence.
84
 *
85
 * @param node a ControlItem
86
 * @returns true if node is a StatementSequence, false otherwise.
87
 */
88
export const isStatementSequence = (node: ControlItem): node is StatementSequence => {
72✔
89
  return isNode(node) && node.type === 'StatementSequence';
6,416✔
90
};
91

92
/**
93
 * Typeguard for esRestElement. To verify if a Node is a block statement.
94
 *
95
 * @param node a Node
96
 * @returns true if node is an esRestElement, false otherwise.
97
 */
98
export const isRestElement = (node: Node): node is es.RestElement => {
72✔
99
  return node.type == 'RestElement';
610,598✔
100
};
101

102
/**
103
 * Generate a unique id, for use in environments, arrays and closures.
104
 *
105
 * @param context the context used to provide the new unique id
106
 * @returns a unique id
107
 */
108
export const uniqueId = (context: Context): string => {
72✔
109
  return `${context.runtime.objectCount++}`;
476,261✔
110
};
111

112
/**
113
 * Returns whether `item` is an array with `id` and `environment` properties already attached.
114
 */
115
export const isEnvArray = (item: any): item is EnvArray => {
72✔
116
  return (
200✔
117
    Array.isArray(item) &&
425✔
118
    {}.hasOwnProperty.call(item, 'id') &&
119
    {}.hasOwnProperty.call(item, 'environment')
120
  );
121
};
122

123
/**
124
 * Returns whether `item` is a non-closure function that returns a stream.
125
 * If the function has been called already and we have the result, we can
126
 * pass it in here as a 2nd argument for stronger checking
127
 */
128
export const isStreamFn = (item: any, result?: any): result is [any, () => any] => {
72✔
129
  if (result == null || !Array.isArray(result) || result.length !== 2) return false;
642✔
130
  return (
162✔
131
    isFunction(item) &&
132
    !(item instanceof Closure) &&
133
    (item.name === 'stream' || {}.hasOwnProperty.call(item, 'environment'))
134
  );
135
};
136

137
/**
138
 * Adds the properties `id` and `environment` to the given array, and adds the array to the
139
 * current environment's heap. Adds the array to the heap of `envOverride` instead if it's defined.
140
 *
141
 * @param context the context used to provide the current environment and new unique id
142
 * @param array the array to attach properties to, and for addition to the heap
143
 */
144
export const handleArrayCreation = (
72✔
145
  context: Context,
146
  array: any[],
147
  envOverride?: Environment,
148
): void => {
149
  const environment = envOverride ?? currentEnvironment(context);
1,757✔
150
  // Both id and environment are non-enumerable so iterating
151
  // through the array will not return these values
152
  Object.defineProperties(array, {
1,757✔
153
    id: { value: uniqueId(context) },
154
    // Make environment writable as there are cases on the frontend where
155
    // environments of objects need to be modified
156
    environment: { value: environment, writable: true },
157
  });
158
  environment.heap.add(array as EnvArray);
1,757✔
159
};
160

161
/**
162
 * A helper function for handling sequences of statements.
163
 * Statements must be pushed in reverse order, and each statement is separated by a pop
164
 * instruction so that only the result of the last statement remains on stash.
165
 * Value producing statements have an extra pop instruction.
166
 *
167
 * @param seq Array of statements.
168
 * @returns Array of commands to be pushed into control.
169
 */
170
export const handleSequence = (seq: es.Program['body']): ControlItem[] => {
72✔
171
  const result: ControlItem[] = [];
5,027✔
172
  let valueProduced = false;
5,027✔
173
  for (const command of seq) {
5,027✔
174
    if (!isImportDeclaration(command)) {
28,387✔
175
      if (valueProducing(command)) {
28,386✔
176
        // Value producing statements have an extra pop instruction
177
        if (valueProduced) {
4,911✔
178
          result.push(instr.popInstr(command));
315✔
179
        } else {
180
          valueProduced = true;
4,596✔
181
        }
182
      }
183
      result.push(command);
28,386✔
184
    }
185
  }
186
  // Push statements in reverse order
187
  return result.reverse();
5,027✔
188
};
189

190
/**
191
 * This function is used for ConditionalExpressions and IfStatements, to create the sequence
192
 * of control items to be added.
193
 */
194
export const reduceConditional = (
72✔
195
  node: es.IfStatement | es.ConditionalExpression,
196
): ControlItem[] => {
197
  return [instr.branchInstr(node.consequent, node.alternate, node), node.test];
415,475✔
198
};
199

200
/**
201
 * To determine if a control item is value producing. JavaScript distinguishes value producing
202
 * statements and non-value producing statements.
203
 * Refer to https://sourceacademy.nus.edu.sg/sicpjs/4.1.2 exercise 4.8.
204
 *
205
 * @param command Control item to determine if it is value producing.
206
 * @returns true if it is value producing, false otherwise.
207
 */
208
export const valueProducing = (command: Node): boolean => {
72✔
209
  const type = command.type;
591,669✔
210
  return (
591,669✔
211
    type !== 'VariableDeclaration' &&
3,565,764✔
212
    type !== 'FunctionDeclaration' &&
213
    type !== 'ContinueStatement' &&
214
    type !== 'BreakStatement' &&
215
    type !== 'DebuggerStatement' &&
216
    (type !== 'BlockStatement' || command.body.some(valueProducing))
217
  );
218
};
219

220
/**
221
 * To determine if a control item changes the environment.
222
 * There is a change in the environment when
223
 *  1. pushEnvironment() is called when creating a new frame, if there are variable declarations.
224
 *     Called in Program, BlockStatement, and Application instructions.
225
 *  2. there is an assignment.
226
 *     Called in Assignment and Array Assignment instructions.
227
 *  3. a new object is created.
228
 *     Called in ExpressionStatement that contains an ArrowFunctionExpression, or an ArrowFunctionExpression
229
 *
230
 * @param command Control item to check against.
231
 * @returns true if it changes the environment, false otherwise.
232
 */
233
export const envChanging = (command: ControlItem): boolean => {
72✔
234
  if (isNode(command)) {
7,154,302✔
235
    const type = command.type;
5,211,872✔
236
    return (
5,211,872✔
237
      type === 'Program' ||
20,838,230✔
238
      type === 'BlockStatement' ||
239
      type === 'ArrowFunctionExpression' ||
240
      (type === 'ExpressionStatement' && command.expression.type === 'ArrowFunctionExpression')
241
    );
242
  } else if (isInstr(command)) {
1,942,430!
243
    const type = command.instrType;
1,942,430✔
244
    return (
1,942,430✔
245
      type === InstrType.ENVIRONMENT ||
9,815,405✔
246
      type === InstrType.ARRAY_LITERAL ||
247
      type === InstrType.ASSIGNMENT ||
248
      type === InstrType.ARRAY_ASSIGNMENT ||
249
      (type === InstrType.APPLICATION && (command as AppInstr).numOfArgs > 0)
250
    );
251
  } else {
252
    // TODO deal with scheme control items
253
    // for now, as per the CSE machine paper,
254
    // we decide to ignore environment optimizations
255
    // for scheme control items :P
256
    return true;
×
257
  }
258
};
259

260
// TODO: This type guard does not seem to be doing what it thinks its doing
261
/**
262
 * To determine if the function is simple.
263
 * Simple functions contain a single return statement.
264
 *
265
 * @param node The function to check against.
266
 * @returns true if the function is simple, false otherwise.
267
 */
268
export const isSimpleFunction = (node: any) => {
72✔
269
  if (node.body.type !== 'BlockStatement' && node.body.type !== 'StatementSequence') {
550,542!
270
    return true;
×
271
  } else {
272
    const block = node.body;
550,542✔
273
    return block.body.length === 1 && block.body[0].type === 'ReturnStatement';
550,542✔
274
  }
275
};
276

277
/**
278
 * Environments
279
 */
280

281
export const currentEnvironment = (context: Context) => context.runtime.environments[0];
1,870,817✔
282

283
export const createEnvironment = (
72✔
284
  context: Context,
285
  closure: Closure,
286
  args: Value[],
287
  callExpression: es.CallExpression,
288
): Environment => {
289
  const environment: Environment = {
450,434✔
290
    name: isIdentifier(callExpression.callee)
450,434✔
291
      ? callExpression.callee.name
292
      : (closure.declaredName ?? closure.functionName),
12✔
293
    tail: closure.environment,
294
    head: {},
295
    heap: new Heap(),
296
    id: uniqueId(context),
297
    callExpression: {
298
      ...callExpression,
299
      arguments: args.map(ast.primitive),
300
    },
301
  };
302
  closure.node.params.forEach((param, index) => {
450,434✔
303
    if (isRestElement(param)) {
610,598✔
304
      const array = args.slice(index);
2✔
305
      handleArrayCreation(context, array, environment);
2✔
306
      environment.head[(param.argument as es.Identifier).name] = array;
2✔
307
    } else {
308
      environment.head[(param as es.Identifier).name] = args[index];
610,596✔
309
    }
310
  });
311
  return environment;
450,434✔
312
};
313

314
export const popEnvironment = (context: Context) => context.runtime.environments.shift();
108,736✔
315

316
export const pushEnvironment = (context: Context, environment: Environment) => {
72✔
317
  context.runtime.environments.unshift(environment);
455,205✔
318
  context.runtime.environmentTree.insert(environment);
455,205✔
319
};
320

321
export const createBlockEnvironment = (
72✔
322
  context: Context,
323
  name = 'blockEnvironment',
4,776✔
324
): Environment => {
325
  return {
4,776✔
326
    name,
327
    tail: currentEnvironment(context),
328
    head: {},
329
    heap: new Heap(),
330
    id: uniqueId(context),
331
  };
332
};
333

334
export const createProgramEnvironment = (context: Context, isPrelude: boolean): Environment => {
72✔
335
  return createBlockEnvironment(context, isPrelude ? 'prelude' : 'programEnvironment');
847✔
336
};
337

338
/**
339
 * Variables
340
 */
341

342
const UNASSIGNED_CONST = Symbol('const declaration');
72✔
343
const UNASSIGNED_LET = Symbol('let declaration');
72✔
344

345
export function declareIdentifier(
346
  context: Context,
347
  name: string,
348
  node:
349
    | es.Declaration
350
    | es.ImportSpecifier
351
    | es.ImportDefaultSpecifier
352
    | es.ImportNamespaceSpecifier,
353
  environment: Environment,
354
  constant: boolean = false,
23,455✔
355
) {
356
  if (environment.head.hasOwnProperty(name)) {
23,455!
357
    const descriptors = Object.getOwnPropertyDescriptors(environment.head);
×
358

NEW
359
    if (isVariableDeclaration(node)) {
×
NEW
360
      return handleRuntimeError(
×
361
        context,
362
        new errors.VariableRedeclarationError(node, name, !!descriptors[name].writable),
363
      );
364
    }
365

NEW
366
    assert(descriptors[name].writable === false, `${node.type} should not be reassignable`);
×
367

UNCOV
368
    return handleRuntimeError(
×
369
      context,
370
      new errors.VariableRedeclarationError(node, name, descriptors[name].writable),
371
    );
372
  }
373
  environment.head[name] = constant ? UNASSIGNED_CONST : UNASSIGNED_LET;
23,455✔
374
  return environment;
23,455✔
375
}
376

377
function declareVariables(
378
  context: Context,
379
  node: es.VariableDeclaration,
380
  environment: Environment,
381
) {
382
  // Retrieve declaration type from node
383
  const constant = node.kind === 'const';
4,305✔
384
  for (const id of extractDeclarations(node)) {
4,305✔
385
    declareIdentifier(context, id.name, node, environment, constant);
4,305✔
386
  }
387
}
388

389
export function declareFunctionsAndVariables(
390
  context: Context,
391
  node: es.BlockStatement | es.Program,
392
  environment: Environment,
393
) {
394
  for (const statement of node.body) {
4,764✔
395
    switch (statement.type) {
27,859✔
396
      case 'VariableDeclaration':
397
        declareVariables(context, statement, environment);
4,305✔
398
        break;
4,305✔
399
      case 'FunctionDeclaration':
400
        // FunctionDeclaration is always of type constant
401
        declareIdentifier(context, statement.id.name, statement, environment, true);
19,150✔
402
        break;
19,150✔
403
    }
404
  }
405
}
406

407
export function defineVariable(
408
  context: Context,
409
  name: string,
410
  value: Value,
411
  constant = false,
23,361✔
412
  node:
413
    | es.VariableDeclaration
414
    | es.ImportSpecifier
415
    | es.ImportDefaultSpecifier
416
    | es.ImportNamespaceSpecifier
417
    | es.FunctionDeclaration,
418
) {
419
  const environment = currentEnvironment(context);
23,361✔
420

421
  if (environment.head[name] !== UNASSIGNED_CONST && environment.head[name] !== UNASSIGNED_LET) {
23,361!
NEW
422
    return handleRuntimeError(
×
423
      context,
424
      new errors.VariableRedeclarationError(node, name, !constant),
425
    );
426
  }
427

428
  if (constant && value instanceof Closure) {
23,361✔
429
    value.declaredName = name;
19,245✔
430
  }
431

432
  Object.defineProperty(environment.head, name, {
23,361✔
433
    value,
434
    writable: !constant,
435
    enumerable: true,
436
  });
437

438
  return environment;
23,361✔
439
}
440

441
export const getVariable = (context: Context, name: string, node: es.Identifier) => {
72✔
442
  let environment: Environment | null = currentEnvironment(context);
1,471,650✔
443
  while (environment) {
1,471,650✔
444
    if (environment.head.hasOwnProperty(name)) {
2,033,720✔
445
      if (
1,471,650✔
446
        environment.head[name] === UNASSIGNED_CONST ||
2,943,297✔
447
        environment.head[name] === UNASSIGNED_LET
448
      ) {
449
        return handleRuntimeError(context, new errors.UnassignedVariableError(name, node));
3✔
450
      } else {
451
        return environment.head[name];
1,471,647✔
452
      }
453
    } else {
454
      environment = environment.tail;
562,070✔
455
    }
456
  }
NEW
457
  return handleRuntimeError(context, new errors.UndefinedVariableError(name, node));
×
458
};
459

460
export const setVariable = (
72✔
461
  context: Context,
462
  name: string,
463
  value: any,
464
  node: es.AssignmentExpression,
465
) => {
466
  let environment: Environment | null = currentEnvironment(context);
3,481✔
467
  while (environment) {
3,481✔
468
    if (environment.head.hasOwnProperty(name)) {
8,254✔
469
      if (
3,481✔
470
        environment.head[name] === UNASSIGNED_CONST ||
6,962✔
471
        environment.head[name] === UNASSIGNED_LET
472
      ) {
473
        break;
1✔
474
      }
475
      const descriptors = Object.getOwnPropertyDescriptors(environment.head);
3,480✔
476
      if (descriptors[name].writable) {
3,480✔
477
        environment.head[name] = value;
3,476✔
478
        return undefined;
3,476✔
479
      }
480
      return handleRuntimeError(context, new errors.ConstAssignmentError(node, name));
4✔
481
    } else {
482
      environment = environment.tail;
4,773✔
483
    }
484
  }
485
  return handleRuntimeError(context, new errors.UndefinedVariableError(name, node));
1✔
486
};
487

488
export function handleRuntimeError(context: Context, error: RuntimeSourceError<any>) {
489
  context.errors.push(error);
66✔
490
  throw error;
66✔
491
}
492

493
export const checkNumberOfArguments = (
72✔
494
  context: Context,
495
  callee: Closure | Value,
496
  args: Value[],
497
  exp: es.CallExpression,
498
) => {
499
  if (callee instanceof Closure) {
550,561✔
500
    // User-defined or Pre-defined functions
501
    const params = callee.node.params;
550,555✔
502
    const hasVarArgs = params[params.length - 1]?.type === 'RestElement';
550,555✔
503
    if (hasVarArgs ? params.length - 1 > args.length : params.length !== args.length) {
550,555✔
504
      return handleRuntimeError(
13✔
505
        context,
506
        new errors.InvalidNumberOfArgumentsError(
507
          exp,
508
          hasVarArgs ? params.length - 1 : params.length,
13✔
509
          args.length,
510
          undefined,
511
          hasVarArgs,
512
        ),
513
      );
514
    }
515
  } else if (isCallWithCurrentContinuation(callee)) {
6✔
516
    // call/cc should have a single argument
517
    if (args.length !== 1) {
5✔
518
      return handleRuntimeError(
2✔
519
        context,
520
        new errors.InvalidNumberOfArgumentsError(exp, 1, args.length, undefined, false),
521
      );
522
    }
523
    return undefined;
3✔
524
  } else if (callee instanceof Continuation) {
1!
525
    // Continuations have variadic arguments,
526
    // and so we can let it pass
527
    // TODO: in future, if we can somehow check the number of arguments
528
    // expected by the continuation, we can add a check here.
529
    return undefined;
1✔
530
  } else {
531
    // Pre-built functions
UNCOV
532
    const hasVarArgs = callee.minArgsNeeded != undefined;
×
UNCOV
533
    if (hasVarArgs ? callee.minArgsNeeded > args.length : callee.length !== args.length) {
×
UNCOV
534
      return handleRuntimeError(
×
535
        context,
536
        new errors.InvalidNumberOfArgumentsError(
537
          exp,
538
          hasVarArgs ? callee.minArgsNeeded : callee.length,
×
539
          args.length,
540
          undefined,
541
          hasVarArgs,
542
        ),
543
      );
544
    }
545
  }
546
  return undefined;
550,542✔
547
};
548

549
/**
550
 * This function can be used to check for a stack overflow.
551
 * The current limit is set to be a control size of 1.0 x 10^5, if the control
552
 * flows beyond this limit an error is thrown.
553
 * This corresponds to about 10mb of space according to tests ran.
554
 */
555
export const checkStackOverFlow = (context: Context, control: Control) => {
72✔
556
  if (control.size() > 100000) {
551,241✔
557
    const stacks: es.CallExpression[] = [];
4✔
558
    let counter = 0;
4✔
559
    for (
4✔
560
      let i = 0;
4✔
561
      counter < errors.MaximumStackLimitExceededError.MAX_CALLS_TO_SHOW &&
16✔
562
      i < context.runtime.environments.length;
563
      i++
564
    ) {
565
      if (context.runtime.environments[i].callExpression) {
12!
566
        stacks.unshift(context.runtime.environments[i].callExpression!);
12✔
567
        counter++;
12✔
568
      }
569
    }
570
    handleRuntimeError(
4✔
571
      context,
572
      new errors.MaximumStackLimitExceededError(context.runtime.nodes[0], stacks),
573
    );
574
  }
575
};
576

577
/**
578
 * Checks whether an `if` statement returns in every possible branch.
579
 * @param body The `if` statement to be checked
580
 * @return `true` if every branch has a return statement, else `false`.
581
 */
582
export const hasReturnStatementIf = (statement: es.IfStatement): boolean => {
72✔
583
  let hasReturn = true;
2,916✔
584
  // Parser enforces that if/else have braces (block statement)
585
  hasReturn = hasReturn && hasReturnStatement(statement.consequent as es.BlockStatement);
2,916✔
586
  if (statement.alternate) {
2,916✔
587
    if (isIfStatement(statement.alternate)) {
2,912!
588
      hasReturn = hasReturn && hasReturnStatementIf(statement.alternate);
×
589
    } else if (isBlockStatement(statement.alternate) || isStatementSequence(statement.alternate)) {
2,912!
590
      hasReturn = hasReturn && hasReturnStatement(statement.alternate);
2,912✔
591
    }
592
  }
593
  return hasReturn;
2,916✔
594
};
595

596
/**
597
 * Checks whether a block returns in every possible branch.
598
 * @param body The block to be checked
599
 * @return `true` if every branch has a return statement, else `false`.
600
 */
601
export const hasReturnStatement = (block: es.BlockStatement | StatementSequence): boolean => {
72✔
602
  let hasReturn = false;
24,584✔
603
  for (const statement of block.body) {
24,584✔
604
    if (isReturnStatement(statement)) {
30,464✔
605
      hasReturn = true;
21,261✔
606
    } else if (isIfStatement(statement)) {
9,203✔
607
      // Parser enforces that if/else have braces (block statement)
608
      hasReturn = hasReturn || hasReturnStatementIf(statement);
2,918✔
609
    } else if (isBlockStatement(statement) || isStatementSequence(statement)) {
6,285✔
610
      hasReturn = hasReturn && hasReturnStatement(statement);
1!
611
    }
612
  }
613
  return hasReturn;
24,584✔
614
};
615

616
function nodeVisitor(node: Node, type: Node['type']): boolean {
617
  switch (node.type) {
12,964✔
618
    case 'BlockStatement':
619
    case 'StatementSequence':
620
    case 'Program':
621
      return node.body.some(each => nodeVisitor(each, type));
10,709✔
622
    case 'IfStatement': {
623
      const { consequent, alternate } = node;
64✔
624
      if (nodeVisitor(consequent, type)) return true;
64✔
625

626
      return !!alternate && nodeVisitor(alternate, type);
29✔
627
    }
628
    default:
629
      return node.type === type;
6,467✔
630
  }
631
}
632

633
/**
634
 * Checks whether a block OR any of its child blocks has a `break` statement.
635
 * @param body The block to be checked
636
 * @return `true` if there is a `break` statement, else `false`.
637
 */
638
export const hasBreakStatement = (block: es.BlockStatement | StatementSequence): boolean => {
72✔
639
  return nodeVisitor(block, 'BreakStatement');
359✔
640
};
641

642
/**
643
 * Checks whether a block OR any of its child blocks has a `continue` statement.
644
 * @param body The block to be checked
645
 * @return `true` if there is a `continue` statement, else `false`.
646
 */
647
export const hasContinueStatement = (block: es.BlockStatement | StatementSequence): boolean => {
72✔
648
  return nodeVisitor(block, 'ContinueStatement');
1,817✔
649
};
650

651
/**
652
 * Utility type for getting all the keys of a ControlItem that have values
653
 * that are assignable to Nodes
654
 */
655
type GetNodeKeys<T extends ControlItem> = {
656
  [K in keyof T as T[K] extends Node | null | undefined ? K : never]: K;
657
};
658
/**
659
 * Extracts all the keys of a ControlItem that have values that are assignable to Nodes
660
 * as a union
661
 */
662
type KeysOfNodeProperties<T extends ControlItem> = GetNodeKeys<T>[keyof GetNodeKeys<T>];
663

664
/**
665
 * To provide a specification on how to calculate whether a ControlItem is env dependent or not:
666
 * - If a boolean is provided, that value is used directly
667
 * - If a string is provided, it is treated as the name of a property. `isEnvDependent` is then called on the value of that property.
668
 * - If an array of strings is provided, all values are treated as names of properties and `isEnvDependent` is called on all
669
 * their values
670
 * - If a function is provided, the function is called with the node and the return value is used
671
 */
672
type EnvDependentCalculator<T extends ControlItem> =
673
  | ((item: T) => boolean)
674
  | boolean
675
  | KeysOfNodeProperties<T>
676
  | KeysOfNodeProperties<T>[];
677

678
type EnvCalculators = {
679
  [K in Node['type']]?: EnvDependentCalculator<NodeTypeToNode<K>>;
680
} & {
681
  [K in InstrType]?: EnvDependentCalculator<InstrTypeToInstr<K>>;
682
};
683

684
const envCalculators: EnvCalculators = {
72✔
685
  ArrayExpression: ({ elements }) => elements.some(isEnvDependent),
322✔
686
  ArrowFunctionExpression: true,
687
  AssignmentExpression: ['left', 'right'],
688
  BlockStatement: ({ body }) => body.some(isEnvDependent),
1,424✔
689
  BinaryExpression: ['left', 'right'],
690
  BreakStatement: false,
691
  CallExpression: node => [node.callee, ...node.arguments].some(isEnvDependent),
399✔
692
  ConditionalExpression: ['alternate', 'consequent', 'test'],
693
  ContinueStatement: false,
694
  DebuggerStatement: false,
695
  ExpressionStatement: 'expression',
696
  ForStatement: ['body', 'init', 'test', 'update'],
697
  FunctionDeclaration: true,
698
  Identifier: true,
699
  IfStatement: ['alternate', 'consequent', 'test'],
700
  ImportDeclaration: ({ specifiers }) => specifiers.some(isEnvDependent),
1✔
701
  ImportDefaultSpecifier: true,
702
  ImportSpecifier: true,
703
  Literal: false,
704
  LogicalExpression: ['left', 'right'],
705
  MemberExpression: ['object', 'property'],
706
  Program: ({ body }) => body.some(isEnvDependent),
934✔
707
  ReturnStatement: 'argument',
708
  StatementSequence: ({ body }) => body.some(isEnvDependent),
266,763✔
709
  UnaryExpression: 'argument',
710
  VariableDeclaration: true,
711
  WhileStatement: ['body', 'test'],
712

713
  //Instruction
714
  [InstrType.APPLICATION]: true,
715
  [InstrType.ARRAY_ACCESS]: false,
716
  [InstrType.ARRAY_ASSIGNMENT]: false,
717
  [InstrType.ARRAY_LITERAL]: true,
718
  [InstrType.ASSIGNMENT]: true,
719
  [InstrType.BINARY_OP]: false,
720
  [InstrType.BRANCH]: ['alternate', 'consequent'],
721
  [InstrType.BREAK_MARKER]: false,
722
  [InstrType.CONTINUE]: false,
723
  [InstrType.CONTINUE_MARKER]: false,
724
  [InstrType.ENVIRONMENT]: false,
725
  [InstrType.MARKER]: false,
726
  [InstrType.POP]: false,
727
  [InstrType.RESET]: false,
728
  [InstrType.SPREAD]: false,
729
  [InstrType.UNARY_OP]: false,
730
  [InstrType.WHILE]: ['body', 'test'],
731
  [InstrType.FOR]: ['body', 'init', 'test', 'update'],
732
};
733
/**
734
 * Checks whether the evaluation of the given control item depends on the current environment.
735
 * The item is also considered environment dependent if its evaluation introduces
736
 * environment dependent items
737
 * @param item The control item to be checked
738
 * @return `true` if the item is environment depedent, else `false`.
739
 */
740

741
export function isEnvDependent(item: ControlItem | null | undefined): boolean {
742
  if (item === null || item === undefined) {
15,868,274✔
743
    return false;
43✔
744
  }
745

746
  // If result is already calculated, return it
747
  if (item.isEnvDependent !== undefined) {
15,868,231✔
748
    return item.isEnvDependent;
13,010,134✔
749
  }
750

751
  let calculator;
752
  if (isNode(item)) {
2,858,097✔
753
    calculator = envCalculators[item.type];
347,818✔
754
  } else if (isInstr(item)) {
2,510,279!
755
    calculator = envCalculators[item.instrType];
2,510,279✔
756
  }
757

758
  switch (typeof calculator) {
2,858,097✔
759
    case 'boolean': {
760
      item.isEnvDependent = calculator;
2,159,146✔
761
      break;
2,159,146✔
762
    }
763
    case 'string': {
764
      // @ts-expect-error Indexing with an arbitrary index
765
      item.isEnvDependent = isEnvDependent(item[calculator]);
710✔
766
      break;
710✔
767
    }
768
    case 'function': {
769
      // @ts-expect-error Function parameter gets narrowed to never
770
      item.isEnvDependent = calculator(item);
269,843✔
771
      break;
269,843✔
772
    }
773
    case 'undefined': {
774
      item.isEnvDependent = false;
6✔
775
      break;
6✔
776
    }
777
    default: {
778
      if (!Array.isArray(calculator)) throw new Error(`Invalid setter for ${item}: ${calculator}`);
428,392!
779

780
      // @ts-expect-error Indexing with an arbitrary index
781
      item.isEnvDependent = calculator.some(each => isEnvDependent(item[each]));
433,528✔
782
      break;
428,392✔
783
    }
784
  }
785

786
  return item.isEnvDependent;
2,858,097✔
787
}
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