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

source-academy / js-slang / 13489801448

24 Feb 2025 02:47AM UTC coverage: 81.529% (-0.1%) from 81.652%
13489801448

Pull #1739

github

web-flow
Merge 3bb7b6dae into 07f1f877a
Pull Request #1739: Typed modules

3663 of 4885 branches covered (74.98%)

Branch coverage included in aggregate %.

56 of 84 new or added lines in 4 files covered. (66.67%)

54 existing lines in 1 file now uncovered.

11516 of 13733 relevant lines covered (83.86%)

140051.49 hits per line

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

86.4
/src/typeChecker/typeErrorChecker.ts
1
import { parse as babelParse } from '@babel/parser'
80✔
2
import * as es from 'estree'
3
import { cloneDeep, isEqual } from 'lodash'
80✔
4

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

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

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

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

175
      if (node.type === 'Program') {
245✔
176
        // Import statements should only exist in program body
177
        handleImportDeclarations(node)
162✔
178
      }
179

180
      // Add all declarations in the current scope to the environment first
181
      addTypeDeclarationsToEnvironment(node)
245✔
182

183
      // Check all statements in program/block body
184
      for (const stmt of node.body) {
244✔
185
        if (stmt.type === 'IfStatement' || stmt.type === 'ReturnStatement') {
1,556✔
186
          returnType = typeCheckAndReturnType(stmt)
65✔
187
          if (stmt.type === 'ReturnStatement') {
65✔
188
            // If multiple return statements are present, only take the first type
189
            break
55✔
190
          }
191
        } else {
192
          typeCheckAndReturnType(stmt)
1,491✔
193
        }
194
      }
195
      if (node.type === 'BlockStatement') {
243✔
196
        // Types are saved for programs, but not for blocks
197
        env.pop()
82✔
198
      }
199

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

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

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

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

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

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

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

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

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

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

396
        // Due to the use of generics, pair, list and stream functions are handled separately
397
        const pairFunctions = ['pair']
51✔
398
        const listFunctions = ['list', 'map', 'filter', 'accumulate', 'reverse']
51✔
399
        const streamFunctions = ['stream_map', 'stream_reverse']
51✔
400
        if (
51✔
401
          pairFunctions.includes(fnName) ||
104✔
402
          listFunctions.includes(fnName) ||
403
          streamFunctions.includes(fnName)
404
        ) {
405
          const calleeType = cloneDeep(typeCheckAndReturnType(callee))
28✔
406
          if (calleeType.kind !== 'function') {
28!
NEW
407
            if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
×
NEW
408
              context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)))
×
409
            }
NEW
410
            return tAny
×
411
          }
412

413
          const expectedTypes = calleeType.parameterTypes
28✔
414
          let returnType = calleeType.returnType
28✔
415

416
          // Check argument types before returning declared return type
417
          if (args.length !== expectedTypes.length) {
28✔
418
            context.errors.push(
2✔
419
              new InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length)
420
            )
421
            return returnType
2✔
422
          }
423

424
          for (let i = 0; i < expectedTypes.length; i++) {
26✔
425
            const node = args[i]
51✔
426
            const actualType = typeCheckAndReturnType(node)
51✔
427
            // Get all valid type variable mappings for current argument
428
            const mappings = getTypeVariableMappings(node, actualType, expectedTypes[i])
51✔
429
            // Apply type variable mappings to subsequent argument types and return type
430
            for (const mapping of mappings) {
51✔
431
              const typeVar = tVar(mapping[0])
53✔
432
              const typeToSub = mapping[1]
53✔
433
              for (let j = i; j < expectedTypes.length; j++) {
53✔
434
                expectedTypes[j] = substituteVariableTypes(expectedTypes[j], typeVar, typeToSub)
84✔
435
              }
436
              returnType = substituteVariableTypes(returnType, typeVar, typeToSub)
53✔
437
            }
438
            // Typecheck current argument
439
            checkForTypeMismatch(node, actualType, expectedTypes[i])
51✔
440
          }
441
          return returnType
26✔
442
        }
443
      }
444
      const calleeType = cloneDeep(typeCheckAndReturnType(callee))
83✔
445
      if (calleeType.kind !== 'function') {
83✔
446
        if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
11✔
447
          context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)))
1✔
448
        }
449
        return tAny
11✔
450
      }
451

452
      const expectedTypes = calleeType.parameterTypes
72✔
453
      let returnType = calleeType.returnType
72✔
454

455
      // If any of the arguments is a spread element, skip type checking of arguments
456
      // TODO: Add support for type checking of call expressions with spread elements
457
      const hasVarArgs = args.reduce((prev, curr) => prev || curr.type === 'SpreadElement', false)
112✔
458
      if (hasVarArgs) {
72!
UNCOV
459
        return returnType
×
460
      }
461

462
      // Check argument types before returning declared return type
463
      if (args.length !== expectedTypes.length) {
72✔
464
        context.errors.push(
8✔
465
          new InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length)
466
        )
467
        return returnType
8✔
468
      }
469

470
      for (let i = 0; i < expectedTypes.length; i++) {
64✔
471
        const node = args[i]
94✔
472
        const actualType = typeCheckAndReturnType(node)
94✔
473
        // Typecheck current argument
474
        checkForTypeMismatch(node, actualType, expectedTypes[i])
94✔
475
      }
476
      return returnType
64✔
477
    }
478
    case 'AssignmentExpression':
479
      const expectedType = typeCheckAndReturnType(node.left)
25✔
480
      const actualType = typeCheckAndReturnType(node.right)
25✔
481

482
      if (node.left.type === 'Identifier' && lookupDeclKind(node.left.name, env) === 'const') {
25✔
483
        context.errors.push(new ConstNotAssignableTypeError(node, node.left.name))
1✔
484
      }
485
      checkForTypeMismatch(node, actualType, expectedType)
25✔
486
      return actualType
25✔
487
    case 'ArrayExpression':
488
      // Casting is safe here as Source disallows use of spread elements and holes in arrays
489
      const elements = node.elements.filter(
30✔
490
        (elem): elem is Exclude<tsEs.ArrayExpression['elements'][0], tsEs.SpreadElement | null> =>
491
          elem !== null && elem.type !== 'SpreadElement'
54✔
492
      )
493
      if (elements.length !== node.elements.length) {
30!
UNCOV
494
        throw new TypecheckError(node, 'Disallowed array element type')
×
495
      }
496
      if (elements.length === 0) {
30✔
497
        return tArray(tAny)
2✔
498
      }
499
      const elementTypes = elements.map(elem => typeCheckAndReturnType(elem))
54✔
500
      return tArray(mergeTypes(node, ...elementTypes))
28✔
501
    case 'MemberExpression':
502
      const indexType = typeCheckAndReturnType(node.property)
14✔
503
      const objectType = typeCheckAndReturnType(node.object)
14✔
504
      // Typecheck index against number
505
      if (hasTypeMismatchErrors(node, indexType, tNumber)) {
14✔
506
        context.errors.push(new InvalidIndexTypeError(node, formatTypeString(indexType, true)))
5✔
507
      }
508
      // Expression being accessed must be array
509
      if (objectType.kind !== 'array') {
14✔
510
        context.errors.push(new InvalidArrayAccessTypeError(node, formatTypeString(objectType)))
5✔
511
        return tAny
5✔
512
      }
513
      return objectType.elementType
