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

source-academy / js-slang / 13616034401

02 Mar 2025 01:44PM UTC coverage: 81.131% (-0.5%) from 81.652%
13616034401

Pull #1739

github

web-flow
Merge 990642701 into a50f7dcef
Pull Request #1739: Typed modules

3580 of 4791 branches covered (74.72%)

Branch coverage included in aggregate %.

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

231 existing lines in 11 files now uncovered.

11198 of 13424 relevant lines covered (83.42%)

125405.78 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'
78✔
2
import * as es from 'estree'
3
import { cloneDeep, isEqual } from 'lodash'
78✔
4

5
import {
78✔
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 {
78✔
24
  BindableType,
25
  Chapter,
26
  type Context,
27
  disallowedTypes,
28
  type Pair,
29
  PrimitiveType,
30
  SArray,
31
  TSAllowedTypes,
32
  TSBasicType,
33
  TSDisallowedTypes,
34
  type Type,
35
  TypeEnvironment,
36
  Variable
37
} from '../types'
38
import { TypecheckError } from './internalTypeErrors'
78✔
39
import { parseTreeTypesPrelude } from './parseTreeTypes.prelude'
78✔
40
import * as tsEs from './tsESTree'
41
import {
78✔
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
78✔
75
let env: TypeEnvironment = []
78✔
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 {
78✔
82
  // Set context as global variable
83
  context = inputContext
111✔
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)
111✔
87
  // Override predeclared function types
88
  for (const [name, type] of getTypeOverrides(context.chapter)) {
111✔
89
    setType(name, type, env)
2,703✔
90
  }
91
  if (context.chapter >= 4) {
111✔
92
    // Add parse tree types to type environment
93
    const source4Types = babelParse(parseTreeTypesPrelude, {
28✔
94
      sourceType: 'module',
95
      plugins: ['typescript', 'estree']
96
    }).program as unknown as tsEs.Program
97
    typeCheckAndReturnType(source4Types)
28✔
98
  }
99
  try {
111✔
100
    typeCheckAndReturnType(program)
111✔
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
111✔
117
  env = []
111✔
118
  return removeTSNodes(program)
111✔
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) {
3,243!
128
    case 'Literal': {
129
      // Infers type
130
      if (node.value === undefined) {
522!
131
        return tUndef
×
132
      }
133
      if (node.value === null) {
522✔
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 (
508!
139
        typeof node.value !== 'string' &&
952✔
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
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)
508✔
149
    }
150
    case 'TemplateLiteral': {
151
      // Quasis array should only have one element as
152
      // string interpolation is not allowed in Source
153
      return tPrimitive('string', node.quasis[0].value.raw)
×
154
    }
155
    case 'Identifier': {
156
      const varName = node.name
478✔
157
      const varType = lookupTypeAndRemoveForAllAndPredicateTypes(varName)
478✔
158
      if (varType) {
478✔
159
        return varType
477✔
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
168
      return tAny
×
169
    case 'Program':
170
    case 'BlockStatement': {
171
      let returnType: Type = tVoid
245✔
172
      pushEnv(env)
245✔
173

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

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

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

199
      return returnType
243✔
200
    }
201
    case 'ExpressionStatement': {
202
      // Check expression
203
      return typeCheckAndReturnType(node.expression)
90✔
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)
15✔
218
      const operator = node.operator
15✔
219
      switch (operator) {
15!
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
5✔
231
        default:
232
          throw new TypecheckError(node, 'Unknown operator')
×
233
      }
234
    }
235
    case 'BinaryExpression': {
236
      return typeCheckAndReturnBinaryExpressionType(node)
101✔
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)
48✔
249
    }
250
    case 'FunctionDeclaration':
251
      if (node.id === null) {
45!
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
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(
45✔
259
        (param): param is tsEs.Identifier | tsEs.RestElement =>
260
          param.type === 'Identifier' || param.type === 'RestElement'
44!
261
      )
262
      if (params.length !== node.params.length) {
45!
263
        throw new TypecheckError(node, 'Unknown function parameter type')
×
264
      }
265
      const fnName = node.id.name
45✔
266
      const expectedReturnType = getTypeAnnotationType(node.returnType)
45✔
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)
45✔
271
      if (hasVarArgs) {
45!
272
        setType(fnName, tAny, env)
×
273
        return tUndef
×
274
      }
275

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

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

292
      if (
45✔
293
        isEqual(actualReturnType, tVoid) &&
51✔
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)
44✔
301
      }
302

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

332
      // Save variable type and decl kind in type env
333
      setType(id.name, expectedType, env)
420✔
334
      setDeclKind(id.name, node.kind, env)
420✔
335
      return tUndef
420✔
336
    }
337
    case 'ClassDeclaration': {
338
      return tAny
69✔
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
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!
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 = ['pair']
51✔
397
        const listFunctions = ['list', 'map', 'filter', 'accumulate', 'reverse']
51✔
398
        const streamFunctions = ['stream_map', 'stream_reverse']
51✔
399
        if (
51✔
400
          pairFunctions.includes(fnName) ||
104✔
401
          listFunctions.includes(fnName) ||
402
          streamFunctions.includes(fnName)
403
        ) {
404
          const calleeType = cloneDeep(typeCheckAndReturnType(callee))
28✔
405
          if (calleeType.kind !== 'function') {
28!
NEW
406
            if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
×
NEW
407
              context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)))
×
408
            }
NEW
409
            return tAny
×
410
          }
411

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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