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

source-academy / js-slang / 23995741899

05 Apr 2026 06:14AM UTC coverage: 77.093% (+0.002%) from 77.091%
23995741899

push

github

web-flow
Upgrade to TypeScript 6 and Prettier improvements (#1936)

* Upgrade TypeScript to v6

* Fix import source

* Fix tsconfig

* Fix preexisting type errors

* Remove scm-slang

* Bump node types

* Fix tsconfig

* Fix node types specifier

* Enable trailing commas

* Enable semicolons

* Check and commit files with changed line numbers

* Update Yarn to 4.13.0

* Remove unneeded sicp package deps

3112 of 4282 branches covered (72.68%)

Branch coverage included in aggregate %.

3761 of 5218 new or added lines in 152 files covered. (72.08%)

26 existing lines in 9 files now uncovered.

7136 of 9011 relevant lines covered (79.19%)

175254.05 hits per line

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

86.88
/src/typeChecker/typeErrorChecker.ts
1
import { parse as babelParse } from '@babel/parser';
2
import type es from 'estree';
3
import { cloneDeep, isEqual } from 'lodash';
4

5
import {
6
  ConstNotAssignableTypeError,
7
  DuplicateTypeAliasError,
8
  FunctionShouldHaveReturnValueError,
9
  InvalidArrayAccessTypeError,
10
  InvalidIndexTypeError,
11
  InvalidNumberOfArgumentsTypeError,
12
  InvalidNumberOfTypeArgumentsForGenericTypeError,
13
  TypeAliasNameNotAllowedError,
14
  TypecastError,
15
  TypeMismatchError,
16
  TypeNotAllowedError,
17
  TypeNotCallableError,
18
  TypeNotFoundError,
19
  TypeNotGenericError,
20
  TypeParameterNameNotAllowedError,
21
  UndefinedVariableTypeError,
22
} from '../errors/typeErrors';
23
import { Chapter } from '../langs';
24
import {
25
  type BindableType,
26
  type Context,
27
  disallowedTypes,
28
  type Pair,
29
  type PrimitiveType,
30
  type SArray,
31
  type TSAllowedTypes,
32
  type TSBasicType,
33
  type TSDisallowedTypes,
34
  type Type,
35
  type TypeEnvironment,
36
  type Variable,
37
} from '../types';
38
import { TypecheckError } from './internalTypeErrors';
39
import { parseTreeTypesPrelude } from './parseTreeTypes.prelude';
40
import type * as tsEs from './tsESTree';
41
import {
42
  formatTypeString,
43
  getTypeOverrides,
44
  lookupDeclKind,
45
  lookupType,
46
  lookupTypeAlias,
47
  pushEnv,
48
  RETURN_TYPE_IDENTIFIER,
49
  setDeclKind,
50
  setType,
51
  setTypeAlias,
52
  tAny,
53
  tArray,
54
  tBool,
55
  tForAll,
56
  tFunc,
57
  tList,
58
  tLiteral,
59
  tNull,
60
  tNumber,
61
  tPair,
62
  tPrimitive,
63
  tStream,
64
  tString,
65
  tUndef,
66
  tUnion,
67
  tVar,
68
  tVoid,
69
  typeAnnotationKeywordToBasicTypeMap,
70
} from './utils';
71
// import { ModuleNotFoundError } from '../modules/errors'
72

73
// Context and type environment are saved as global variables so that they are not passed between functions excessively
74
let context: Context = {} as Context;
65✔
75
let env: TypeEnvironment = [];
65✔
76

77
/**
78
 * Entry function for type error checker.
79
 * Checks program for type errors, and returns the program with all TS-related nodes removed.
80
 */
81
export function checkForTypeErrors(program: tsEs.Program, inputContext: Context): es.Program {
82
  // Set context as global variable
83
  context = inputContext;
139✔
84
  // Deep copy type environment to avoid modifying type environment in the context,
85
  // which might affect the type inference checker
86
  env = cloneDeep(context.typeEnvironment);
139✔
87
  // Override predeclared function types
88
  for (const [name, type] of getTypeOverrides(context.chapter)) {
139✔
89
    setType(name, type, env);
3,941✔
90
  }
91
  if (context.chapter >= 4) {
139✔
92
    // Add parse tree types to type environment
93
    const source4Types = babelParse(parseTreeTypesPrelude, {
54✔
94
      sourceType: 'module',
95
      plugins: ['typescript', 'estree'],
96
    }).program as unknown as tsEs.Program;
97
    typeCheckAndReturnType(source4Types);
54✔
98
  }
99
  try {
139✔
100
    typeCheckAndReturnType(program);
139✔
101
  } catch (error) {
102
    // Catch-all for thrown errors
103
    // (either errors that cause early termination or errors that should not be reached logically)
104
    // console.error(error)
105
    context.errors.push(
1✔
106
      error instanceof TypecheckError
1!
107
        ? error
108
        : new TypecheckError(
109
            program,
110
            'Uncaught error during typechecking, report this to the administrators!\n' +
111
              error.message,
112
          ),
113
    );
114
  }
115
  // Reset global variables
116
  context = {} as Context;
139✔
117
  env = [];
139✔
118
  return removeTSNodes(program);
139✔
119
}
120

121
/**
122
 * Recurses through the given node to check for any type errors,
123
 * then returns the node's inferred/declared type.
124
 * Any errors found are added to the context.
125
 */
126
function typeCheckAndReturnType(node: tsEs.Node): Type {
127
  switch (node.type) {
4,330!
128
    case 'Literal': {
129
      // Infers type
130
      if (node.value === undefined) {
533!
NEW
131
        return tUndef;
×
132
      }
133
      if (node.value === null) {
533✔
134
        // For Source 1, skip typecheck as null literals will be handled by the noNull rule,
135
        // which is run after typechecking
136
        return context.chapter === Chapter.SOURCE_1 ? tAny : tNull;
14✔
137
      }
138
      if (
519!
139
        typeof node.value !== 'string' &&
972✔
140
        typeof node.value !== 'number' &&
141
        typeof node.value !== 'boolean'
142
      ) {
143
        // Skip typecheck as unspecified literals will be handled by the noUnspecifiedLiteral rule,
144
        // which is run after typechecking
NEW
145
        return tAny;
×
146
      }
147
      // Casting is safe here as above check already narrows type to string, number or boolean
148
      return tPrimitive(typeof node.value as PrimitiveType, node.value);
519✔
149
    }
150
    case 'TemplateLiteral': {
151
      // Quasis array should only have one element as
152
      // string interpolation is not allowed in Source
NEW
153
      return tPrimitive('string', node.quasis[0].value.raw);
×
154
    }
155
    case 'Identifier': {
156
      const varName = node.name;
546✔
157
      const varType = lookupTypeAndRemoveForAllAndPredicateTypes(varName);
546✔
158
      if (varType) {
546✔
159
        return varType;
545✔
160
      } else {
161
        context.errors.push(new UndefinedVariableTypeError(node, varName));
1✔
162
        return tAny;
1✔
163
      }
164
    }
165
    case 'RestElement':
166
    case 'SpreadElement':
167
      // TODO: Add support for rest and spread element
NEW
168
      return tAny;
×
169
    case 'Program':
170
    case 'BlockStatement': {
171
      let returnType: Type = tVoid;
325✔
172
      pushEnv(env);
325✔
173

174
      if (node.type === 'Program') {
325✔
175
        // Import statements should only exist in program body
176
        handleImportDeclarations(node);
218✔
177
      }
178

179
      // Add all declarations in the current scope to the environment first
180
      addTypeDeclarationsToEnvironment(node);
325✔
181

182
      // Check all statements in program/block body
183
      for (const stmt of node.body) {
325✔
184
        if (stmt.type === 'IfStatement' || stmt.type === 'ReturnStatement') {
2,431✔
185
          returnType = typeCheckAndReturnType(stmt);
89✔
186
          if (stmt.type === 'ReturnStatement') {
89✔
187
            // If multiple return statements are present, only take the first type
188
            break;
79✔
189
          }
190
        } else {
191
          typeCheckAndReturnType(stmt);
2,342✔
192
        }
193
      }
194
      if (node.type === 'BlockStatement') {
323✔
195
        // Types are saved for programs, but not for blocks
196
        env.pop();
106✔
197
      }
198

199
      return returnType;
323✔
200
    }
201
    case 'ExpressionStatement': {
202
      // Check expression
203
      return typeCheckAndReturnType(node.expression);
92✔
204
    }
205
    case 'ConditionalExpression':
206
    case 'IfStatement': {
207
      // Typecheck predicate against boolean
208
      const predicateType = typeCheckAndReturnType(node.test);
23✔
209
      checkForTypeMismatch(node, predicateType, tBool);
23✔
210

211
      // Return type is union of consequent and alternate type
212
      const consType = typeCheckAndReturnType(node.consequent);
23✔
213
      const altType = node.alternate ? typeCheckAndReturnType(node.alternate) : tUndef;
23!
214
      return mergeTypes(node, consType, altType);
23✔
215
    }
216
    case 'UnaryExpression': {
217
      const argType = typeCheckAndReturnType(node.argument);
17✔
218
      const operator = node.operator;
17✔
219
      switch (operator) {
17!
220
        case '-':
221
          // Typecheck against number
222
          checkForTypeMismatch(node, argType, tNumber);
5✔
223
          return tNumber;
5✔
224
        case '!':
225
          // Typecheck against boolean
226
          checkForTypeMismatch(node, argType, tBool);
5✔
227
          return tBool;
5✔
228
        case 'typeof':
229
          // No checking needed, typeof operation can be used on any type
230
          return tString;
7✔
231
        default:
NEW
232
          throw new TypecheckError(node, 'Unknown operator');
×
233
      }
234
    }
235
    case 'BinaryExpression': {
236
      return typeCheckAndReturnBinaryExpressionType(node);
116✔
237
    }
238
    case 'LogicalExpression': {
239
      // Typecheck left type against boolean
240
      const leftType = typeCheckAndReturnType(node.left);
14✔
241
      checkForTypeMismatch(node, leftType, tBool);
14✔
242

243
      // Return type is union of boolean and right type
244
      const rightType = typeCheckAndReturnType(node.right);
14✔
245
      return mergeTypes(node, tBool, rightType);
14✔
246
    }
247
    case 'ArrowFunctionExpression': {
248
      return typeCheckAndReturnArrowFunctionType(node);
84✔
249
    }
250
    case 'FunctionDeclaration':
251
      if (node.id === null) {
69!
252
        // Block should not be reached since node.id is only null when function declaration
253
        // is part of `export default function`, which is not used in Source
NEW
254
        throw new TypecheckError(node, 'Function declaration should always have an identifier');
×
255
      }
256

257
      // Only identifiers/rest elements are used as function params in Source
258
      const params = node.params.filter(
69✔
259
        (param): param is tsEs.Identifier | tsEs.RestElement =>
260
          param.type === 'Identifier' || param.type === 'RestElement',
65!
261
      );
262
      if (params.length !== node.params.length) {
69!
NEW
263
        throw new TypecheckError(node, 'Unknown function parameter type');
×
264
      }
265
      const fnName = node.id.name;
69✔
266
      const expectedReturnType = getTypeAnnotationType(node.returnType);
69✔
267

268
      // If the function has variable number of arguments, set function type as any
269
      // TODO: Add support for variable number of function arguments
270
      const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
69✔
271
      if (hasVarArgs) {
69!
NEW
272
        setType(fnName, tAny, env);
×
NEW
273
        return tUndef;
×
274
      }
275

276
      const types = getParamTypes(params);
69✔
277
      // Return type will always be last item in types array
278
      types.push(expectedReturnType);
69✔
279
      const fnType = tFunc(...types);
69✔
280

281
      // Typecheck function body, creating new environment to store arg types, return type and function type
282
      pushEnv(env);
69✔
283
      params.forEach((param: tsEs.Identifier) => {
69✔
284
        setType(param.name, getTypeAnnotationType(param.typeAnnotation), env);
65✔
285
      });
286
      // Set unique identifier so that typechecking can be carried out for return statements
287
      setType(RETURN_TYPE_IDENTIFIER, expectedReturnType, env);
69✔
288
      setType(fnName, fnType, env);
69✔
289
      const actualReturnType = typeCheckAndReturnType(node.body);
69✔
290
      env.pop();
69✔
291

292
      if (
69✔
293
        isEqual(actualReturnType, tVoid) &&
294
        !isEqual(expectedReturnType, tAny) &&
295
        !isEqual(expectedReturnType, tVoid)
296
      ) {
297
        // Type error where function does not return anything when it should
298
        context.errors.push(new FunctionShouldHaveReturnValueError(node));
1✔
299
      } else {
300
        checkForTypeMismatch(node, actualReturnType, expectedReturnType);
68✔
301
      }
302

303
      // Save function type in type env
304
      setType(fnName, fnType, env);
69✔
305
      return tUndef;
69✔
306
    case 'VariableDeclaration': {
307
      if (node.kind === 'var') {
458!
NEW
308
        throw new TypecheckError(node, 'Variable declaration using "var" is not allowed');
×
309
      }
310
      if (node.declarations.length !== 1) {
458!
311
        throw new TypecheckError(
×
312
          node,
313
          'Variable declaration should have one and only one declaration',
314
        );
315
      }
316
      if (node.declarations[0].id.type !== 'Identifier') {
458!
NEW
317
        throw new TypecheckError(node, 'Variable declaration ID should be an identifier');
×
318
      }
319
      const id = node.declarations[0].id;
458✔
320
      if (!node.declarations[0].init) {
458!
NEW
321
        throw new TypecheckError(node, 'Variable declaration must have value');
×
322
      }
323
      const init = node.declarations[0].init;
458✔
324
      // Look up declared type if current environment contains name
325
      const expectedType = env[env.length - 1].typeMap.has(id.name)
458✔
326
        ? (lookupTypeAndRemoveForAllAndPredicateTypes(id.name) ??
452!
327
          getTypeAnnotationType(id.typeAnnotation))
328
        : getTypeAnnotationType(id.typeAnnotation);
329
      const initType = typeCheckAndReturnType(init);
458✔
330
      checkForTypeMismatch(node, initType, expectedType);
458✔
331

332
      // Save variable type and decl kind in type env
333
      setType(id.name, expectedType, env);
458✔
334
      setDeclKind(id.name, node.kind, env);
458✔
335
      return tUndef;
458✔
336
    }
337
    case 'ClassDeclaration': {
338
      return tAny;
75✔
339
    }
340
    case 'CallExpression': {
341
      const callee = node.callee;
169✔
342
      const args = node.arguments;
169✔
343
      if (context.chapter >= 2 && callee.type === 'Identifier') {
169✔
344
        // Special functions for Source 2+: list, head, tail, stream
345
        // The typical way of getting the return type of call expressions is insufficient to type lists,
346
        // as we need to save the pair representation of the list as well (lists are pairs).
347
        // head and tail should preserve the pair representation of lists whenever possible.
348
        // Hence, these 3 functions are handled separately.
349
        // Streams are treated similarly to lists, except only for Source 3+ and we do not need to store the pair representation.
350
        const fnName = callee.name;
109✔
351
        if (fnName === 'list') {
109✔
352
          if (args.length === 0) {
24✔
353
            return tNull;
2✔
354
          }
355
          // Element type is union of all types of arguments in list
356
          let elementType = typeCheckAndReturnType(args[0]);
22✔
357
          for (let i = 1; i < args.length; i++) {
22✔
358
            elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i]));
19✔
359
          }
360
          // Type the list as a pair, for use when checking for type mismatches against pairs
361
          let pairType = tPair(typeCheckAndReturnType(args[args.length - 1]), tNull);
22✔
362
          for (let i = args.length - 2; i >= 0; i--) {
22✔
363
            pairType = tPair(typeCheckAndReturnType(args[i]), pairType);
19✔
364
          }
365
          return tList(elementType, pairType);
22✔
366
        }
367
        if (fnName === 'head' || fnName === 'tail') {
85✔
368
          if (args.length !== 1) {
32✔
369
            context.errors.push(new InvalidNumberOfArgumentsTypeError(node, 1, args.length));
2✔
370
            return tAny;
2✔
371
          }
372
          const actualType = typeCheckAndReturnType(args[0]);
30✔
373
          // Argument should be either a pair or a list
374
          const expectedType = tUnion(tPair(tAny, tAny), tList(tAny));
30✔
375
          const numErrors = context.errors.length;
30✔
376
          checkForTypeMismatch(node, actualType, expectedType);
30✔
377
          if (context.errors.length > numErrors) {
30!
378
            // If errors were found, return "any" type
NEW
379
            return tAny;
×
380
          }
381
          return fnName === 'head' ? getHeadType(node, actualType) : getTailType(node, actualType);
30✔
382
        }
383
        if (fnName === 'stream' && context.chapter >= 3) {
53✔
384
          if (args.length === 0) {
2!
NEW
385
            return tNull;
×
386
          }
387
          // Element type is union of all types of arguments in stream
388
          let elementType = typeCheckAndReturnType(args[0]);
2✔
389
          for (let i = 1; i < args.length; i++) {
2✔
390
            elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i]));
4✔
391
          }