9✔
514
    case 'ReturnStatement': {
515
      if (!node.argument) {
55!
516
        // Skip typecheck as unspecified literals will be handled by the noImplicitReturnUndefined rule,
517
        // which is run after typechecking
UNCOV
518
        return tUndef
×
519
      } else {
520
        // Check type only if return type is specified
521
        const expectedType = lookupTypeAndRemoveForAllAndPredicateTypes(RETURN_TYPE_IDENTIFIER)
55✔
522
        if (expectedType) {
55!
523
          const argumentType = typeCheckAndReturnType(node.argument)
55✔
524
          checkForTypeMismatch(node, argumentType, expectedType)
55✔
525
          return expectedType
55✔
526
        } else {
UNCOV
527
          return typeCheckAndReturnType(node.argument)
×
528
        }
529
      }
530
    }
531
    case 'WhileStatement': {
532
      // Typecheck predicate against boolean
533
      const testType = typeCheckAndReturnType(node.test)
5✔
534
      checkForTypeMismatch(node, testType, tBool)
5✔
535
      return typeCheckAndReturnType(node.body)
5✔
536
    }
537
    case 'ForStatement': {
538
      // Add new environment so that new variable declared in init node can be isolated to within for statement only
539
      pushEnv(env)
7✔
540
      if (node.init) {
7✔
541
        typeCheckAndReturnType(node.init)
7✔
542
      }
543
      if (node.test) {
7✔
544
        // Typecheck predicate against boolean
545
        const testType = typeCheckAndReturnType(node.test)
7✔
546
        checkForTypeMismatch(node, testType, tBool)
7✔
547
      }
548
      if (node.update) {
7✔
549
        typeCheckAndReturnType(node.update)
7✔
550
      }
551
      const bodyType = typeCheckAndReturnType(node.body)
7✔
552
      env.pop()
7✔
553
      return bodyType
7✔
554
    }
555
    case 'ImportDeclaration':
556
      // No typechecking needed, import declarations have already been handled separately
557
      return tUndef
25✔
558
    case 'TSTypeAliasDeclaration':
559
      // No typechecking needed, type has already been added to environment
560
      return tUndef
834✔
561
    case 'TSAsExpression':
562
      const originalType = typeCheckAndReturnType(node.expression)
9✔
563
      const typeToCastTo = getTypeAnnotationType(node)
9✔
564
      const formatAsLiteral =
565
        typeContainsLiteralType(originalType) || typeContainsLiteralType(typeToCastTo)
9✔
566
      // Type to cast to must have some overlap with original type
567
      if (hasTypeMismatchErrors(node, typeToCastTo, originalType)) {
9✔
568
        context.errors.push(
2✔
569
          new TypecastError(
570
            node,
571
            formatTypeString(originalType, formatAsLiteral),
572
            formatTypeString(typeToCastTo, formatAsLiteral)
573
          )
574
        )
575
      }
576
      return typeToCastTo
9✔
577
    case 'TSInterfaceDeclaration':
UNCOV
578
      throw new TypecheckError(node, 'Interface declarations are not allowed')
×
579
    case 'ExportNamedDeclaration':
UNCOV
580
      return typeCheckAndReturnType(node.declaration!)
×
581
    default:
UNCOV
582
      throw new TypecheckError(node, 'Unknown node type')
×
583
  }
584
}
585

586
/**
587
 * Adds types for imported functions to the type environment.
588
 * All imports have their types set to the "any" primitive type.
589
 */
590
function handleImportDeclarations(node: tsEs.Program) {
591
  const importStmts: tsEs.ImportDeclaration[] = node.body.filter(
162✔
592
    (stmt): stmt is tsEs.ImportDeclaration => stmt.type === 'ImportDeclaration'
1,470✔
593
  )
594
  if (importStmts.length === 0) {
162✔
595
    return
137✔
596
  }
597

598
  const importedModuleTypesTextMap: Record<string, string> = {}
25✔
599

600
  importStmts.forEach(stmt => {
25✔
601
    // Source only uses strings for import source value
602
    const moduleName = stmt.source.value as string
25✔
603

604
    // Precondition: loadedModulesTypes are fetched from the modules repo
605
    const moduleTypesTextMap = context.nativeStorage.loadedModulesTypes
25✔
606

607
    // Module has no types
608
    if (Object.keys(moduleTypesTextMap).length == 0) {
25✔
609
      // Set all imported names to be of type any
610
      // TODO: Consider switching to 'Module not supported' error after more modules have been typed
611
      stmt.specifiers.map(spec => {
2✔
612
        if (spec.type !== 'ImportSpecifier') {
4!
NEW
613
          throw new TypecheckError(stmt, 'Unknown specifier type')
×
614
        }
615
        setType(spec.local.name, tAny, env)
4✔
616
      })
617
      return
2✔
618
    }
619

620
    // Add prelude for module, which contains types that are shared by
621
    // multiple variables and functions in the module
622
    if (!importedModuleTypesTextMap[moduleName]) {
23!
623
      importedModuleTypesTextMap[moduleName] = moduleTypesTextMap[moduleName]['prelude']
23✔
624
    } else {
NEW
625
      importedModuleTypesTextMap[moduleName] =
×
626
        importedModuleTypesTextMap[moduleName] + '\n' + moduleTypesTextMap[moduleName]['prelude']
627
    }
628

629
    stmt.specifiers.forEach(spec => {
23✔
630
      if (spec.type !== 'ImportSpecifier') {
49!
NEW
631
        throw new TypecheckError(stmt, 'Unknown specifier type')
×
632
      }
633

634
      const importedName = spec.local.name
49✔
635
      const importedType = moduleTypesTextMap[moduleName][importedName]
49✔
636
      if (!importedType) {
49!
637
        // Set imported name to be of type any to prevent further typecheck errors
NEW
638
        setType(importedName, tAny, env)
×
NEW
639
        return
×
640
      }
641

642
      importedModuleTypesTextMap[moduleName] =
49✔
643
        importedModuleTypesTextMap[moduleName] + '\n' + importedType
644
    })
645
  })
646

647
  Object.values(importedModuleTypesTextMap).forEach(typesText => {
25✔
648
    const parsedModuleTypes = babelParse(typesText, {
23✔
649
      sourceType: 'module',
650
      plugins: ['typescript', 'estree']
651
    }).program as unknown as tsEs.Program
652
    typeCheckAndReturnType(parsedModuleTypes)
23✔
653
  })
654
}
655

656
/**
657
 * Adds all types for variable/function/type declarations to the current environment.
658
 * This is so that the types can be referenced before the declarations are initialized.
659
 * Type checking is not carried out as this function is only responsible for hoisting declarations.
660
 */