392
          return tStream(elementType);
2✔
393
        }
394

395
        // Due to the use of generics, pair, list and stream functions are handled separately
396
        const pairFunctions = [
51✔
397
          'pair',
398
          'is_pair',
399
          'head',
400
          'tail',
401
          'is_null',
402
          'set_head',
403
          'set_tail',
404
        ];
405
        const listFunctions = [
51✔
406
          'list',
407
          'equal',
408
          'length',
409
          'map',
410
          'build_list',
411
          'for_each',
412
          'list_to_string',
413
          'append',
414
          'member',
415
          'remove',
416
          'remove_all',
417
          'filter',
418
          'enum_list',
419
          'list_ref',
420
          'accumulate',
421
          'reverse',
422
        ];
423
        const streamFunctions = [
51✔
424
          'stream_tail',
425
          'is_stream',
426
          'list_to_stream',
427
          'stream_to_list',
428
          'stream_length',
429
          'stream_map',
430
          'build_stream',
431
          'stream_for_each',
432
          'stream_reverse',
433
          'stream_append',
434
          'stream_member',
435
          'stream_remove',
436
          'stream_remove_all',
437
          'stream_filter',
438
          'enum_stream',
439
          'integers_from',
440
          'eval_stream',
441
          'stream_ref',
442
        ];
443
        if (
51✔
444
          pairFunctions.includes(fnName) ||
104✔
445
          listFunctions.includes(fnName) ||
446
          streamFunctions.includes(fnName)
447
        ) {
448
          const calleeType = cloneDeep(typeCheckAndReturnType(callee));
28✔
449
          if (calleeType.kind !== 'function') {
28!
450
            if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
×
NEW
451
              context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)));