661
function addTypeDeclarationsToEnvironment(node: tsEs.Program | tsEs.BlockStatement) {
662
  node.body.forEach(bodyNode => {
245✔
663
    switch (bodyNode.type) {
1,557✔
664
      case 'FunctionDeclaration':
665
        if (bodyNode.id === null) {
45!
UNCOV
666
          throw new Error(
×
667
            'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'
668
          )
669
        }
670
        // Only identifiers/rest elements are used as function params in Source
671
        const params = bodyNode.params.filter(
45✔
672
          (param): param is tsEs.Identifier | tsEs.RestElement =>
673
            param.type === 'Identifier' || param.type === 'RestElement'
44!
674
        )
675
        if (params.length !== bodyNode.params.length) {
45!
UNCOV
676
          throw new TypecheckError(bodyNode, 'Unknown function parameter type')
×
677
        }
678
        const fnName = bodyNode.id.name
45✔
679
        const returnType = getTypeAnnotationType(bodyNode.returnType)
45✔
680

681
        // If the function has variable number of arguments, set function type as any
682
        // TODO: Add support for variable number of function arguments
683
        const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false)
45✔
684
        if (hasVarArgs) {
45!
685
          setType(fnName, tAny, env)
×
UNCOV
686
          break
×
687
        }
688

689
        const types = getParamTypes(params)
45✔
690
        // Return type will always be last item in types array
691
        types.push(returnType)
45✔
692
        const fnType = tFunc(...types)
45✔
693

694
        // Save function type in type env
695
        setType(fnName, fnType, env)
45✔
696
        break
45✔
697
      case 'VariableDeclaration':
698
        if (bodyNode.kind === 'var') {
414!
UNCOV
699
          throw new TypecheckError(bodyNode, 'Variable declaration using "var" is not allowed')
×
700
        }
701
        if (bodyNode.declarations.length !== 1) {
414!
UNCOV
702
          throw new TypecheckError(
×
703
            bodyNode,
704
            'Variable declaration should have one and only one declaration'
705
          )
706
        }
707
        if (bodyNode.declarations[0].id.type !== 'Identifier') {
414!
UNCOV
708
          throw new TypecheckError(bodyNode, 'Variable declaration ID should be an identifier')
×
709
        }
710
        const id = bodyNode.declarations[0].id as tsEs.Identifier
414✔
711
        const expectedType = getTypeAnnotationType(id.typeAnnotation)
414✔
712

713
        // Save variable type and decl kind in type env
714
        setType(id.name, expectedType, env)
414✔
715
        setDeclKind(id.name, bodyNode.kind, env)
414✔
716
        break
414✔
717
      case 'ClassDeclaration':
718
        const className: string = bodyNode.id.name
69✔
719
        const classType: Variable = {
69✔
720
          kind: 'variable',
721
          name: className,
722
          constraint: 'none'
723
        }
724
        setType(className, classType, env)
69✔
725
        setTypeAlias(className, classType, env)
69✔
726
        break
69✔
727
      case 'TSTypeAliasDeclaration':
728
        if (node.type === 'BlockStatement') {
835✔
729
          throw new TypecheckError(
1✔
730
            bodyNode,
731
            'Type alias declarations may only appear at the top level'
732
          )
733
        }
734
        const alias = bodyNode.id.name
834✔
735
        if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(alias as TSBasicType)) {
834✔
736
          context.errors.push(new TypeAliasNameNotAllowedError(bodyNode, alias))
4✔
737
          break
4✔
738
        }
739
        if (lookupTypeAlias(alias, env) !== undefined) {
830✔
740
          // Only happens when attempting to declare type aliases that share names with predeclared types (e.g. Pair, List)
741
          // Declaration of two type aliases with the same name will be caught as syntax error by parser
742
          context.errors.push(new DuplicateTypeAliasError(bodyNode, alias))
3✔
743
          break
3✔
744
        }
745

746
        let type: BindableType = tAny
827✔
747
        if (bodyNode.typeParameters && bodyNode.typeParameters.params.length > 0) {
827✔
748
          const typeParams: Variable[] = []
4✔
749
          // Check validity of type parameters
750
          pushEnv(env)
4✔
751
          bodyNode.typeParameters.params.forEach(param => {
4✔
752
            if (param.type !== 'TSTypeParameter') {
10!
UNCOV
753
              throw new TypecheckError(bodyNode, 'Invalid type parameter type')
×
754
            }
755
            const name = param.name
10✔
756
            if (Object.values(typeAnnotationKeywordToBasicTypeMap).includes(name as TSBasicType)) {
10✔
757
              context.errors.push(new TypeParameterNameNotAllowedError(param, name))
4✔
758
              return
4✔
759
            }
760
            typeParams.push(tVar(name))
6✔
761
          })
762
          type = tForAll(getTypeAnnotationType(bodyNode), typeParams)
4✔
763
          env.pop()
4✔
764
        } else {
765
          type = getTypeAnnotationType(bodyNode)
823✔
766
        }
767
        setTypeAlias(alias, type, env)
827✔
768
        break
827✔
769
      default:
770
        break
194✔
771
    }
772
  })
773
}
774

775
/**
776
 * Typechecks the body of a binary expression, adding any type errors to context if necessary.
777
 * Then, returns the type of the binary expression, inferred based on the operator.
778
 */
779
function typeCheckAndReturnBinaryExpressionType(node: tsEs.BinaryExpression): Type {
780
  const leftType = typeCheckAndReturnType(node.left)
101✔
781
  const rightType = typeCheckAndReturnType(node.right)
101✔
782
  const leftTypeString = formatTypeString(leftType)
101✔
783
  const rightTypeString = formatTypeString(rightType)
101✔
784
  const operator = node.operator
101✔
785
  switch (operator) {
101!
786
    case '-':
787
    case '*':
788
    case '/':
789
    case '%':
790
      // Typecheck both sides against number
791
      checkForTypeMismatch(node, leftType, tNumber)
11✔
792
      checkForTypeMismatch(node, rightType, tNumber)
11✔
793
      // Return type number
794
      return tNumber
11✔
795
    case '+':
796
      // Typecheck both sides against number or string
797
      // However, the case where one side is string and other side is number is not allowed
798
      if (leftTypeString === 'number' || leftTypeString === 'string') {
57✔
799
        checkForTypeMismatch(node, rightType, leftType)
43✔
800
        // If left type is number or string, return left type
801
        return leftType
43✔
802
      }
803
      if (rightTypeString === 'number' || rightTypeString === 'string') {
14✔
804
        checkForTypeMismatch(node, leftType, rightType)
6✔
805
        // If left type is not number or string but right type is number or string, return right type
806
        return rightType
6✔
807
      }
808

809
      checkForTypeMismatch(node, leftType, tUnion(tNumber, tString))
8✔
810
      checkForTypeMismatch(node, rightType, tUnion(tNumber, tString))
8✔
811
      // Return type is number | string if both left and right are neither number nor string
812
      return tUnion(tNumber, tString)
8✔
813
    case '<':
814
    case '<=':
815
    case '>':
816
    case '>=':
817
    case '!==':
818
    case '===':
819
      // In Source 3 and above, skip type checking as equality can be applied between two items of any type
820
      if (context.chapter > 2 && (operator === '===' || operator === '!==')) {
33✔
821
        return tBool
13✔
822
      }
823
      // Typecheck both sides against number or string
824
      // However, case where one side is string and other side is number is not allowed
825
      if (leftTypeString === 'number' || leftTypeString === 'string') {
20✔
826
        checkForTypeMismatch(node, rightType, leftType)
14✔
827
        return tBool
14✔
828
      }
829
      if (rightTypeString === 'number' || rightTypeString === 'string') {
6✔
830
        checkForTypeMismatch(node, leftType, rightType)
3✔
831
        return tBool
3✔
832
      }
833

834
      checkForTypeMismatch(node, leftType, tUnion(tNumber, tString))
3✔
835
      checkForTypeMismatch(node, rightType, tUnion(tNumber, tString))
3✔
836
      // Return type boolean
837
      return tBool
3✔
838
    default:
UNCOV
839
      throw new TypecheckError(node, 'Unknown operator')
×
840
  }
841
}
842

843
/**
844
 * Typechecks the body of an arrow function, adding any type errors to context if necessary.
845
 * Then, returns the inferred/declared type of the function.
846
 */
847
function typeCheckAndReturnArrowFunctionType(node: tsEs.ArrowFunctionExpression): Type {
848
  // Only identifiers/rest elements are used as function params in Source
849
  const params = node.params.filter(
48✔
850
    (param): param is tsEs.Identifier | tsEs.RestElement =>
851
      param.type === 'Identifier' || param.type === 'RestElement'
70!
852
  )
853
  if (params.length !== node.params.length) {
48!
UNCOV
854
    throw new TypecheckError(node, 'Unknown function parameter type')
×
855
  }
856
  const expectedReturnType = getTypeAnnotationType(node.returnType)
48✔
857

858
  // If the function has variable number of arguments, set function type as any
859
  // TODO: Add support for variable number of function arguments
860
  const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false)
70✔
861
  if (hasVarArgs) {
48!
UNCOV
862
    return tAny
×
863
  }
864

865
  // Typecheck function body, creating new environment to store arg types and return type
866
  pushEnv(env)
48✔
867
  params.forEach((param: tsEs.Identifier) => {
48✔
868
    setType(param.name, getTypeAnnotationType(param.typeAnnotation), env)
70✔
869
  })
870
  // Set unique identifier so that typechecking can be carried out for return statements
871
  setType(RETURN_TYPE_IDENTIFIER, expectedReturnType, env)
48✔
872
  const actualReturnType = typeCheckAndReturnType(node.body)
48✔
873
  env.pop()
48✔
874

875
  if (
48✔
876
    isEqual(actualReturnType, tVoid) &&
52✔
877
    !isEqual(expectedReturnType, tAny) &&
878
    !isEqual(expectedReturnType, tVoid)
879
  ) {
880
    // Type error where function does not return anything when it should
881
    context.errors.push(new FunctionShouldHaveReturnValueError(node))
1✔
882
  } else {
883
    checkForTypeMismatch(node, actualReturnType, expectedReturnType)
47✔
884
  }
885

886
  const types = getParamTypes(params)
48✔
887
  // Return type will always be last item in types array
888
  types.push(node.returnType ? expectedReturnType : actualReturnType)
48✔
889
  return tFunc(...types)
48✔
890
}
891

892
/**
893
 * Recurses through the two given types and returns an array of tuples
894
 * that map type variable names to the type to substitute.
895
 */
896
function getTypeVariableMappings(
897
  node: tsEs.Node,
898
  actualType: Type,
899
  expectedType: Type
900
): [string, Type][] {
901
  // If type variable mapping is found, terminate early
902
  if (expectedType.kind === 'variable') {
67✔
903
    return [[expectedType.name, actualType]]
53✔
904
  }
905
  // If actual type is a type reference, expand type first
906
  if (actualType.kind === 'variable') {
14✔
907
    actualType = lookupTypeAliasAndRemoveForAllTypes(node, actualType)
5✔
908
  }
909
  const mappings: [string, Type][] = []
14✔
910
  switch (expectedType.kind) {
14✔
911
    case 'pair':
912
      if (actualType.kind === 'list') {
2!
913
        if (actualType.typeAsPair !== undefined) {
×
UNCOV
914
          mappings.push(
×
915
            ...getTypeVariableMappings(node, actualType.typeAsPair.headType, expectedType.headType)
916
          )
UNCOV
917
          mappings.push(
×
918
            ...getTypeVariableMappings(node, actualType.typeAsPair.tailType, expectedType.tailType)
919
          )
920
        } else {
UNCOV
921
          mappings.push(
×
922
            ...getTypeVariableMappings(node, actualType.elementType, expectedType.headType)
923
          )
UNCOV
924
          mappings.push(
×
925
            ...getTypeVariableMappings(node, actualType.elementType, expectedType.tailType)
926
          )
927
        }
928
      }
929
      if (actualType.kind === 'pair') {
2✔
930
        mappings.push(...getTypeVariableMappings(node, actualType.headType, expectedType.headType))
2✔
931
        mappings.push(...getTypeVariableMappings(node, actualType.tailType, expectedType.tailType))
2✔
932
      }
933
      break
2✔
934
    case 'list':
935
      if (actualType.kind === 'list') {
3✔
936
        mappings.push(
3✔
937
          ...getTypeVariableMappings(node, actualType.elementType, expectedType.elementType)
938
        )
939
      }
940
      break
3✔
941
    case 'function':
942
      if (
5✔
943
        actualType.kind === 'function' &&
10✔
944
        actualType.parameterTypes.length === expectedType.parameterTypes.length
945
      ) {
946
        for (let i = 0; i < actualType.parameterTypes.length; i++) {
5✔
947
          mappings.push(
4✔
948
            ...getTypeVariableMappings(
949
              node,
950
              actualType.parameterTypes[i],
951
              expectedType.parameterTypes[i]
952
            )
953
          )
954
        }
955
        mappings.push(
5✔
956
          ...getTypeVariableMappings(node, actualType.returnType, expectedType.returnType)
957
        )
958
      }
959
      break
5✔
960
    default:
961
      break
4✔
962
  }
963
  return mappings
14✔
964
}
965

966
/**
967
 * Checks if the actual type matches the expected type.
968
 * If not, adds type mismatch error to context.
969
 */
970
function checkForTypeMismatch(node: tsEs.Node, actualType: Type, expectedType: Type): void {
971
  const formatAsLiteral =
972
    typeContainsLiteralType(expectedType) || typeContainsLiteralType(actualType)
935✔
973
  if (hasTypeMismatchErrors(node, actualType, expectedType)) {
935✔
974
    context.errors.push(
156✔
975
      new TypeMismatchError(
976
        node,
977
        formatTypeString(actualType, formatAsLiteral),
978
        formatTypeString(expectedType, formatAsLiteral)
979
      )
980
    )
981
  }
982
}
983

984
/**
985
 * Returns true if given type contains literal type, false otherwise.
986
 * This is necessary to determine whether types should be formatted as
987
 * literal type or primitive type in error messages.
988
 */
989
function typeContainsLiteralType(type: Type): boolean {
990
  switch (type.kind) {
2,463✔
991
    case 'primitive':
992
    case 'variable':
993
      return false
2,040✔
994
    case 'literal':
995
      return true
19✔
996
    case 'function':
997
      return (
66✔
998
        typeContainsLiteralType(type.returnType) ||
132✔
999
        type.parameterTypes.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false)
86✔
1000
      )
1001
    case 'union':
1002
      return type.types.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false)
446✔
1003
    default:
1004
      return false
152✔
1005
  }
1006
}
1007

1008
/**
1009
 * Returns true if the actual type and the expected type do not match, false otherwise.
1010
 * The two types will not match if the intersection of the two types is empty.
1011
 *
1012
 * @param node Current node being checked
1013
 * @param actualType Type being checked
1014
 * @param expectedType Type the actual type is being checked against
1015
 * @param visitedTypeAliasesForActualType Array that keeps track of previously encountered type aliases
1016
 * for actual type to prevent infinite recursion
1017
 * @param visitedTypeAliasesForExpectedType Array that keeps track of previously encountered type aliases
1018
 * for expected type to prevent infinite recursion
1019
 * @param skipTypeAliasExpansion If true, type aliases are not expanded (e.g. in type alias declarations)
1020
 * @returns true if the actual type and the expected type do not match, false otherwise
1021
 */