×
452
            }
NEW
453
            return tAny;
×
454
          }
455

456
          const expectedTypes = calleeType.parameterTypes;
28✔
457
          let returnType = calleeType.returnType;
28✔
458

459
          // Check argument types before returning declared return type
460
          if (args.length !== expectedTypes.length) {
28✔
461
            context.errors.push(
2✔
462
              new InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length),
463
            );
464
            return returnType;
2✔
465
          }
466

467
          for (let i = 0; i < expectedTypes.length; i++) {
26✔
468
            const node = args[i];
51✔
469
            const actualType = typeCheckAndReturnType(node);
51✔
470
            // Get all valid type variable mappings for current argument
471
            const mappings = getTypeVariableMappings(node, actualType, expectedTypes[i]);
51✔
472
            // Apply type variable mappings to subsequent argument types and return type
473
            for (const mapping of mappings) {
51✔
474
              const typeVar = tVar(mapping[0]);
53✔
475
              const typeToSub = mapping[1];
53✔
476
              for (let j = i; j < expectedTypes.length; j++) {
53✔
477
                expectedTypes[j] = substituteVariableTypes(expectedTypes[j], typeVar, typeToSub);
84✔
478
              }
479
              returnType = substituteVariableTypes(returnType, typeVar, typeToSub);
53✔
480
            }
481
            // Typecheck current argument
482
            checkForTypeMismatch(node, actualType, expectedTypes[i]);
51✔
483
          }
484
          return returnType;
26✔
485
        }
486
      }
487
      const calleeType = cloneDeep(typeCheckAndReturnType(callee));
83✔
488
      if (calleeType.kind !== 'function') {
83✔
489
        if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
11✔
490
          context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)));
1✔
491
        }
492
        return tAny;
11✔
493
      }
494

495
      const expectedTypes = calleeType.parameterTypes;
72✔
496
      let returnType = calleeType.returnType;
72✔
497

498
      // If any of the arguments is a spread element, skip type checking of arguments
499
      // TODO: Add support for type checking of call expressions with spread elements
500
      const hasVarArgs = args.reduce((prev, curr) => prev || curr.type === 'SpreadElement', false);
112✔
501
      if (hasVarArgs) {
72!
NEW
502
        return returnType;
×
503
      }
504

505
      // Check argument types before returning declared return type
506
      if (args.length !== expectedTypes.length) {
72✔
507
        context.errors.push(
8✔
508
          new InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length),
509
        );
510
        return returnType;
8✔
511
      }
512

513
      for (let i = 0; i < expectedTypes.length; i++) {
64✔
514
        const node = args[i];
94✔
515
        const actualType = typeCheckAndReturnType(node);
94✔
516
        // Typecheck current argument
517
        checkForTypeMismatch(node, actualType, expectedTypes[i]);
94✔
518
      }
519
      return returnType;
64✔
520
    }
521
    case 'AssignmentExpression':
522
      const expectedType = typeCheckAndReturnType(node.left);
25✔
523
      const actualType = typeCheckAndReturnType(node.right);
25✔
524

525
      if (node.left.type === 'Identifier' && lookupDeclKind(node.left.name, env) === 'const') {
25✔
526
        context.errors.push(new ConstNotAssignableTypeError(node, node.left.name));
1✔
527
      }
528
      checkForTypeMismatch(node, actualType, expectedType);
25✔
529
      return actualType;
25✔
530
    case 'ArrayExpression':
531
      // Casting is safe here as Source disallows use of spread elements and holes in arrays
532
      const elements = node.elements.filter(
30✔
533
        (elem): elem is Exclude<tsEs.ArrayExpression['elements'][0], tsEs.SpreadElement | null> =>
534
          elem !== null && elem.type !== 'SpreadElement',
54✔
535
      );
536
      if (elements.length !== node.elements.length) {
30!
NEW
537
        throw new TypecheckError(node, 'Disallowed array element type');
×
538
      }
539
      if (elements.length === 0) {
30✔
540
        return tArray(tAny);
2✔
541
      }
542
      const elementTypes = elements.map(elem => typeCheckAndReturnType(elem));
54✔
543
      return tArray(mergeTypes(node, ...elementTypes));
28✔
544
    case 'MemberExpression':
545
      const indexType = typeCheckAndReturnType(node.property);
14✔
546
      const objectType = typeCheckAndReturnType(node.object);
14✔
547
      // Typecheck index against number
548
      if (hasTypeMismatchErrors(node, indexType, tNumber)) {
14✔
549
        context.errors.push(new InvalidIndexTypeError(node, formatTypeString(indexType, true)));
5✔
550
      }
551
      // Expression being accessed must be array
552
      if (objectType.kind !== 'array') {
14✔
553
        context.errors.push(new InvalidArrayAccessTypeError(node, formatTypeString(objectType)));
5✔
554
        return tAny;
5✔
555
      }
556
      return objectType.elementType;
9✔
557
    case 'ReturnStatement': {
558
      if (!node.argument) {
79!
559
        // Skip typecheck as unspecified literals will be handled by the noImplicitReturnUndefined rule,
560
        // which is run after typechecking
NEW
561
        return tUndef;
×
562
      } else {
563
        // Check type only if return type is specified
564
        const expectedType = lookupTypeAndRemoveForAllAndPredicateTypes(RETURN_TYPE_IDENTIFIER);
79✔
565
        if (expectedType) {
79!
566
          const argumentType = typeCheckAndReturnType(node.argument);
79✔
567
          checkForTypeMismatch(node, argumentType, expectedType);
79✔
568
          return expectedType;
79✔
569
        } else {
NEW
570
          return typeCheckAndReturnType(node.argument);
×
571
        }
572
      }
573
    }
574
    case 'WhileStatement': {
575
      // Typecheck predicate against boolean
576
      const testType = typeCheckAndReturnType(node.test);
5✔
577
      checkForTypeMismatch(node, testType, tBool);
5✔
578
      return typeCheckAndReturnType(node.body);
5✔
579
    }
580
    case 'ForStatement': {
581
      // Add new environment so that new variable declared in init node can be isolated to within for statement only
582
      pushEnv(env);
7✔
583
      if (node.init) {
7!
584
        typeCheckAndReturnType(node.init);
7✔
585
      }
586
      if (node.test) {
7!
587
        // Typecheck predicate against boolean
588
        const testType = typeCheckAndReturnType(node.test);
7✔
589
        checkForTypeMismatch(node, testType, tBool);
7✔
590
      }
591
      if (node.update) {
7!
592
        typeCheckAndReturnType(node.update);
7✔
593
      }
594
      const bodyType = typeCheckAndReturnType(node.body);
7✔
595
      env.pop();
7✔
596
      return bodyType;
7✔
597
    }
598
    case 'ImportDeclaration':
599
      // No typechecking needed, import declarations have already been handled separately
600
      return tUndef;
27✔
601
    case 'TSTypeAliasDeclaration':
602
      // No typechecking needed, type has already been added to environment
603
      return tUndef;
1,613✔
604
    case 'TSAsExpression':
605
      const originalType = typeCheckAndReturnType(node.expression);
9✔
606
      const typeToCastTo = getTypeAnnotationType(node);
9✔
607
      const formatAsLiteral =
608
        typeContainsLiteralType(originalType) || typeContainsLiteralType(typeToCastTo);
9✔
609
      // Type to cast to must have some overlap with original type
610
      if (hasTypeMismatchErrors(node, typeToCastTo, originalType)) {
9✔
611
        context.errors.push(
2✔
612
          new TypecastError(
613
            node,
614
            formatTypeString(originalType, formatAsLiteral),
615
            formatTypeString(typeToCastTo, formatAsLiteral),
616
          ),
617
        );
618
      }
619
      return typeToCastTo;
9✔
620
    case 'TSInterfaceDeclaration':
NEW
621
      throw new TypecheckError(node, 'Interface declarations are not allowed');
×
622
    case 'ExportNamedDeclaration':
NEW
623
      return typeCheckAndReturnType(node.declaration!);
×
624
    default:
NEW
625
      throw new TypecheckError(node, 'Unknown node type');
×
626
  }
627
}
628

629
/**
630
 * Adds types for imported functions to the type environment.
631
 * All imports have their types set to the "any" primitive type.
632
 */