1022
function hasTypeMismatchErrors(
1023
  node: tsEs.Node,
1024
  actualType: Type,
1025
  expectedType: Type,
1026
  visitedTypeAliasesForActualType: Variable[] = [],
958✔
1027
  visitedTypeAliasesForExpectedType: Variable[] = [],
958✔
1028
  skipTypeAliasExpansion: boolean = false
958✔
1029
): boolean {
1030
  if (isEqual(actualType, tAny) || isEqual(expectedType, tAny)) {
7,669✔
1031
    // Exit early as "any" is guaranteed not to cause type mismatch errors
1032
    return false
207✔
1033
  }
1034
  if (expectedType.kind !== 'variable' && actualType.kind === 'variable') {
7,462✔
1035
    // If the expected type is not a variable type but the actual type is a variable type,
1036
    // Swap the order of the types around
1037
    // This removes the need to check if the actual type is a variable type in all of the switch cases
1038
    return hasTypeMismatchErrors(
106✔
1039
      node,
1040
      expectedType,
1041
      actualType,
1042
      visitedTypeAliasesForExpectedType,
1043
      visitedTypeAliasesForActualType,
1044
      skipTypeAliasExpansion
1045
    )
1046
  }
1047
  if (expectedType.kind !== 'union' && actualType.kind === 'union') {
7,356✔
1048
    // If the expected type is not a union type but the actual type is a union type,
1049
    // Check if the expected type matches any of the actual types
1050
    // This removes the need to check if the actual type is a union type in all of the switch cases
1051
    return !containsType(
71✔
1052
      node,
1053
      actualType.types,
1054
      expectedType,
1055
      visitedTypeAliasesForActualType,
1056
      visitedTypeAliasesForExpectedType
1057
    )
1058
  }
1059
  switch (expectedType.kind) {
7,285!
1060
    case 'variable':
1061
      if (actualType.kind === 'variable') {
3,967✔
1062
        // If both are variable types, types match if both name and type arguments match
1063
        if (expectedType.name === actualType.name) {
3,525✔
1064
          if (expectedType.typeArgs === undefined || expectedType.typeArgs.length === 0) {
42✔
1065
            return actualType.typeArgs === undefined ? false : actualType.typeArgs.length !== 0
12!
1066
          }
1067
          if (actualType.typeArgs?.length !== expectedType.typeArgs.length) {
30!
UNCOV
1068
            return true
×
1069
          }
1070
          for (let i = 0; i < expectedType.typeArgs.length; i++) {
30✔
1071
            if (
31✔
1072
              hasTypeMismatchErrors(
1073
                node,
1074
                actualType.typeArgs[i],
1075
                expectedType.typeArgs[i],
1076
                visitedTypeAliasesForActualType,
1077
                visitedTypeAliasesForExpectedType,
1078
                skipTypeAliasExpansion
1079
              )
1080
            ) {
1081
              return true
22✔
1082
            }
1083
          }
1084
          return false
8✔
1085
        }
1086
      }
1087
      for (const visitedType of visitedTypeAliasesForExpectedType) {
3,925✔
1088
        if (isEqual(visitedType, expectedType)) {
3,309✔
1089
          // Circular dependency, treat as type mismatch
1090
          return true
63✔
1091
        }
1092
      }
1093
      // Skips expansion, treat as type mismatch
1094
      if (skipTypeAliasExpansion) {
3,862✔
1095
        return true
3,447✔
1096
      }
1097
      visitedTypeAliasesForExpectedType.push(expectedType)
415✔
1098
      // Expand type and continue typechecking
1099
      const aliasType = lookupTypeAliasAndRemoveForAllTypes(node, expectedType)
415✔
1100
      return hasTypeMismatchErrors(
415✔
1101
        node,
1102
        actualType,
1103
        aliasType,
1104
        visitedTypeAliasesForActualType,
1105
        visitedTypeAliasesForExpectedType,
1106
        skipTypeAliasExpansion
1107
      )
1108
    case 'primitive':
1109
      if (actualType.kind === 'literal') {
1,222✔
1110
        return expectedType.value === undefined
11!
1111
          ? typeof actualType.value !== expectedType.name
1112
          : actualType.value !== expectedType.value
1113
      }
1114
      if (actualType.kind !== 'primitive') {
1,211✔
1115
        return true
1✔
1116
      }
1117
      return actualType.name !== expectedType.name
1,210✔
1118
    case 'function':
1119
      if (actualType.kind !== 'function') {
37!
UNCOV
1120
        return true
×
1121
      }
1122
      // Check parameter types
1123
      const actualParamTypes = actualType.parameterTypes
37✔
1124
      const expectedParamTypes = expectedType.parameterTypes
37✔
1125
      if (actualParamTypes.length !== expectedParamTypes.length) {
37✔
1126
        return true
3✔
1127
      }
1128
      for (let i = 0; i < actualType.parameterTypes.length; i++) {
34✔
1129
        // Note that actual and expected types are swapped here
1130
        // to simulate contravariance for function parameter types
1131
        // This will be useful if type checking in Source Typed were to be made stricter in the future
1132
        if (
36✔
1133
          hasTypeMismatchErrors(
1134
            node,
1135
            expectedParamTypes[i],
1136
            actualParamTypes[i],
1137
            visitedTypeAliasesForExpectedType,
1138
            visitedTypeAliasesForActualType,
1139
            skipTypeAliasExpansion
1140
          )
1141
        ) {
1142
          return true
3✔
1143
        }
1144
      }
1145
      // Check return type
1146
      return hasTypeMismatchErrors(
31✔
1147
        node,
1148
        actualType.returnType,
1149
        expectedType.returnType,
1150
        visitedTypeAliasesForActualType,
1151
        visitedTypeAliasesForExpectedType,
1152
        skipTypeAliasExpansion
1153
      )
1154
    case 'union':
1155
      // If actual type is not union type, check if actual type matches one of the expected types
1156
      if (actualType.kind !== 'union') {
111✔
1157
        return !containsType(node, expectedType.types, actualType)
75✔
1158
      }
1159
      // If both are union types, there are no type errors as long as one of the types match
1160
      for (const type of actualType.types) {
36✔
1161
        if (containsType(node, expectedType.types, type)) {
36✔
1162
          return false
36✔
1163
        }
1164
      }
UNCOV
1165
      return true
×
1166
    case 'literal':
1167
      if (actualType.kind !== 'literal' && actualType.kind !== 'primitive') {
1,680!
UNCOV
1168
        return true
×
1169
      }
1170
      if (actualType.kind === 'primitive' && actualType.value === undefined) {
1,680✔
1171
        return actualType.name !== typeof expectedType.value
17✔
1172
      }
1173
      return actualType.value !== expectedType.value
1,663✔
1174
    case 'pair':
1175
      if (actualType.kind === 'list') {
196✔
1176
        // Special case, as lists are pairs
1177
        if (actualType.typeAsPair !== undefined) {
30✔
1178
          // If pair representation of list is present, check against pair type
1179
          return hasTypeMismatchErrors(
7✔
1180
            node,
1181
            actualType.typeAsPair,
1182
            expectedType,
1183
            visitedTypeAliasesForActualType,
1184
            visitedTypeAliasesForExpectedType,
1185
            skipTypeAliasExpansion
1186
          )
1187
        }
1188
        // Head of pair should match list element type; tail of pair should match list type
1189
        return (
23✔
1190
          hasTypeMismatchErrors(
23!
1191
            node,
1192
            actualType.elementType,
1193
            expectedType.headType,
1194
            visitedTypeAliasesForActualType,
1195
            visitedTypeAliasesForExpectedType,
1196
            skipTypeAliasExpansion
1197
          ) ||
1198
          hasTypeMismatchErrors(
1199
            node,
1200
            actualType,
1201
            expectedType.tailType,
1202
            visitedTypeAliasesForActualType,
1203
            visitedTypeAliasesForExpectedType,
1204
            skipTypeAliasExpansion
1205
          )
1206
        )
1207
      }
1208
      if (actualType.kind !== 'pair') {
166✔
1209
        return true
100✔
1210
      }
1211
      return (
66✔
1212
        hasTypeMismatchErrors(
106✔
1213
          node,
1214
          actualType.headType,
1215
          expectedType.headType,
1216
          visitedTypeAliasesForActualType,
1217
          visitedTypeAliasesForExpectedType,
1218
          skipTypeAliasExpansion
1219
        ) ||
1220
        hasTypeMismatchErrors(
1221
          node,
1222
          actualType.tailType,
1223
          expectedType.tailType,
1224
          visitedTypeAliasesForActualType,
1225
          visitedTypeAliasesForExpectedType,
1226
          skipTypeAliasExpansion
1227
        )
1228
      )
1229
    case 'list':
1230
      if (isEqual(actualType, tNull)) {
40✔
1231
        // Null matches against any list type as null is empty list
1232
        return false
5✔
1233
      }
1234
      if (actualType.kind === 'pair') {
35✔
1235
        // Special case, as pairs can be lists
1236
        if (expectedType.typeAsPair !== undefined) {
10!
1237
          // If pair representation of list is present, check against pair type
UNCOV
1238
          return hasTypeMismatchErrors(
×
1239
            node,
1240
            actualType,
1241
            expectedType.typeAsPair,
1242
            visitedTypeAliasesForActualType,
1243
            visitedTypeAliasesForExpectedType,
1244
            skipTypeAliasExpansion
1245
          )
1246
        }
1247
        // Head of pair should match list element type; tail of pair should match list type
1248
        return (
10✔
1249
          hasTypeMismatchErrors(
20✔
1250
            node,
1251
            actualType.headType,
1252
            expectedType.elementType,
1253
            visitedTypeAliasesForActualType,
1254
            visitedTypeAliasesForExpectedType,
1255
            skipTypeAliasExpansion
1256
          ) ||
1257
          hasTypeMismatchErrors(
1258
            node,
1259
            actualType.tailType,
1260
            expectedType,
1261
            visitedTypeAliasesForActualType,
1262
            visitedTypeAliasesForExpectedType,
1263
            skipTypeAliasExpansion
1264
          )
1265
        )
1266
      }
1267
      if (actualType.kind !== 'list') {
25✔
1268
        return true
1✔
1269
      }
1270
      return hasTypeMismatchErrors(
24✔
1271
        node,
1272
        actualType.elementType,
1273
        expectedType.elementType,
1274
        visitedTypeAliasesForActualType,
1275
        visitedTypeAliasesForExpectedType,
1276
        skipTypeAliasExpansion
1277
      )
1278
    case 'array':
1279
      if (actualType.kind === 'union') {
32!
1280
        // Special case: number[] | string[] matches with (number | string)[]
1281
        const types = actualType.types.filter((type): type is SArray => type.kind === 'array')
×
1282
        if (types.length !== actualType.types.length) {
×
UNCOV
1283
          return true
×
1284
        }
1285
        const combinedType = types.map(type => type.elementType)
×
UNCOV
1286
        return hasTypeMismatchErrors(
×
1287
          node,
1288
          tUnion(...combinedType),
1289
          expectedType.elementType,
1290
          visitedTypeAliasesForActualType,
1291
          visitedTypeAliasesForExpectedType,
1292
          skipTypeAliasExpansion
1293
        )
1294
      }
1295
      if (actualType.kind !== 'array') {
32✔
1296
        return true
1✔
1297
      }
1298
      return hasTypeMismatchErrors(
31✔
1299
        node,
1300
        actualType.elementType,
1301
        expectedType.elementType,
1302
        visitedTypeAliasesForActualType,
1303
        visitedTypeAliasesForExpectedType,
1304
        skipTypeAliasExpansion
1305
      )
1306
    default:
UNCOV
1307
      return true
×
1308
  }
1309
}
1310

1311
/**
1312
 * Converts type annotation/type alias declaration node to its corresponding type representation in Source.
1313
 * If no type annotation exists, returns the "any" primitive type.
1314
 */
1315
function getTypeAnnotationType(
1316
  annotationNode:
1317
    | tsEs.TSTypeAnnotation
1318
    | tsEs.TSTypeAliasDeclaration
1319
    | tsEs.TSAsExpression
1320
    | undefined
1321
): Type {
1322
  if (!annotationNode) {
1,729✔
1323
    return tAny
103✔
1324
  }
1325
  return getAnnotatedType(annotationNode.typeAnnotation)
1,626✔
1326
}
1327

1328
/**
1329
 * Converts type node to its corresponding type representation in Source.
1330
 */
1331
function getAnnotatedType(typeNode: tsEs.TSType): Type {
1332
  switch (typeNode.type) {
6,998!
1333
    case 'TSFunctionType':
1334
      const params = typeNode.parameters
25✔
1335
      // If the function has variable number of arguments, set function type as any
1336
      // TODO: Add support for variable number of function arguments
1337
      const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false)
38✔
1338
      if (hasVarArgs) {
25!
UNCOV
1339
        return tAny
×
1340
      }
1341
      const fnTypes = getParamTypes(params)
25✔
1342
      // Return type will always be last item in types array
1343
      fnTypes.push(getTypeAnnotationType(typeNode.typeAnnotation))
25✔
1344
      return tFunc(...fnTypes)
25✔
1345
    case 'TSLiteralType':
1346
      const value = typeNode.literal.value
1,094✔
1347
      if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
1,094!
UNCOV
1348
        throw new TypecheckError(typeNode, 'Unknown literal type')
×
1349
      }
1350
      return tLiteral(value)
1,094✔
1351
    case 'TSArrayType':
1352
      return tArray(getAnnotatedType(typeNode.elementType))
22✔
1353
    case 'TSUnionType':
1354
      const unionTypes = typeNode.types.map(node => getAnnotatedType(node))
1,459✔
1355
      return mergeTypes(typeNode, ...unionTypes)
302✔
1356
    case 'TSIntersectionType':
UNCOV
1357
      throw new TypecheckError(typeNode, 'Intersection types are not allowed')
×
1358
    case 'TSTypeReference':
1359
      const name = typeNode.typeName.name
3,918✔
1360
      // Save name and type arguments in variable type
1361
      if (typeNode.typeParameters) {
3,918✔
1362
        const typesToSub: Type[] = []
2,018✔
1363
        for (const paramNode of typeNode.typeParameters.params) {
2,018✔
1364
          if (paramNode.type === 'TSTypeParameter') {
3,889!
UNCOV
1365
            throw new TypecheckError(typeNode, 'Type argument should not be type parameter')
×
1366
          }
1367
          typesToSub.push(getAnnotatedType(paramNode))
3,889✔
1368
        }
1369
        return tVar(name, typesToSub)
2,018✔
1370
      }
1371
      return tVar(name)