633
function handleImportDeclarations(node: tsEs.Program) {
634
  const importStmts: tsEs.ImportDeclaration[] = node.body.filter(
218✔
635
    (stmt): stmt is tsEs.ImportDeclaration => stmt.type === 'ImportDeclaration',
2,312✔
636
  );
637
  if (importStmts.length === 0) {
218✔
638
    return;
191✔
639
  }
640

641
  const importedModuleTypesTextMap: Record<string, string> = {};
27✔
642

643
  importStmts.forEach(stmt => {
27✔
644
    // Source only uses strings for import source value
645
    const moduleName = stmt.source.value as string;
27✔
646

647
    // Precondition: loadedModulesTypes are fetched from the modules repo
648
    const moduleTypesTextMap = context.nativeStorage.loadedModuleTypes;
27✔
649

650
    // Module has no types
651
    if (!moduleTypesTextMap[moduleName]) {
27✔
652
      // Set all imported names to be of type any
653
      // TODO: Consider switching to 'Module not supported' error after more modules have been typed
654
      stmt.specifiers.map(spec => {
2✔
655
        if (spec.type !== 'ImportSpecifier') {
4!
NEW
656
          throw new TypecheckError(stmt, 'Unknown specifier type');
×
657
        }
658
        setType(spec.local.name, tAny, env);
4✔
659
      });
660
      return;
2✔
661
    }
662

663
    // Add prelude for module, which contains types that are shared by
664
    // multiple variables and functions in the module
665
    if (!importedModuleTypesTextMap[moduleName]) {
25!
666
      importedModuleTypesTextMap[moduleName] = moduleTypesTextMap[moduleName]['prelude'];
25✔
667
    } else {
668
      importedModuleTypesTextMap[moduleName] =
×
669
        importedModuleTypesTextMap[moduleName] + '\n' + moduleTypesTextMap[moduleName]['prelude'];
670
    }
671

672
    stmt.specifiers.forEach(spec => {
25✔
673
      if (spec.type !== 'ImportSpecifier') {
51!
NEW
674
        throw new TypecheckError(stmt, 'Unknown specifier type');
×
675
      }
676

677
      const importedName = spec.local.name;
51✔
678
      const importedType = moduleTypesTextMap[moduleName][importedName];
51✔
679
      if (!importedType) {
51!
680
        // Set imported name to be of type any to prevent further typecheck errors
NEW
681
        setType(importedName, tAny, env);
×
NEW
682
        return;
×
683
      }
684

685
      importedModuleTypesTextMap[moduleName] =
51✔
686
        importedModuleTypesTextMap[moduleName] + '\n' + importedType;
687
    });
688
  });
689

690
  Object.values(importedModuleTypesTextMap).forEach(typesText => {
27✔
691
    const parsedModuleTypes = babelParse(typesText, {
25✔
692
      sourceType: 'module',
693
      plugins: ['typescript', 'estree'],
694
    }).program as unknown as tsEs.Program;
695
    typeCheckAndReturnType(parsedModuleTypes);
25✔
696
  });
697
}
698

699
/**
700
 * Adds all types for variable/function/type declarations to the current environment.
701
 * This is so that the types can be referenced before the declarations are initialized.
702
 * Type checking is not carried out as this function is only responsible for hoisting declarations.
703
 */
704
function addTypeDeclarationsToEnvironment(node: tsEs.Program | tsEs.BlockStatement) {
705
  node.body.forEach(bodyNode => {
325✔
706
    switch (bodyNode.type) {
2,432✔
707
      case 'FunctionDeclaration':
708
        if (bodyNode.id === null) {
69!
709
          throw new Error(
×
710
            'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.',
711
          );
712
        }
713
        // Only identifiers/rest elements are used as function params in Source
714
        const params = bodyNode.params.filter(
69✔
715
          (param): param is tsEs.Identifier | tsEs.RestElement =>
716
            param.type === 'Identifier' || param.type === 'RestElement',
65!
717
        );
718
        if (params.length !== bodyNode.params.length) {
69!
NEW
719
          throw new TypecheckError(bodyNode, 'Unknown function parameter type');
×
720
        }
721
        const fnName = bodyNode.id.name;
69✔
722
        const returnType = getTypeAnnotationType(bodyNode.returnType);
69✔
723

724
        // If the function has variable number of arguments, set function type as any
725
        // TODO: Add support for variable number of function arguments
726
        const hasVarArgs = params.reduce(
69✔
727
          (prev, curr) => prev || curr.type === 'RestElement',
65✔
728
          false,
729
        );
730
        if (hasVarArgs) {
69!
NEW
731
          setType(fnName, tAny, env);
×
NEW
732
          break;
×
733
        }
734

735
        const types = getParamTypes(params);
69✔
736
        // Return type will always be last item in types array
737
        types.push(returnType);
69✔
738
        const fnType = tFunc(...types);
69✔
739

740
        // Save function type in type env
741
        setType(fnName, fnType, env);
69✔
742
        break;
69✔
743
      case 'VariableDeclaration':
744
        if (bodyNode.kind === 'var') {
452!
NEW
745
          throw new TypecheckError(bodyNode, 'Variable declaration using "var" is not allowed');
×
746
        }
747
        if (bodyNode.declarations.length !== 1) {
452!
748
          throw new TypecheckError(
×
749
            bodyNode,
750
            'Variable declaration should have one and only one declaration',
751
          );
752
        }
753
        if (bodyNode.declarations[0].id.type !== 'Identifier') {
452!
NEW
754
          throw new TypecheckError(bodyNode, 'Variable declaration ID should be an identifier');
×
755
        }
756
        const id = bodyNode.declarations[0].id;
452✔
757
        const expectedType = getTypeAnnotationType(id.typeAnnotation);
452✔
758

759
        // Save variable type and decl kind in type env
760
        setType(id.name, expectedType, env);
452✔
761
        setDeclKind(id.name, bodyNode.kind, env);
452✔
762
        break;
452✔
763
      case 'ClassDeclaration':
764
        const className: string = bodyNode.id.name;
75✔
765
        const classType: Variable = {
75✔
766
          kind: 'variable',
767
          name: className,
768
          constraint: 'none',
769
        };
770
        setType(className, classType, env);
75✔
771
        setTypeAlias(className, classType, env);
75✔
772
        break;
75✔
773
      case 'TSTypeAliasDeclaration':
774
        if (node.type === 'BlockStatement') {
1,614✔
775
          throw new TypecheckError(
1✔
776
            bodyNode,
777
            'Type alias declarations may only appear at the top level',
778
          );
779
        }
780
        const alias = bodyNode.id.name;
1,613✔
781
        if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(alias as TSBasicType)) {
1,613✔
782
          context.errors.push(new TypeAliasNameNotAllowedError(bodyNode, alias));
4✔
783
          break;
4✔
784
        }
785
        if (lookupTypeAlias(alias, env) !== undefined) {
1,609✔
786
          // Only happens when attempting to declare type aliases that share names with predeclared types (e.g. Pair, List)
787
          // Declaration of two type aliases with the same name will be caught as syntax error by parser
788
          context.errors.push(new DuplicateTypeAliasError(bodyNode, alias));
3✔
789
          break;
3✔
790
        }
791

792
        let type: BindableType = tAny;
1,606✔
793
        if (bodyNode.typeParameters && bodyNode.typeParameters.params.length > 0) {
1,606✔
794
          const typeParams: Variable[] = [];
4✔
795
          // Check validity of type parameters
796
          pushEnv(env);
4✔
797
          bodyNode.typeParameters.params.forEach(param => {
4✔
798
            if (param.type !== 'TSTypeParameter') {
10!
NEW
799
              throw new TypecheckError(bodyNode, 'Invalid type parameter type');
×
800
            }
801
            const name = param.name;
10✔
802
            if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(name as TSBasicType)) {
10✔
803
              context.errors.push(new TypeParameterNameNotAllowedError(param, name));
4✔
804
              return;
4✔
805
            }
806
            typeParams.push(tVar(name));
6✔
807
          });
808
          type = tForAll(getTypeAnnotationType(bodyNode), typeParams);
4✔
809
          env.pop();
4✔
810
        } else {
811
          type = getTypeAnnotationType(bodyNode);
1,602✔
812
        }
813
        setTypeAlias(alias, type, env);
1,606✔
814
        break;
1,606✔
815
      default:
816
        break;
222✔
817
    }
818
  });
819
}
820

821
/**
822
 * Typechecks the body of a binary expression, adding any type errors to context if necessary.
823
 * Then, returns the type of the binary expression, inferred based on the operator.
824
 */
825
function typeCheckAndReturnBinaryExpressionType(node: tsEs.BinaryExpression): Type {
826
  const leftType = typeCheckAndReturnType(node.left);
116✔
827
  const rightType = typeCheckAndReturnType(node.right);
116✔
828
  const leftTypeString = formatTypeString(leftType);
116✔
829
  const rightTypeString = formatTypeString(rightType);
116✔
830
  const operator = node.operator;
116✔
831
  switch (operator) {
116!
832
    case '-':
833
    case '*':
834
    case '/':
835
    case '%':
836
      // Typecheck both sides against number
837
      checkForTypeMismatch(node, leftType, tNumber);
11✔
838
      checkForTypeMismatch(node, rightType, tNumber);
11✔
839
      // Return type number
840
      return tNumber;
11✔
841
    case '+':
842
      // Typecheck both sides against number or string
843
      // However, the case where one side is string and other side is number is not allowed
844
      if (leftTypeString === 'number' || leftTypeString === 'string') {
72✔
845
        checkForTypeMismatch(node, rightType, leftType);
56✔
846
        // If left type is number or string, return left type
847
        return leftType;
56✔
848
      }
849
      if (rightTypeString === 'number' || rightTypeString === 'string') {
16✔
850
        checkForTypeMismatch(node, leftType, rightType);
8✔
851
        // If left type is not number or string but right type is number or string, return right type
852
        return rightType;
8✔
853
      }
854

855
      checkForTypeMismatch(node, leftType, tUnion(tNumber, tString));
8✔
856
      checkForTypeMismatch(node, rightType, tUnion(tNumber, tString));
8✔
857
      // Return type is number | string if both left and right are neither number nor string
858
      return tUnion(tNumber, tString);
8✔
859
    case '<':
860
    case '<=':
861
    case '>':
862
    case '>=':
863
    case '!==':
864
    case '===':
865
      // In Source 3 and above, skip type checking as equality can be applied between two items of any type
866
      if (context.chapter > 2 && (operator === '===' || operator === '!==')) {
33✔
867
        return tBool;
13✔
868
      }
869
      // Typecheck both sides against number or string
870
      // However, case where one side is string and other side is number is not allowed
871
      if (leftTypeString === 'number' || leftTypeString === 'string') {
20✔
872
        checkForTypeMismatch(node, rightType, leftType);
14✔
873
        return tBool;
14✔
874
      }
875
      if (rightTypeString === 'number' || rightTypeString === 'string') {
6✔
876
        checkForTypeMismatch(node, leftType, rightType);
3✔
877
        return tBool;
3✔
878
      }
879

880
      checkForTypeMismatch(node, leftType, tUnion(tNumber, tString));
3✔
881
      checkForTypeMismatch(node, rightType, tUnion(tNumber, tString));
3✔
882
      // Return type boolean
883
      return tBool;
3✔
884
    default:
NEW
885
      throw new TypecheckError(node, 'Unknown operator');
×
886
  }
887
}
888