1,900✔
1372
    case 'TSParenthesizedType':
1373
      return getAnnotatedType(typeNode.typeAnnotation)
2✔
1374
    default:
1375
      return getBasicType(typeNode)
1,635✔
1376
  }
1377
}
1378

1379
/**
1380
 * Converts an array of function parameters into an array of types.
1381
 */
1382
function getParamTypes(params: (tsEs.Identifier | tsEs.RestElement)[]): Type[] {
1383
  return params.map(param => getTypeAnnotationType(param.typeAnnotation))
196✔
1384
}
1385

1386
/**
1387
 * Returns the head type of the input type.
1388
 */
1389
function getHeadType(node: tsEs.Node, type: Type): Type {
1390
  switch (type.kind) {
136✔
1391
    case 'pair':
1392
      return type.headType
49✔
1393
    case 'list':
1394
      return type.elementType
5✔
1395
    case 'union':
1396
      return tUnion(...type.types.map(type => getHeadType(node, type)))
46✔
1397
    case 'variable':
1398
      return getHeadType(node, lookupTypeAliasAndRemoveForAllTypes(node, type))
75✔
1399
    default:
1400
      return type
3✔
1401
  }
1402
}
1403

1404
/**
1405
 * Returns the tail type of the input type.
1406
 */
1407
function getTailType(node: tsEs.Node, type: Type): Type {
1408
  switch (type.kind) {
163✔
1409
    case 'pair':
1410
      return type.tailType
51✔
1411
    case 'list':
1412
      return tList(
5✔
1413
        type.elementType,
1414
        type.typeAsPair && type.typeAsPair.tailType.kind === 'pair'
12✔
1415
          ? type.typeAsPair.tailType
1416
          : undefined
1417
      )
1418
    case 'union':
1419
      return tUnion(...type.types.map(type => getTailType(node, type)))
46✔
1420
    case 'variable':
1421
      return getTailType(node, lookupTypeAliasAndRemoveForAllTypes(node, type))
102✔
1422
    default:
1423
      return type
1✔
1424
  }
1425
}
1426

1427
/**
1428
 * Converts node type to basic type, adding errors to context if disallowed/unknown types are used.
1429
 * If errors are found, returns the "any" type to prevent throwing of further errors.
1430
 */
1431
function getBasicType(node: tsEs.TSKeywordType) {
1432
  const basicType = typeAnnotationKeywordToBasicTypeMap[node.type] ?? 'unknown'
1,635!
1433
  if (
1,635✔
1434
    disallowedTypes.includes(basicType as TSDisallowedTypes) ||
3,786✔
1435
    (context.chapter === 1 && basicType === 'null')
1436
  ) {
1437
    context.errors.push(new TypeNotAllowedError(node, basicType))
6✔
1438
    return tAny
6✔
1439
  }
1440
  return tPrimitive(basicType as PrimitiveType | TSAllowedTypes)
1,629✔
1441
}
1442

1443
/**
1444
 * Wrapper function for lookupTypeAlias that removes forall and predicate types.
1445
 * Predicate types are substituted with the function type that takes in 1 argument and returns a boolean.
1446
 * For forall types, the poly type is returned.
1447
 */
1448
function lookupTypeAndRemoveForAllAndPredicateTypes(name: string): Type | undefined {
1449
  const type = lookupType(name, env)
947✔
1450
  if (!type) {
947✔
1451
    return undefined
1✔
1452
  }
1453
  if (type.kind === 'forall') {
946✔
1454
    if (type.polyType.kind !== 'function') {
35✔
1455
      // Skip typecheck as function has variable number of arguments;
1456
      // this only occurs for certain prelude functions
1457
      // TODO: Add support for functions with variable number of arguments
1458
      return tAny
7✔
1459
    }
1460
    // Clone type so that original type is not modified
1461
    return cloneDeep(type.polyType)
28✔
1462
  }
1463
  if (type.kind === 'predicate') {
911!
1464
    // All predicate functions (e.g. is_number)
1465
    // take in 1 parameter and return a boolean
UNCOV
1466
    return tFunc(tAny, tBool)
×
1467
  }
1468
  return type
911✔
1469
}
1470

1471
/**
1472
 * Wrapper function for lookupTypeAlias that removes forall and predicate types.
1473
 * An error is thrown for predicate types as type aliases should not ever be predicate types.
1474
 * For forall types, the given type arguments are substituted into the poly type,
1475
 * and the resulting type is returned.
1476
 */
1477
function lookupTypeAliasAndRemoveForAllTypes(node: tsEs.Node, varType: Variable): Type {
1478
  // Check if type alias exists
1479
  const aliasType = lookupTypeAlias(varType.name, env)
597✔
1480
  if (!aliasType) {
597✔
1481
    context.errors.push(new TypeNotFoundError(node, varType.name))
1✔
1482
    return tAny
1✔
1483
  }
1484
  // If type saved in type environment is not generic,
1485
  // given variable type should not have type arguments
1486
  if (aliasType.kind !== 'forall') {
596✔
1487
    if (varType.typeArgs !== undefined && varType.typeArgs.length > 0) {
265✔
1488
      context.errors.push(new TypeNotGenericError(node, varType.name))
1✔
1489
      return tAny
1✔
1490
    }
1491
    return aliasType
264✔
1492
  }
1493
  // Check type parameters
1494
  if (aliasType.typeParams === undefined) {
331!
1495
    if (varType.typeArgs !== undefined && varType.typeArgs.length > 0) {
×
UNCOV
1496
      context.errors.push(new TypeNotGenericError(node, varType.name))
×
1497
    }
UNCOV
1498
    return tAny
×
1499
  }
1500
  if (varType.typeArgs?.length !== aliasType.typeParams.length) {
331✔
1501
    context.errors.push(
4✔
1502
      new InvalidNumberOfTypeArgumentsForGenericTypeError(
1503
        node,
1504
        varType.name,
1505
        aliasType.typeParams.length
1506
      )
1507
    )
1508
    return tAny
4✔
1509
  }
1510
  // Clone type to prevent modifying generic type saved in type env
1511
  let polyType = cloneDeep(aliasType.polyType)
327✔
1512
  // Substitute all type parameters with type arguments
1513
  for (let i = 0; i < varType.typeArgs.length; i++) {
327✔
1514
    polyType = substituteVariableTypes(polyType, aliasType.typeParams[i], varType.typeArgs[i])
597✔
1515
  }
1516
  return polyType
327✔
1517
}
1518

1519
/**
1520
 * Recurses through the given type and returns a new type
1521
 * with all variable types that match the given type variable substituted with the type to substitute.
1522
 */