889
/**
890
 * Typechecks the body of an arrow function, adding any type errors to context if necessary.
891
 * Then, returns the inferred/declared type of the function.
892
 */
893
function typeCheckAndReturnArrowFunctionType(node: tsEs.ArrowFunctionExpression): Type {
894
  // Only identifiers/rest elements are used as function params in Source
895
  const params = node.params.filter(
84✔
896
    (param): param is tsEs.Identifier | tsEs.RestElement =>
897
      param.type === 'Identifier' || param.type === 'RestElement',
106!
898
  );
899
  if (params.length !== node.params.length) {
84!
NEW
900
    throw new TypecheckError(node, 'Unknown function parameter type');
×
901
  }
902
  const expectedReturnType = getTypeAnnotationType(node.returnType);
84✔
903

904
  // If the function has variable number of arguments, set function type as any
905
  // TODO: Add support for variable number of function arguments
906
  const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
106✔
907
  if (hasVarArgs) {
84!
NEW
908
    return tAny;
×
909
  }
910

911
  // Typecheck function body, creating new environment to store arg types and return type
912
  pushEnv(env);
84✔
913
  params.forEach((param: tsEs.Identifier) => {
84✔
914
    setType(param.name, getTypeAnnotationType(param.typeAnnotation), env);
106✔
915
  });
916
  // Set unique identifier so that typechecking can be carried out for return statements
917
  setType(RETURN_TYPE_IDENTIFIER, expectedReturnType, env);
84✔
918
  const actualReturnType = typeCheckAndReturnType(node.body);
84✔
919
  env.pop();
84✔
920

921
  if (
84✔
922
    isEqual(actualReturnType, tVoid) &&
923
    !isEqual(expectedReturnType, tAny) &&
924
    !isEqual(expectedReturnType, tVoid)
925
  ) {
926
    // Type error where function does not return anything when it should
927
    context.errors.push(new FunctionShouldHaveReturnValueError(node));
1✔
928
  } else {
929
    checkForTypeMismatch(node, actualReturnType, expectedReturnType);
83✔
930
  }
931

932
  const types = getParamTypes(params);
84✔
933
  // Return type will always be last item in types array
934
  types.push(node.returnType ? expectedReturnType : actualReturnType);
84✔
935
  return tFunc(...types);
84✔
936
}
937

938
/**
939
 * Recurses through the two given types and returns an array of tuples
940
 * that map type variable names to the type to substitute.
941
 */
942
function getTypeVariableMappings(
943
  node: tsEs.Node,
944
  actualType: Type,
945
  expectedType: Type,
946
): [string, Type][] {
947
  // If type variable mapping is found, terminate early
948
  if (expectedType.kind === 'variable') {
67✔
949
    return [[expectedType.name, actualType]];
53✔
950
  }
951
  // If actual type is a type reference, expand type first
952
  if (actualType.kind === 'variable') {
14✔
953
    actualType = lookupTypeAliasAndRemoveForAllTypes(node, actualType);
5✔
954
  }
955
  const mappings: [string, Type][] = [];
14✔
956
  switch (expectedType.kind) {
14✔
957
    case 'pair':
958
      if (actualType.kind === 'list') {
2!
959
        if (actualType.typeAsPair !== undefined) {
×
960
          mappings.push(
×
961
            ...getTypeVariableMappings(node, actualType.typeAsPair.headType, expectedType.headType),
962
          );
UNCOV
963
          mappings.push(
×
964
            ...getTypeVariableMappings(node, actualType.typeAsPair.tailType, expectedType.tailType),
965
          );
966
        } else {
967
          mappings.push(
×
968
            ...getTypeVariableMappings(node, actualType.elementType, expectedType.headType),
969
          );
UNCOV
970
          mappings.push(
×
971
            ...getTypeVariableMappings(node, actualType.elementType, expectedType.tailType),
972
          );
973
        }
974
      }
975
      if (actualType.kind === 'pair') {
2!
976
        mappings.push(...getTypeVariableMappings(node, actualType.headType, expectedType.headType));
2✔
977
        mappings.push(...getTypeVariableMappings(node, actualType.tailType, expectedType.tailType));
2✔
978
      }
979
      break;
2✔
980
    case 'list':
981
      if (actualType.kind === 'list') {
3!
982
        mappings.push(
3✔
983
          ...getTypeVariableMappings(node, actualType.elementType, expectedType.elementType),
984
        );
985
      }
986
      break;
3✔
987
    case 'function':
988
      if (
5!
989
        actualType.kind === 'function' &&
10✔
990
        actualType.parameterTypes.length === expectedType.parameterTypes.length
991
      ) {
992
        for (let i = 0; i < actualType.parameterTypes.length; i++) {
5✔
993
          mappings.push(
4✔
994
            ...getTypeVariableMappings(
995
              node,
996
              actualType.parameterTypes[i],
997
              expectedType.parameterTypes[i],
998
            ),
999
          );
1000
        }
1001
        mappings.push(
5✔
1002
          ...getTypeVariableMappings(node, actualType.returnType, expectedType.returnType),
1003
        );
1004
      }
1005
      break;
5✔
1006
    default:
1007
      break;
4✔
1008
  }
1009
  return mappings;
14✔
1010
}
1011

1012
/**
1013
 * Checks if the actual type matches the expected type.
1014
 * If not, adds type mismatch error to context.
1015
 */
1016
function checkForTypeMismatch(node: tsEs.Node, actualType: Type, expectedType: Type): void {
1017
  const formatAsLiteral =
1018
    typeContainsLiteralType(expectedType) || typeContainsLiteralType(actualType);
1,072✔
1019
  if (hasTypeMismatchErrors(node, actualType, expectedType)) {
1,072✔
1020
    context.errors.push(
157✔
1021
      new TypeMismatchError(
1022
        node,
1023
        formatTypeString(actualType, formatAsLiteral),
1024
        formatTypeString(expectedType, formatAsLiteral),
1025
      ),
1026
    );
1027
  }
1028
}
1029

1030
/**
1031
 * Returns true if given type contains literal type, false otherwise.
1032
 * This is necessary to determine whether types should be formatted as
1033
 * literal type or primitive type in error messages.
1034
 */
1035
function typeContainsLiteralType(type: Type): boolean {
1036
  switch (type.kind) {
2,869✔
1037
    case 'primitive':
1038
    case 'variable':
1039
      return false;
2,380✔
1040
    case 'literal':
1041
      return true;
19✔
1042
    case 'function':
1043
      return (
132✔
1044
        typeContainsLiteralType(type.returnType) ||
264✔
1045
        type.parameterTypes.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false)
152✔
1046
      );
1047
    case 'union':
1048
      return type.types.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false);
446✔
1049
    default:
1050
      return false;
152✔
1051
  }
1052
}
1053

1054
/**
1055
 * Returns true if the actual type and the expected type do not match, false otherwise.
1056
 * The two types will not match if the intersection of the two types is empty.
1057
 *
1058
 * @param node Current node being checked
1059
 * @param actualType Type being checked
1060
 * @param expectedType Type the actual type is being checked against
1061
 * @param visitedTypeAliasesForActualType Array that keeps track of previously encountered type aliases
1062
 * for actual type to prevent infinite recursion
1063
 * @param visitedTypeAliasesForExpectedType Array that keeps track of previously encountered type aliases
1064
 * for expected type to prevent infinite recursion
1065
 * @param skipTypeAliasExpansion If true, type aliases are not expanded (e.g. in type alias declarations)
1066
 * @returns true if the actual type and the expected type do not match, false otherwise
1067
 */
1068
function hasTypeMismatchErrors(
1069
  node: tsEs.Node,
1070
  actualType: Type,
1071
  expectedType: Type,
1072
  visitedTypeAliasesForActualType: Variable[] = [],
12,674✔
1073
  visitedTypeAliasesForExpectedType: Variable[] = [],
12,674✔
1074
  skipTypeAliasExpansion: boolean = false,
12,674✔
1075
): boolean {
1076
  if (isEqual(actualType, tAny) || isEqual(expectedType, tAny)) {
12,674✔
1077
    // Exit early as "any" is guaranteed not to cause type mismatch errors
1078
    return false;
322✔
1079
  }
1080
  if (expectedType.kind !== 'variable' && actualType.kind === 'variable') {
12,352✔
1081
    // If the expected type is not a variable type but the actual type is a variable type,
1082
    // Swap the order of the types around
1083
    // This removes the need to check if the actual type is a variable type in all of the switch cases
1084
    return hasTypeMismatchErrors(
106✔
1085
      node,
1086
      expectedType,
1087
      actualType,
1088
      visitedTypeAliasesForExpectedType,
1089
      visitedTypeAliasesForActualType,
1090
      skipTypeAliasExpansion,
1091
    );
1092
  }
1093
  if (expectedType.kind !== 'union' && actualType.kind === 'union') {
12,246✔
1094
    // If the expected type is not a union type but the actual type is a union type,
1095
    // Check if the expected type matches any of the actual types
1096
    // This removes the need to check if the actual type is a union type in all of the switch cases
1097
    return !containsType(
71✔
1098
      node,
1099
      actualType.types,
1100
      expectedType,
1101
      visitedTypeAliasesForActualType,
1102
      visitedTypeAliasesForExpectedType,
1103
    );
1104
  }
1105
  switch (expectedType.kind) {
12,175!
1106
    case 'variable':
1107
      if (actualType.kind === 'variable') {
7,175✔
1108
        // If both are variable types, types match if both name and type arguments match
1109
        if (expectedType.name === actualType.name) {
6,731✔
1110
          if (expectedType.typeArgs === undefined || expectedType.typeArgs.length === 0) {
47✔
1111
            return actualType.typeArgs === undefined ? false : actualType.typeArgs.length !== 0;
17!
1112
          }
1113
          if (actualType.typeArgs?.length !== expectedType.typeArgs.length) {
30!
NEW
1114
            return true;
×
1115
          }
1116
          for (let i = 0; i < expectedType.typeArgs.length; i++) {
30✔
1117
            if (
31✔
1118
              hasTypeMismatchErrors(
1119
                node,
1120
                actualType.typeArgs[i],
1121
                expectedType.typeArgs[i],
1122
                visitedTypeAliasesForActualType,
1123
                visitedTypeAliasesForExpectedType,
1124
                skipTypeAliasExpansion,
1125
              )
1126
            ) {
1127
              return true;
22✔
1128
            }
1129
          }
1130
          return false;
8✔
1131
        }
1132
      }
1133
      for (const visitedType of visitedTypeAliasesForExpectedType) {
7,128✔
1134
        if (isEqual(visitedType, expectedType)) {
3,315✔
1135
          // Circular dependency, treat as type mismatch
1136
          return true;
64✔
1137
        }
1138
      }
1139
      // Skips expansion, treat as type mismatch
1140
      if (skipTypeAliasExpansion) {
7,064✔
1141
        return true;
6,645✔
1142
      }
1143
      visitedTypeAliasesForExpectedType.push(expectedType);
419✔
1144
      // Expand type and continue typechecking
1145
      const aliasType = lookupTypeAliasAndRemoveForAllTypes(node, expectedType);
419✔
1146
      return hasTypeMismatchErrors(
419✔
1147
        node,
1148
        actualType,
1149
        aliasType,
1150
        visitedTypeAliasesForActualType,
1151
        visitedTypeAliasesForExpectedType,
1152
        skipTypeAliasExpansion,
1153
      );
1154
    case 'primitive':
1155
      if (actualType.kind === 'literal') {
1,408✔
1156
        return expectedType.value === undefined
11!
1157
          ? typeof actualType.value !== expectedType.name
1158
          : actualType.value !== expectedType.value;
1159
      }
1160
      if (actualType.kind !== 'primitive') {
1,397✔
1161
        return true;
1✔
1162
      }
1163
      return actualType.name !== expectedType.name;
1,396✔
1164
    case 'function':
1165
      if (actualType.kind !== 'function') {
51!
NEW
1166
        return true;
×
1167
      }
1168
      // Check parameter types
1169
      const actualParamTypes = actualType.parameterTypes;
51✔
1170
      const expectedParamTypes = expectedType.parameterTypes;
51✔
1171
      if (actualParamTypes.length !== expectedParamTypes.length) {
51✔
1172
        return true;
3✔
1173
      }
1174
      for (let i = 0; i < actualType.parameterTypes.length; i++) {
48✔
1175
        // Note that actual and expected types are swapped here
1176
        // to simulate contravariance for function parameter types
1177
        // This will be useful if type checking in Source Typed were to be made stricter in the future
1178
        if (
50✔
1179
          hasTypeMismatchErrors(
1180
            node,
1181
            expectedParamTypes[i],
1182
            actualParamTypes[i],
1183
            visitedTypeAliasesForExpectedType,
1184
            visitedTypeAliasesForActualType,
1185
            skipTypeAliasExpansion,
1186
          )
1187
        ) {
1188
          return true;
3✔
1189
        }
1190
      }
1191
      // Check return type
1192
      return hasTypeMismatchErrors(
45✔
1193
        node,
1194
        actualType.returnType,
1195
        expectedType.returnType,
1196
        visitedTypeAliasesForActualType,
1197
        visitedTypeAliasesForExpectedType,
1198
        skipTypeAliasExpansion,
1199
      );
1200
    case 'union':
1201
      // If actual type is not union type, check if actual type matches one of the expected types
1202
      if (actualType.kind !== 'union') {
111✔
1203
        return !containsType(node, expectedType.types, actualType);
75✔
1204
      }
1205
      // If both are union types, there are no type errors as long as one of the types match
1206
      for (const type of actualType.types) {
36✔
1207
        if (containsType(node, expectedType.types, type)) {
36!
1208
          return false;
36✔
1209
        }
1210
      }
NEW
1211
      return true;
×
1212
    case 'literal':
1213
      if (actualType.kind !== 'literal' && actualType.kind !== 'primitive') {
3,162!
NEW
1214
        return true;
×
1215
      }
1216
      if (actualType.kind === 'primitive' && actualType.value === undefined) {
3,162✔
1217
        return actualType.name !== typeof expectedType.value;
17✔
1218
      }
1219
      return actualType.value !== expectedType.value;
3,145✔
1220
    case 'pair':
1221
      if (actualType.kind === 'list') {
196✔
1222
        // Special case, as lists are pairs
1223
        if (actualType.typeAsPair !== undefined) {
30✔
1224
          // If pair representation of list is present, check against pair type
1225
          return hasTypeMismatchErrors(
7✔
1226
            node,
1227
            actualType.typeAsPair,
1228
            expectedType,
1229
            visitedTypeAliasesForActualType,
1230
            visitedTypeAliasesForExpectedType,
1231
            skipTypeAliasExpansion,
1232
          );
1233
        }
1234
        // Head of pair should match list element type; tail of pair should match list type
1235
        return (
23✔
1236
          hasTypeMismatchErrors(
23!
1237
            node,
1238
            actualType.elementType,
1239
            expectedType.headType,
1240
            visitedTypeAliasesForActualType,
1241
            visitedTypeAliasesForExpectedType,
1242
            skipTypeAliasExpansion,
1243
          ) ||
1244
          hasTypeMismatchErrors(
1245
            node,
1246
            actualType,
1247
            expectedType.tailType,
1248
            visitedTypeAliasesForActualType,
1249
            visitedTypeAliasesForExpectedType,
1250
            skipTypeAliasExpansion,
1251
          )
1252
        );
1253
      }
1254
      if (actualType.kind !== 'pair') {
166✔
1255
        return true;
100✔
1256
      }
1257
      return (
66✔
1258
        hasTypeMismatchErrors(
106✔
1259
          node,
1260
          actualType.headType,
1261
          expectedType.headType,
1262
          visitedTypeAliasesForActualType,
1263
          visitedTypeAliasesForExpectedType,
1264
          skipTypeAliasExpansion,
1265
        ) ||
1266
        hasTypeMismatchErrors(
1267
          node,
1268
          actualType.tailType,
1269
          expectedType.tailType,
1270
          visitedTypeAliasesForActualType,
1271
          visitedTypeAliasesForExpectedType,
1272
          skipTypeAliasExpansion,
1273
        )
1274
      );
1275
    case 'list':
1276
      if (isEqual(actualType, tNull)) {
40✔
1277
        // Null matches against any list type as null is empty list
1278
        return false;
5✔
1279
      }
1280
      if (actualType.kind === 'pair') {
35✔
1281
        // Special case, as pairs can be lists
1282
        if (expectedType.typeAsPair !== undefined) {
10!
1283
          // If pair representation of list is present, check against pair type
1284
          return hasTypeMismatchErrors(
×
1285
            node,
1286
            actualType,
1287
            expectedType.typeAsPair,
1288
            visitedTypeAliasesForActualType,
1289
            visitedTypeAliasesForExpectedType,
1290
            skipTypeAliasExpansion,
1291
          );
1292
        }
1293
        // Head of pair should match list element type; tail of pair should match list type
1294
        return (
10✔
1295
          hasTypeMismatchErrors(
20✔
1296
            node,
1297
            actualType.headType,
1298
            expectedType.elementType,
1299
            visitedTypeAliasesForActualType,
1300
            visitedTypeAliasesForExpectedType,
1301
            skipTypeAliasExpansion,
1302
          ) ||
1303
          hasTypeMismatchErrors(
1304
            node,
1305
            actualType.tailType,
1306
            expectedType,
1307
            visitedTypeAliasesForActualType,
1308
            visitedTypeAliasesForExpectedType,
1309
            skipTypeAliasExpansion,
1310
          )
1311
        );
1312
      }
1313
      if (actualType.kind !== 'list') {
25✔
1314
        return true;
1✔
1315
      }
1316
      return hasTypeMismatchErrors(
24✔
1317
        node,
1318
        actualType.elementType,
1319
        expectedType.elementType,
1320
        visitedTypeAliasesForActualType,
1321
        visitedTypeAliasesForExpectedType,
1322
        skipTypeAliasExpansion,
1323
      );
1324
    case 'array':
1325
      if (actualType.kind === 'union') {
32!
1326
        // Special case: number[] | string[] matches with (number | string)[]
NEW
1327
        const types = actualType.types.filter((type): type is SArray => type.kind === 'array');
×
1328
        if (types.length !== actualType.types.length) {
×
NEW
1329
          return true;
×
1330
        }
NEW
1331
        const combinedType = types.map(type => type.elementType);
×
1332
        return hasTypeMismatchErrors(
×
1333
          node,
1334
          tUnion(...combinedType),
1335
          expectedType.elementType,
1336
          visitedTypeAliasesForActualType,
1337
          visitedTypeAliasesForExpectedType,
1338
          skipTypeAliasExpansion,
1339
        );
1340
      }
1341
      if (actualType.kind !== 'array') {
32✔
1342
        return true;
1✔
1343
      }
1344
      return hasTypeMismatchErrors(
31✔
1345
        node,
1346
        actualType.elementType,
1347
        expectedType.elementType,
1348
        visitedTypeAliasesForActualType,
1349
        visitedTypeAliasesForExpectedType,
1350
        skipTypeAliasExpansion,
1351
      );
1352
    default:
NEW
1353
      return true;
×
1354
  }
1355
}
1356