1523
function substituteVariableTypes(type: Type, typeVar: Variable, typeToSub: Type): Type {
1524
  switch (type.kind) {
2,045!
1525
    case 'primitive':
1526
    case 'literal':
1527
      return type
291✔
1528
    case 'variable':
1529
      if (isEqual(type, typeVar)) {
1,063✔
1530
        return typeToSub
699✔
1531
      }
1532
      if (type.typeArgs !== undefined) {
364✔
1533
        for (let i = 0; i < type.typeArgs.length; i++) {
25✔
1534
          if (isEqual(type.typeArgs[i], typeVar)) {
25✔
1535
            type.typeArgs[i] = typeToSub
15✔
1536
          }
1537
        }
1538
      }
1539
      return type
364✔
1540
    case 'function':
1541
      const types = type.parameterTypes.map(param =>
28✔
1542
        substituteVariableTypes(param, typeVar, typeToSub)
10✔
1543
      )
1544
      types.push(substituteVariableTypes(type.returnType, typeVar, typeToSub))
28✔
1545
      return tFunc(...types)
28✔
1546
    case 'union':
1547
      return tUnion(...type.types.map(type => substituteVariableTypes(type, typeVar, typeToSub)))
28✔
1548
    case 'pair':
1549
      return tPair(
595✔
1550
        substituteVariableTypes(type.headType, typeVar, typeToSub),
1551
        substituteVariableTypes(type.tailType, typeVar, typeToSub)
1552
      )
1553
    case 'list':
1554
      return tList(
55✔
1555
        substituteVariableTypes(type.elementType, typeVar, typeToSub),
1556
        type.typeAsPair && (substituteVariableTypes(type.typeAsPair, typeVar, typeToSub) as Pair)
55!
1557
      )
1558
    case 'array':
UNCOV
1559
      return tArray(substituteVariableTypes(type.elementType, typeVar, typeToSub))
×
1560
    default:
UNCOV
1561
      return type
×
1562
  }
1563
}
1564

1565
/**
1566
 * Combines all types provided in the parameters into one, removing duplicate types in the process.
1567
 * Type aliases encountered are not expanded as it is sufficient to compare the variable types at name level without expanding them;
1568
 * in fact, expanding type aliases here would lead to type aliases with circular dependencies being incorrectly flagged as not declared.
1569
 */
1570
function mergeTypes(node: tsEs.Node, ...types: Type[]): Type {
1571
  const mergedTypes: Type[] = []
390✔
1572
  for (const currType of types) {
390✔
1573
    if (isEqual(currType, tAny)) {
1,633✔
1574
      return tAny
2✔
1575
    }
1576
    if (currType.kind === 'union') {
1,631✔
1577
      for (const type of currType.types) {
6✔
1578
        if (!containsType(node, mergedTypes, type, [], [], true)) {
14✔
1579
          mergedTypes.push(type)
9✔
1580
        }
1581
      }
1582
    } else {
1583
      if (!containsType(node, mergedTypes, currType, [], [], true)) {
1,625✔
1584
        mergedTypes.push(currType)
1,559✔
1585
      }
1586
    }
1587
  }
1588
  if (mergedTypes.length === 1) {
388✔
1589
    return mergedTypes[0]
65✔
1590
  }
1591
  return tUnion(...mergedTypes)
323✔
1592
}
1593

1594
/**
1595
 * Checks if a type exists in an array of types.
1596
 */
1597
function containsType(
1598
  node: tsEs.Node,
1599
  arr: Type[],
1600
  typeToCheck: Type,
1601
  visitedTypeAliasesForTypes: Variable[] = [],
111✔
1602
  visitedTypeAliasesForTypeToCheck: Variable[] = [],
111✔
1603
  skipTypeAliasExpansion: boolean = false
182✔
1604
) {
1605
  for (const type of arr) {
1,821✔
1606
    if (
5,881✔
1607
      !hasTypeMismatchErrors(
1608
        node,
1609
        typeToCheck,
1610
        type,
1611
        visitedTypeAliasesForTypeToCheck,
1612
        visitedTypeAliasesForTypes,
1613
        skipTypeAliasExpansion
1614
      )
1615
    ) {
1616
      return true
201✔
1617
    }
1618
  }
1619
  return false
1,620✔
1620
}
1621

1622
/**
1623
 * Traverses through the program and removes all TS-related nodes, returning the result.
1624
 */
1625
export function removeTSNodes(node: tsEs.Node | undefined | null): any {
80✔
1626
  if (node === undefined || node === null) {
2,852!
UNCOV
1627
    return node
×
1628
  }
1629
  const type = node.type
2,852✔
1630
  switch (type) {
2,852✔
1631
    case 'Literal':
1632
    case 'Identifier': {
1633
      return node
1,134✔
1634
    }
1635
    case 'Program':
1636
    case 'BlockStatement': {
1637
      const newBody: tsEs.Statement[] = []
250✔
1638
      node.body.forEach(stmt => {
250✔
1639
        const type = stmt.type
748✔
1640
        if (type.startsWith('TS')) {
748✔
1641
          switch (type) {
24!
1642
            case 'TSAsExpression':
1643
              newBody.push(removeTSNodes(stmt))
×
UNCOV
1644
              break
×
1645
            default:
1646
              // Remove node from body
1647
              break
24✔
1648
          }
1649
        } else {
1650
          newBody.push(removeTSNodes(stmt))
724✔
1651
        }
1652
      })
1653
      node.body = newBody
250✔
1654
      return node
250✔
1655
    }
1656
    case 'ExpressionStatement': {
1657
      node.expression = removeTSNodes(node.expression)
98✔
1658
      return node
98✔
1659
    }
1660
    case 'ConditionalExpression':
1661
    case 'IfStatement': {
1662
      node.test = removeTSNodes(node.test)
74✔
1663
      node.consequent = removeTSNodes(node.consequent)
74✔
1664
      node.alternate = removeTSNodes(node.alternate)
74✔
1665
      return node
74✔
1666
    }
1667
    case 'UnaryExpression':
1668
    case 'RestElement':
1669
    case 'SpreadElement':
1670
    case 'ReturnStatement': {
1671
      node.argument = removeTSNodes(node.argument)
113✔
1672
      return node
113✔
1673
    }
1674
    case 'BinaryExpression':
1675
    case 'LogicalExpression':
1676
    case 'AssignmentExpression': {
1677
      node.left = removeTSNodes(node.left)
177✔
1678
      node.right = removeTSNodes(node.right)
177✔
1679
      return node
177✔
1680
    }
1681
    case 'ArrowFunctionExpression':
1682
    case 'FunctionDeclaration':
1683
      node.body = removeTSNodes(node.body)
130✔
1684
      return node
130✔
1685
    case 'VariableDeclaration': {
1686
      node.declarations[0].init = removeTSNodes(node.declarations[0].init)
394✔
1687
      return node
394✔
1688
    }
1689
    case 'CallExpression': {
1690
      node.arguments = node.arguments.map(removeTSNodes)
389✔
1691
      return node
389✔
1692
    }
1693
    case 'ArrayExpression':
1694
      // Casting is safe here as Source disallows use of spread elements and holes in arrays
1695
      node.elements = node.elements.map(removeTSNodes)
30✔
1696
      return node
30✔
1697
    case 'MemberExpression':
1698
      node.property = removeTSNodes(node.property)
14✔
1699
      node.object = removeTSNodes(node.object)
14✔
1700
      return node
14✔
1701
    case 'WhileStatement': {
1702
      node.test = removeTSNodes(node.test)
5✔
1703
      node.body = removeTSNodes(node.body)
5✔
1704
      return node
5✔
1705
    }
1706
    case 'ForStatement': {
1707
      node.init = removeTSNodes(node.init)
7✔
1708
      node.test = removeTSNodes(node.test)
7✔
1709
      node.update = removeTSNodes(node.update)
7✔
1710
      node.body = removeTSNodes(node.body)
7✔
1711
      return node
7✔
1712
    }
1713
    case 'TSAsExpression':
1714
      // Remove wrapper node
1715
      return removeTSNodes(node.expression)
9✔
1716
    default:
1717
      // Remove all other TS nodes
1718
      return type.startsWith('TS') ? undefined : node
28!
1719
  }
1720
}
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