1357
/**
1358
 * Converts type annotation/type alias declaration node to its corresponding type representation in Source.
1359
 * If no type annotation exists, returns the "any" primitive type.
1360
 */
1361
function getTypeAnnotationType(
1362
  annotationNode:
1363
    | tsEs.TSTypeAnnotation
1364
    | tsEs.TSTypeAliasDeclaration
1365
    | tsEs.TSAsExpression
1366
    | undefined,
1367
): Type {
1368
  if (!annotationNode) {
2,839✔
1369
    return tAny;
195✔
1370
  }
1371
  return getAnnotatedType(annotationNode.typeAnnotation);
2,644✔
1372
}
1373

1374
/**
1375
 * Converts type node to its corresponding type representation in Source.
1376
 */
1377
function getAnnotatedType(typeNode: tsEs.TSType): Type {
1378
  switch (typeNode.type) {
12,748!
1379
    case 'TSFunctionType':
1380
      const params = typeNode.parameters;
62✔
1381
      // If the function has variable number of arguments, set function type as any
1382
      // TODO: Add support for variable number of function arguments
1383
      const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
75✔
1384
      if (hasVarArgs) {
62!
NEW
1385
        return tAny;
×
1386
      }
1387
      const fnTypes = getParamTypes(params);
62✔
1388
      // Return type will always be last item in types array
1389
      fnTypes.push(getTypeAnnotationType(typeNode.typeAnnotation));
62✔
1390
      return tFunc(...fnTypes);
62✔
1391
    case 'TSLiteralType':
1392
      const value = typeNode.literal.value;
2,082✔
1393
      if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
2,082!
NEW
1394
        throw new TypecheckError(typeNode, 'Unknown literal type');
×
1395
      }
1396
      return tLiteral(value);
2,082✔
1397
    case 'TSArrayType':
1398
      return tArray(getAnnotatedType(typeNode.elementType));
22✔
1399
    case 'TSUnionType':
1400
      const unionTypes = typeNode.types.map(node => getAnnotatedType(node));
2,655✔
1401
      return mergeTypes(typeNode, ...unionTypes);
510✔
1402
    case 'TSIntersectionType':
NEW
1403
      throw new TypecheckError(typeNode, 'Intersection types are not allowed');
×
1404
    case 'TSTypeReference':
1405
      const name = typeNode.typeName.name;
7,588✔
1406
      // Save name and type arguments in variable type
1407
      if (typeNode.typeParameters) {
7,588✔
1408
        const typesToSub: Type[] = [];
3,838✔
1409
        for (const paramNode of typeNode.typeParameters.params) {
3,838✔
1410
          if (paramNode.type === 'TSTypeParameter') {
7,425!
NEW
1411
            throw new TypecheckError(typeNode, 'Type argument should not be type parameter');
×
1412
          }
1413
          typesToSub.push(getAnnotatedType(paramNode));
7,425✔
1414
        }
1415
        return tVar(name, typesToSub);
3,838✔
1416
      }
1417
      return tVar(name);
3,750✔
1418
    case 'TSParenthesizedType':
1419
      return getAnnotatedType(typeNode.typeAnnotation);
2✔
1420
    default:
1421
      return getBasicType(typeNode);
2,482✔
1422
  }
1423
}
1424

1425
/**
1426
 * Converts an array of function parameters into an array of types.
1427
 */
1428
function getParamTypes(params: (tsEs.Identifier | tsEs.RestElement)[]): Type[] {
1429
  return params.map(param => getTypeAnnotationType(param.typeAnnotation));
311✔
1430
}
1431

1432
/**
1433
 * Returns the head type of the input type.
1434
 */
1435
function getHeadType(node: tsEs.Node, type: Type): Type {
1436
  switch (type.kind) {
136✔
1437
    case 'pair':
1438
      return type.headType;
49✔
1439
    case 'list':
1440
      return type.elementType;
5✔
1441
    case 'union':
1442
      return tUnion(...type.types.map(type => getHeadType(node, type)));
46✔
1443
    case 'variable':
1444
      return getHeadType(node, lookupTypeAliasAndRemoveForAllTypes(node, type));
75✔
1445
    default:
1446
      return type;
3✔
1447
  }
1448
}
1449

1450
/**
1451
 * Returns the tail type of the input type.
1452
 */
1453
function getTailType(node: tsEs.Node, type: Type): Type {
1454
  switch (type.kind) {
163✔
1455
    case 'pair':
1456
      return type.tailType;
51✔
1457
    case 'list':
1458
      return tList(
5✔
1459
        type.elementType,
1460
        type.typeAsPair && type.typeAsPair.tailType.kind === 'pair'
12✔
1461
          ? type.typeAsPair.tailType
1462
          : undefined,
1463
      );
1464
    case 'union':
1465
      return tUnion(...type.types.map(type => getTailType(node, type)));
46✔
1466
    case 'variable':
1467
      return getTailType(node, lookupTypeAliasAndRemoveForAllTypes(node, type));
102✔
1468
    default:
1469
      return type;
1✔
1470
  }
1471
}
1472

1473
/**
1474
 * Converts node type to basic type, adding errors to context if disallowed/unknown types are used.
1475
 * If errors are found, returns the "any" type to prevent throwing of further errors.
1476
 */
1477
function getBasicType(node: tsEs.TSKeywordType) {
1478
  const basicType = typeAnnotationKeywordToBasicTypeMap[node.type] ?? 'unknown';
2,482!
1479
  if (
2,482✔
1480
    disallowedTypes.includes(basicType as TSDisallowedTypes) ||
5,480✔
1481
    (context.chapter === 1 && basicType === 'null')
1482
  ) {
1483
    context.errors.push(new TypeNotAllowedError(node, basicType));
6✔
1484
    return tAny;
6✔
1485
  }
1486
  return tPrimitive(basicType as PrimitiveType | TSAllowedTypes);
2,476✔
1487
}
1488

1489
/**
1490
 * Wrapper function for lookupTypeAlias that removes forall and predicate types.
1491
 * Predicate types are substituted with the function type that takes in 1 argument and returns a boolean.
1492
 * For forall types, the poly type is returned.
1493
 */
1494
function lookupTypeAndRemoveForAllAndPredicateTypes(name: string): Type | undefined {
1495
  const type = lookupType(name, env);
1,077✔
1496
  if (!type) {
1,077✔
1497
    return undefined;
1✔
1498
  }
1499
  if (type.kind === 'forall') {
1,076✔
1500
    if (type.polyType.kind !== 'function') {
35✔
1501
      // Skip typecheck as function has variable number of arguments;
1502
      // this only occurs for certain prelude functions
1503
      // TODO: Add support for functions with variable number of arguments
1504
      return tAny;
7✔
1505
    }
1506
    // Clone type so that original type is not modified
1507
    return cloneDeep(type.polyType);
28✔
1508
  }
1509
  if (type.kind === 'predicate') {
1,041!
1510
    // All predicate functions (e.g. is_number)
1511
    // take in 1 parameter and return a boolean
NEW
1512
    return tFunc(tAny, tBool);
×
1513
  }
1514
  return type;
1,041✔
1515
}
1516

1517
/**
1518
 * Wrapper function for lookupTypeAlias that removes forall and predicate types.
1519
 * An error is thrown for predicate types as type aliases should not ever be predicate types.
1520
 * For forall types, the given type arguments are substituted into the poly type,
1521
 * and the resulting type is returned.
1522
 */
1523
function lookupTypeAliasAndRemoveForAllTypes(node: tsEs.Node, varType: Variable): Type {
1524
  // Check if type alias exists
1525
  const aliasType = lookupTypeAlias(varType.name, env);
601✔
1526
  if (!aliasType) {
601✔
1527
    context.errors.push(new TypeNotFoundError(node, varType.name));
1✔
1528
    return tAny;
1✔
1529
  }
1530
  // If type saved in type environment is not generic,
1531
  // given variable type should not have type arguments
1532
  if (aliasType.kind !== 'forall') {
600✔
1533
    if (varType.typeArgs !== undefined && varType.typeArgs.length > 0) {
269✔
1534
      context.errors.push(new TypeNotGenericError(node, varType.name));
1✔
1535
      return tAny;
1✔
1536
    }
1537
    return aliasType;
268✔
1538
  }
1539
  // Check type parameters
1540
  if (aliasType.typeParams === undefined) {
331!
1541
    if (varType.typeArgs !== undefined && varType.typeArgs.length > 0) {
×
NEW
1542
      context.errors.push(new TypeNotGenericError(node, varType.name));
×
1543
    }
NEW
1544
    return tAny;
×
1545
  }
1546
  if (varType.typeArgs?.length !== aliasType.typeParams.length) {
331✔
1547
    context.errors.push(
4✔
1548
      new InvalidNumberOfTypeArgumentsForGenericTypeError(
1549
        node,
1550
        varType.name,
1551
        aliasType.typeParams.length,
1552
      ),
1553
    );
1554
    return tAny;
4✔
1555
  }
1556
  // Clone type to prevent modifying generic type saved in type env
1557
  let polyType = cloneDeep(aliasType.polyType);
327✔
1558
  // Substitute all type parameters with type arguments
1559
  for (let i = 0; i < varType.typeArgs.length; i++) {
327✔
1560
    polyType = substituteVariableTypes(polyType, aliasType.typeParams[i], varType.typeArgs[i]);
597✔
1561
  }
1562
  return polyType;
327✔
1563
}
1564

1565
/**
1566
 * Recurses through the given type and returns a new type
1567
 * with all variable types that match the given type variable substituted with the type to substitute.
1568
 */
1569
function substituteVariableTypes(type: Type, typeVar: Variable, typeToSub: Type): Type {
1570
  switch (type.kind) {
2,045!
1571
    case 'primitive':
1572
    case 'literal':
1573
      return type;
291✔
1574
    case 'variable':
1575
      if (isEqual(type, typeVar)) {
1,063✔
1576
        return typeToSub;
699✔
1577
      }
1578
      if (type.typeArgs !== undefined) {
364✔
1579
        for (let i = 0; i < type.typeArgs.length; i++) {
25✔
1580
          if (isEqual(type.typeArgs[i], typeVar)) {
25✔
1581
            type.typeArgs[i] = typeToSub;
15✔
1582
          }
1583
        }
1584
      }
1585
      return type;
364✔
1586
    case 'function':
1587
      const types = type.parameterTypes.map(param =>
28✔
1588
        substituteVariableTypes(param, typeVar, typeToSub),
10✔
1589
      );
1590
      types.push(substituteVariableTypes(type.returnType, typeVar, typeToSub));
28✔
1591
      return tFunc(...types);
28✔
1592
    case 'union':
1593
      return tUnion(...type.types.map(type => substituteVariableTypes(type, typeVar, typeToSub)));
28✔
1594
    case 'pair':
1595
      return tPair(
595✔
1596
        substituteVariableTypes(type.headType, typeVar, typeToSub),
1597
        substituteVariableTypes(type.tailType, typeVar, typeToSub),
1598
      );
1599
    case 'list':
1600
      return tList(
55✔
1601
        substituteVariableTypes(type.elementType, typeVar, typeToSub),
1602
        type.typeAsPair && (substituteVariableTypes(type.typeAsPair, typeVar, typeToSub) as Pair),
55!
1603
      );
1604
    case 'array':
NEW
1605
      return tArray(substituteVariableTypes(type.elementType, typeVar, typeToSub));
×
1606
    default:
NEW
1607
      return type;
×
1608
  }
1609
}
1610

1611
/**
1612
 * Combines all types provided in the parameters into one, removing duplicate types in the process.
1613
 * Type aliases encountered are not expanded as it is sufficient to compare the variable types at name level without expanding them;
1614
 * in fact, expanding type aliases here would lead to type aliases with circular dependencies being incorrectly flagged as not declared.
1615
 */
1616
function mergeTypes(node: tsEs.Node, ...types: Type[]): Type {
1617
  const mergedTypes: Type[] = [];
598✔
1618
  for (const currType of types) {
598✔
1619
    if (isEqual(currType, tAny)) {
2,829✔
1620
      return tAny;
2✔
1621
    }
1622
    if (currType.kind === 'union') {
2,827✔
1623
      for (const type of currType.types) {
6✔
1624
        if (!containsType(node, mergedTypes, type, [], [], true)) {
14✔
1625
          mergedTypes.push(type);
9✔
1626
        }
1627
      }
1628
    } else {
1629
      if (!containsType(node, mergedTypes, currType, [], [], true)) {
2,821✔
1630
        mergedTypes.push(currType);
2,755✔
1631
      }
1632
    }
1633
  }
1634
  if (mergedTypes.length === 1) {
596✔
1635
    return mergedTypes[0];
65✔
1636
  }
1637
  return tUnion(...mergedTypes);
531✔
1638
}
1639

1640
/**
1641
 * Checks if a type exists in an array of types.
1642
 */
1643
function containsType(
1644
  node: tsEs.Node,
1645
  arr: Type[],
1646
  typeToCheck: Type,
1647
  visitedTypeAliasesForTypes: Variable[] = [],
3,017✔
1648
  visitedTypeAliasesForTypeToCheck: Variable[] = [],
3,017✔
1649
  skipTypeAliasExpansion: boolean = false,
3,017✔
1650
) {
1651
  for (const type of arr) {
3,017✔
1652
    if (
10,717✔
1653
      !hasTypeMismatchErrors(
1654
        node,
1655
        typeToCheck,
1656
        type,
1657
        visitedTypeAliasesForTypeToCheck,
1658
        visitedTypeAliasesForTypes,
1659
        skipTypeAliasExpansion,
1660
      )
1661
    ) {
1662
      return true;
201✔
1663
    }
1664
  }
1665
  return false;
2,816✔
1666
}
1667

1668
/**
1669
 * Traverses through the program and removes all TS-related nodes, returning the result.
1670
 */
1671
export function removeTSNodes(node: tsEs.Node | undefined | null): any {
1672
  if (node === undefined || node === null) {
3,065!
NEW
1673
    return node;
×
1674
  }
1675
  const type = node.type;
3,065✔
1676
  switch (type) {
3,065✔
1677
    case 'Literal':
1678
    case 'Identifier': {
1679
      return node;
1,188✔
1680
    }
1681
    case 'Program':
1682
    case 'BlockStatement': {
1683
      const newBody: tsEs.Statement[] = [];
316✔
1684
      node.body.forEach(stmt => {
316✔
1685
        const type = stmt.type;
813✔
1686
        if (type.startsWith('TS')) {
813✔
1687
          switch (type) {
24!
1688
            case 'TSAsExpression':
NEW
1689
              newBody.push(removeTSNodes(stmt));
×
NEW
1690
              break;
×
1691
            default:
1692
              // Remove node from body
1693
              break;
24✔
1694
          }
1695
        } else {
1696
          newBody.push(removeTSNodes(stmt));
789✔
1697
        }
1698
      });
1699
      node.body = newBody;
316✔
1700
      return node;
316✔
1701
    }
1702
    case 'ExpressionStatement': {
1703
      node.expression = removeTSNodes(node.expression);
102✔
1704
      return node;
102✔
1705
    }
1706
    case 'ConditionalExpression':
1707
    case 'IfStatement': {
1708
      node.test = removeTSNodes(node.test);
74✔
1709
      node.consequent = removeTSNodes(node.consequent);
74✔
1710
      node.alternate = removeTSNodes(node.alternate);
74✔
1711
      return node;
74✔
1712
    }
1713
    case 'UnaryExpression':
1714
    case 'RestElement':
1715
    case 'SpreadElement':
1716
    case 'ReturnStatement': {
1717
      node.argument = removeTSNodes(node.argument);
139✔
1718
      return node;
139✔
1719
    }
1720
    case 'BinaryExpression':
1721
    case 'LogicalExpression':
1722
    case 'AssignmentExpression': {
1723
      node.left = removeTSNodes(node.left);
192✔
1724
      node.right = removeTSNodes(node.right);
192✔
1725
      return node;
192✔
1726
    }
1727
    case 'ArrowFunctionExpression':
1728
    case 'FunctionDeclaration':
1729
      node.body = removeTSNodes(node.body);
165✔
1730
      return node;
165✔
1731
    case 'VariableDeclaration': {
1732
      node.declarations[0].init = removeTSNodes(node.declarations[0].init);
405✔
1733
      return node;
405✔
1734
    }
1735
    case 'CallExpression': {
1736
      node.arguments = node.arguments.map(removeTSNodes);
389✔
1737
      return node;
389✔
1738
    }
1739
    case 'ArrayExpression':
1740
      // Casting is safe here as Source disallows use of spread elements and holes in arrays
1741
      node.elements = node.elements.map(removeTSNodes);
30✔
1742
      return node;
30✔
1743
    case 'MemberExpression':
1744
      node.property = removeTSNodes(node.property);
14✔
1745
      node.object = removeTSNodes(node.object);
14✔
1746
      return node;
14✔
1747
    case 'WhileStatement': {
1748
      node.test = removeTSNodes(node.test);
5✔
1749
      node.body = removeTSNodes(node.body);
5✔
1750
      return node;
5✔
1751
    }
1752
    case 'ForStatement': {
1753
      node.init = removeTSNodes(node.init);
7✔
1754
      node.test = removeTSNodes(node.test);
7✔
1755
      node.update = removeTSNodes(node.update);
7✔
1756
      node.body = removeTSNodes(node.body);
7✔
1757
      return node;
7✔
1758
    }
1759
    case 'TSAsExpression':
1760
      // Remove wrapper node
1761
      return removeTSNodes(node.expression);
9✔
1762
    default:
1763
      // Remove all other TS nodes
1764
      return type.startsWith('TS') ? undefined : node;
30!
1765
  }
1766
}
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