UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

934 lines (933 loc) 63.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.removeTSNodes = exports.checkForTypeErrors = void 0; const parser_1 = require("@babel/parser"); const lodash_1 = require("lodash"); const typeErrors_1 = require("../errors/typeErrors"); const types_1 = require("../types"); const internalTypeErrors_1 = require("./internalTypeErrors"); const parseTreeTypes_prelude_1 = require("./parseTreeTypes.prelude"); const utils_1 = require("./utils"); // Context and type environment are saved as global variables so that they are not passed between functions excessively let context = {}; let env = []; /** * Entry function for type error checker. * Checks program for type errors, and returns the program with all TS-related nodes removed. */ function checkForTypeErrors(program, inputContext) { // Set context as global variable context = inputContext; // Deep copy type environment to avoid modifying type environment in the context, // which might affect the type inference checker env = (0, lodash_1.cloneDeep)(context.typeEnvironment); // Override predeclared function types for (const [name, type] of (0, utils_1.getTypeOverrides)(context.chapter)) { (0, utils_1.setType)(name, type, env); } if (context.chapter >= 4) { // Add parse tree types to type environment const source4Types = (0, parser_1.parse)(parseTreeTypes_prelude_1.parseTreeTypesPrelude, { sourceType: 'module', plugins: ['typescript', 'estree'] }).program; typeCheckAndReturnType(source4Types); } try { typeCheckAndReturnType(program); } catch (error) { // Catch-all for thrown errors // (either errors that cause early termination or errors that should not be reached logically) console.error(error); context.errors.push(error instanceof internalTypeErrors_1.TypecheckError ? error : new internalTypeErrors_1.TypecheckError(program, 'Uncaught error during typechecking, report this to the administrators!\n' + error.message)); } // Reset global variables context = {}; env = []; return removeTSNodes(program); } exports.checkForTypeErrors = checkForTypeErrors; /** * Recurses through the given node to check for any type errors, * then returns the node's inferred/declared type. * Any errors found are added to the context. */ function typeCheckAndReturnType(node) { switch (node.type) { case 'Literal': { // Infers type if (node.value === undefined) { return utils_1.tUndef; } if (node.value === null) { // For Source 1, skip typecheck as null literals will be handled by the noNull rule, // which is run after typechecking return context.chapter === types_1.Chapter.SOURCE_1 ? utils_1.tAny : utils_1.tNull; } if (typeof node.value !== 'string' && typeof node.value !== 'number' && typeof node.value !== 'boolean') { // Skip typecheck as unspecified literals will be handled by the noUnspecifiedLiteral rule, // which is run after typechecking return utils_1.tAny; } // Casting is safe here as above check already narrows type to string, number or boolean return (0, utils_1.tPrimitive)(typeof node.value, node.value); } case 'TemplateLiteral': { // Quasis array should only have one element as // string interpolation is not allowed in Source return (0, utils_1.tPrimitive)('string', node.quasis[0].value.raw); } case 'Identifier': { const varName = node.name; const varType = lookupTypeAndRemoveForAllAndPredicateTypes(varName); if (varType) { return varType; } else { context.errors.push(new typeErrors_1.UndefinedVariableTypeError(node, varName)); return utils_1.tAny; } } case 'RestElement': case 'SpreadElement': // TODO: Add support for rest and spread element return utils_1.tAny; case 'Program': case 'BlockStatement': { let returnType = utils_1.tVoid; (0, utils_1.pushEnv)(env); if (node.type === 'Program') { // Import statements should only exist in program body handleImportDeclarations(node); } // Add all declarations in the current scope to the environment first addTypeDeclarationsToEnvironment(node); // Check all statements in program/block body for (const stmt of node.body) { if (stmt.type === 'IfStatement' || stmt.type === 'ReturnStatement') { returnType = typeCheckAndReturnType(stmt); if (stmt.type === 'ReturnStatement') { // If multiple return statements are present, only take the first type break; } } else { typeCheckAndReturnType(stmt); } } if (node.type === 'BlockStatement') { // Types are saved for programs, but not for blocks env.pop(); } return returnType; } case 'ExpressionStatement': { // Check expression return typeCheckAndReturnType(node.expression); } case 'ConditionalExpression': case 'IfStatement': { // Typecheck predicate against boolean const predicateType = typeCheckAndReturnType(node.test); checkForTypeMismatch(node, predicateType, utils_1.tBool); // Return type is union of consequent and alternate type const consType = typeCheckAndReturnType(node.consequent); const altType = node.alternate ? typeCheckAndReturnType(node.alternate) : utils_1.tUndef; return mergeTypes(node, consType, altType); } case 'UnaryExpression': { const argType = typeCheckAndReturnType(node.argument); const operator = node.operator; switch (operator) { case '-': // Typecheck against number checkForTypeMismatch(node, argType, utils_1.tNumber); return utils_1.tNumber; case '!': // Typecheck against boolean checkForTypeMismatch(node, argType, utils_1.tBool); return utils_1.tBool; case 'typeof': // No checking needed, typeof operation can be used on any type return utils_1.tString; default: throw new internalTypeErrors_1.TypecheckError(node, 'Unknown operator'); } } case 'BinaryExpression': { return typeCheckAndReturnBinaryExpressionType(node); } case 'LogicalExpression': { // Typecheck left type against boolean const leftType = typeCheckAndReturnType(node.left); checkForTypeMismatch(node, leftType, utils_1.tBool); // Return type is union of boolean and right type const rightType = typeCheckAndReturnType(node.right); return mergeTypes(node, utils_1.tBool, rightType); } case 'ArrowFunctionExpression': { return typeCheckAndReturnArrowFunctionType(node); } case 'FunctionDeclaration': if (node.id === null) { // Block should not be reached since node.id is only null when function declaration // is part of `export default function`, which is not used in Source throw new internalTypeErrors_1.TypecheckError(node, 'Function declaration should always have an identifier'); } // Only identifiers/rest elements are used as function params in Source const params = node.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement'); if (params.length !== node.params.length) { throw new internalTypeErrors_1.TypecheckError(node, 'Unknown function parameter type'); } const fnName = node.id.name; const expectedReturnType = getTypeAnnotationType(node.returnType); // If the function has variable number of arguments, set function type as any // TODO: Add support for variable number of function arguments const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false); if (hasVarArgs) { (0, utils_1.setType)(fnName, utils_1.tAny, env); return utils_1.tUndef; } const types = getParamTypes(params); // Return type will always be last item in types array types.push(expectedReturnType); const fnType = (0, utils_1.tFunc)(...types); // Typecheck function body, creating new environment to store arg types, return type and function type (0, utils_1.pushEnv)(env); params.forEach((param) => { (0, utils_1.setType)(param.name, getTypeAnnotationType(param.typeAnnotation), env); }); // Set unique identifier so that typechecking can be carried out for return statements (0, utils_1.setType)(utils_1.RETURN_TYPE_IDENTIFIER, expectedReturnType, env); (0, utils_1.setType)(fnName, fnType, env); const actualReturnType = typeCheckAndReturnType(node.body); env.pop(); if ((0, lodash_1.isEqual)(actualReturnType, utils_1.tVoid) && !(0, lodash_1.isEqual)(expectedReturnType, utils_1.tAny) && !(0, lodash_1.isEqual)(expectedReturnType, utils_1.tVoid)) { // Type error where function does not return anything when it should context.errors.push(new typeErrors_1.FunctionShouldHaveReturnValueError(node)); } else { checkForTypeMismatch(node, actualReturnType, expectedReturnType); } // Save function type in type env (0, utils_1.setType)(fnName, fnType, env); return utils_1.tUndef; case 'VariableDeclaration': { if (node.kind === 'var') { throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration using "var" is not allowed'); } if (node.declarations.length !== 1) { throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration should have one and only one declaration'); } if (node.declarations[0].id.type !== 'Identifier') { throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration ID should be an identifier'); } const id = node.declarations[0].id; if (!node.declarations[0].init) { throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration must have value'); } const init = node.declarations[0].init; // Look up declared type if current environment contains name const expectedType = env[env.length - 1].typeMap.has(id.name) ? lookupTypeAndRemoveForAllAndPredicateTypes(id.name) ?? getTypeAnnotationType(id.typeAnnotation) : getTypeAnnotationType(id.typeAnnotation); const initType = typeCheckAndReturnType(init); checkForTypeMismatch(node, initType, expectedType); // Save variable type and decl kind in type env (0, utils_1.setType)(id.name, expectedType, env); (0, utils_1.setDeclKind)(id.name, node.kind, env); return utils_1.tUndef; } case 'CallExpression': { const callee = node.callee; const args = node.arguments; if (context.chapter >= 2 && callee.type === 'Identifier') { // Special functions for Source 2+: list, head, tail, stream // The typical way of getting the return type of call expressions is insufficient to type lists, // as we need to save the pair representation of the list as well (lists are pairs). // head and tail should preserve the pair representation of lists whenever possible. // Hence, these 3 functions are handled separately. // Streams are treated similarly to lists, except only for Source 3+ and we do not need to store the pair representation. const fnName = callee.name; if (fnName === 'list') { if (args.length === 0) { return utils_1.tNull; } // Element type is union of all types of arguments in list let elementType = typeCheckAndReturnType(args[0]); for (let i = 1; i < args.length; i++) { elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i])); } // Type the list as a pair, for use when checking for type mismatches against pairs let pairType = (0, utils_1.tPair)(typeCheckAndReturnType(args[args.length - 1]), utils_1.tNull); for (let i = args.length - 2; i >= 0; i--) { pairType = (0, utils_1.tPair)(typeCheckAndReturnType(args[i]), pairType); } return (0, utils_1.tList)(elementType, pairType); } if (fnName === 'head' || fnName === 'tail') { if (args.length !== 1) { context.errors.push(new typeErrors_1.InvalidNumberOfArgumentsTypeError(node, 1, args.length)); return utils_1.tAny; } const actualType = typeCheckAndReturnType(args[0]); // Argument should be either a pair or a list const expectedType = (0, utils_1.tUnion)((0, utils_1.tPair)(utils_1.tAny, utils_1.tAny), (0, utils_1.tList)(utils_1.tAny)); const numErrors = context.errors.length; checkForTypeMismatch(node, actualType, expectedType); if (context.errors.length > numErrors) { // If errors were found, return "any" type return utils_1.tAny; } return fnName === 'head' ? getHeadType(node, actualType) : getTailType(node, actualType); } if (fnName === 'stream' && context.chapter >= 3) { if (args.length === 0) { return utils_1.tNull; } // Element type is union of all types of arguments in stream let elementType = typeCheckAndReturnType(args[0]); for (let i = 1; i < args.length; i++) { elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i])); } return (0, utils_1.tStream)(elementType); } } const calleeType = typeCheckAndReturnType(callee); if (calleeType.kind !== 'function') { if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') { context.errors.push(new typeErrors_1.TypeNotCallableError(node, (0, utils_1.formatTypeString)(calleeType))); } return utils_1.tAny; } const expectedTypes = calleeType.parameterTypes; let returnType = calleeType.returnType; // If any of the arguments is a spread element, skip type checking of arguments // TODO: Add support for type checking of call expressions with spread elements const hasVarArgs = args.reduce((prev, curr) => prev || curr.type === 'SpreadElement', false); if (hasVarArgs) { return returnType; } // Check argument types before returning declared return type if (args.length !== expectedTypes.length) { context.errors.push(new typeErrors_1.InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length)); return returnType; } for (let i = 0; i < expectedTypes.length; i++) { const node = args[i]; const actualType = typeCheckAndReturnType(node); // Get all valid type variable mappings for current argument const mappings = getTypeVariableMappings(node, actualType, expectedTypes[i]); // Apply type variable mappings to subsequent argument types and return type for (const mapping of mappings) { const typeVar = (0, utils_1.tVar)(mapping[0]); const typeToSub = mapping[1]; for (let j = i; j < expectedTypes.length; j++) { expectedTypes[j] = substituteVariableTypes(expectedTypes[j], typeVar, typeToSub); } returnType = substituteVariableTypes(returnType, typeVar, typeToSub); } // Typecheck current argument checkForTypeMismatch(node, actualType, expectedTypes[i]); } return returnType; } case 'AssignmentExpression': const expectedType = typeCheckAndReturnType(node.left); const actualType = typeCheckAndReturnType(node.right); if (node.left.type === 'Identifier' && (0, utils_1.lookupDeclKind)(node.left.name, env) === 'const') { context.errors.push(new typeErrors_1.ConstNotAssignableTypeError(node, node.left.name)); } checkForTypeMismatch(node, actualType, expectedType); return actualType; case 'ArrayExpression': // Casting is safe here as Source disallows use of spread elements and holes in arrays const elements = node.elements.filter((elem) => elem !== null && elem.type !== 'SpreadElement'); if (elements.length !== node.elements.length) { throw new internalTypeErrors_1.TypecheckError(node, 'Disallowed array element type'); } if (elements.length === 0) { return (0, utils_1.tArray)(utils_1.tAny); } const elementTypes = elements.map(elem => typeCheckAndReturnType(elem)); return (0, utils_1.tArray)(mergeTypes(node, ...elementTypes)); case 'MemberExpression': const indexType = typeCheckAndReturnType(node.property); const objectType = typeCheckAndReturnType(node.object); // Typecheck index against number if (hasTypeMismatchErrors(node, indexType, utils_1.tNumber)) { context.errors.push(new typeErrors_1.InvalidIndexTypeError(node, (0, utils_1.formatTypeString)(indexType, true))); } // Expression being accessed must be array if (objectType.kind !== 'array') { context.errors.push(new typeErrors_1.InvalidArrayAccessTypeError(node, (0, utils_1.formatTypeString)(objectType))); return utils_1.tAny; } return objectType.elementType; case 'ReturnStatement': { if (!node.argument) { // Skip typecheck as unspecified literals will be handled by the noImplicitReturnUndefined rule, // which is run after typechecking return utils_1.tUndef; } else { // Check type only if return type is specified const expectedType = lookupTypeAndRemoveForAllAndPredicateTypes(utils_1.RETURN_TYPE_IDENTIFIER); if (expectedType) { const argumentType = typeCheckAndReturnType(node.argument); checkForTypeMismatch(node, argumentType, expectedType); return expectedType; } else { return typeCheckAndReturnType(node.argument); } } } case 'WhileStatement': { // Typecheck predicate against boolean const testType = typeCheckAndReturnType(node.test); checkForTypeMismatch(node, testType, utils_1.tBool); return typeCheckAndReturnType(node.body); } case 'ForStatement': { // Add new environment so that new variable declared in init node can be isolated to within for statement only (0, utils_1.pushEnv)(env); if (node.init) { typeCheckAndReturnType(node.init); } if (node.test) { // Typecheck predicate against boolean const testType = typeCheckAndReturnType(node.test); checkForTypeMismatch(node, testType, utils_1.tBool); } if (node.update) { typeCheckAndReturnType(node.update); } const bodyType = typeCheckAndReturnType(node.body); env.pop(); return bodyType; } case 'ImportDeclaration': // No typechecking needed, import declarations have already been handled separately return utils_1.tUndef; case 'TSTypeAliasDeclaration': // No typechecking needed, type has already been added to environment return utils_1.tUndef; case 'TSAsExpression': const originalType = typeCheckAndReturnType(node.expression); const typeToCastTo = getTypeAnnotationType(node); const formatAsLiteral = typeContainsLiteralType(originalType) || typeContainsLiteralType(typeToCastTo); // Type to cast to must have some overlap with original type if (hasTypeMismatchErrors(node, typeToCastTo, originalType)) { context.errors.push(new typeErrors_1.TypecastError(node, (0, utils_1.formatTypeString)(originalType, formatAsLiteral), (0, utils_1.formatTypeString)(typeToCastTo, formatAsLiteral))); } return typeToCastTo; case 'TSInterfaceDeclaration': throw new internalTypeErrors_1.TypecheckError(node, 'Interface declarations are not allowed'); case 'ExportNamedDeclaration': return typeCheckAndReturnType(node.declaration); default: throw new internalTypeErrors_1.TypecheckError(node, 'Unknown node type'); } } /** * Adds types for imported functions to the type environment. * All imports have their types set to the "any" primitive type. */ function handleImportDeclarations(node) { const importStmts = node.body.filter((stmt) => stmt.type === 'ImportDeclaration'); if (importStmts.length === 0) { return; } importStmts.forEach(stmt => { // Source only uses strings for import source value stmt.specifiers.map(spec => { (0, utils_1.setType)(spec.local.name, utils_1.tAny, env); }); }); } /** * Adds all types for variable/function/type declarations to the current environment. * This is so that the types can be referenced before the declarations are initialized. * Type checking is not carried out as this function is only responsible for hoisting declarations. */ function addTypeDeclarationsToEnvironment(node) { node.body.forEach(bodyNode => { switch (bodyNode.type) { case 'FunctionDeclaration': if (bodyNode.id === null) { throw new Error('Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'); } // Only identifiers/rest elements are used as function params in Source const params = bodyNode.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement'); if (params.length !== bodyNode.params.length) { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Unknown function parameter type'); } const fnName = bodyNode.id.name; const returnType = getTypeAnnotationType(bodyNode.returnType); // If the function has variable number of arguments, set function type as any // TODO: Add support for variable number of function arguments const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false); if (hasVarArgs) { (0, utils_1.setType)(fnName, utils_1.tAny, env); break; } const types = getParamTypes(params); // Return type will always be last item in types array types.push(returnType); const fnType = (0, utils_1.tFunc)(...types); // Save function type in type env (0, utils_1.setType)(fnName, fnType, env); break; case 'VariableDeclaration': if (bodyNode.kind === 'var') { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration using "var" is not allowed'); } if (bodyNode.declarations.length !== 1) { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration should have one and only one declaration'); } if (bodyNode.declarations[0].id.type !== 'Identifier') { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration ID should be an identifier'); } const id = bodyNode.declarations[0].id; const expectedType = getTypeAnnotationType(id.typeAnnotation); // Save variable type and decl kind in type env (0, utils_1.setType)(id.name, expectedType, env); (0, utils_1.setDeclKind)(id.name, bodyNode.kind, env); break; case 'TSTypeAliasDeclaration': if (node.type === 'BlockStatement') { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Type alias declarations may only appear at the top level'); } const alias = bodyNode.id.name; if (Object.values(utils_1.typeAnnotationKeywordToBasicTypeMap).includes(alias)) { context.errors.push(new typeErrors_1.TypeAliasNameNotAllowedError(bodyNode, alias)); break; } if ((0, utils_1.lookupTypeAlias)(alias, env) !== undefined) { // Only happens when attempting to declare type aliases that share names with predeclared types (e.g. Pair, List) // Declaration of two type aliases with the same name will be caught as syntax error by parser context.errors.push(new typeErrors_1.DuplicateTypeAliasError(bodyNode, alias)); break; } let type = utils_1.tAny; if (bodyNode.typeParameters && bodyNode.typeParameters.params.length > 0) { const typeParams = []; // Check validity of type parameters (0, utils_1.pushEnv)(env); bodyNode.typeParameters.params.forEach(param => { if (param.type !== 'TSTypeParameter') { throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Invalid type parameter type'); } const name = param.name; if (Object.values(utils_1.typeAnnotationKeywordToBasicTypeMap).includes(name)) { context.errors.push(new typeErrors_1.TypeParameterNameNotAllowedError(param, name)); return; } typeParams.push((0, utils_1.tVar)(name)); }); type = (0, utils_1.tForAll)(getTypeAnnotationType(bodyNode), typeParams); env.pop(); } else { type = getTypeAnnotationType(bodyNode); } (0, utils_1.setTypeAlias)(alias, type, env); break; default: break; } }); } /** * Typechecks the body of a binary expression, adding any type errors to context if necessary. * Then, returns the type of the binary expression, inferred based on the operator. */ function typeCheckAndReturnBinaryExpressionType(node) { const leftType = typeCheckAndReturnType(node.left); const rightType = typeCheckAndReturnType(node.right); const leftTypeString = (0, utils_1.formatTypeString)(leftType); const rightTypeString = (0, utils_1.formatTypeString)(rightType); const operator = node.operator; switch (operator) { case '-': case '*': case '/': case '%': // Typecheck both sides against number checkForTypeMismatch(node, leftType, utils_1.tNumber); checkForTypeMismatch(node, rightType, utils_1.tNumber); // Return type number return utils_1.tNumber; case '+': // Typecheck both sides against number or string // However, the case where one side is string and other side is number is not allowed if (leftTypeString === 'number' || leftTypeString === 'string') { checkForTypeMismatch(node, rightType, leftType); // If left type is number or string, return left type return leftType; } if (rightTypeString === 'number' || rightTypeString === 'string') { checkForTypeMismatch(node, leftType, rightType); // If left type is not number or string but right type is number or string, return right type return rightType; } checkForTypeMismatch(node, leftType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString)); checkForTypeMismatch(node, rightType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString)); // Return type is number | string if both left and right are neither number nor string return (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString); case '<': case '<=': case '>': case '>=': case '!==': case '===': // In Source 3 and above, skip type checking as equality can be applied between two items of any type if (context.chapter > 2 && (operator === '===' || operator === '!==')) { return utils_1.tBool; } // Typecheck both sides against number or string // However, case where one side is string and other side is number is not allowed if (leftTypeString === 'number' || leftTypeString === 'string') { checkForTypeMismatch(node, rightType, leftType); return utils_1.tBool; } if (rightTypeString === 'number' || rightTypeString === 'string') { checkForTypeMismatch(node, leftType, rightType); return utils_1.tBool; } checkForTypeMismatch(node, leftType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString)); checkForTypeMismatch(node, rightType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString)); // Return type boolean return utils_1.tBool; default: throw new internalTypeErrors_1.TypecheckError(node, 'Unknown operator'); } } /** * Typechecks the body of an arrow function, adding any type errors to context if necessary. * Then, returns the inferred/declared type of the function. */ function typeCheckAndReturnArrowFunctionType(node) { // Only identifiers/rest elements are used as function params in Source const params = node.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement'); if (params.length !== node.params.length) { throw new internalTypeErrors_1.TypecheckError(node, 'Unknown function parameter type'); } const expectedReturnType = getTypeAnnotationType(node.returnType); // If the function has variable number of arguments, set function type as any // TODO: Add support for variable number of function arguments const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false); if (hasVarArgs) { return utils_1.tAny; } // Typecheck function body, creating new environment to store arg types and return type (0, utils_1.pushEnv)(env); params.forEach((param) => { (0, utils_1.setType)(param.name, getTypeAnnotationType(param.typeAnnotation), env); }); // Set unique identifier so that typechecking can be carried out for return statements (0, utils_1.setType)(utils_1.RETURN_TYPE_IDENTIFIER, expectedReturnType, env); const actualReturnType = typeCheckAndReturnType(node.body); env.pop(); if ((0, lodash_1.isEqual)(actualReturnType, utils_1.tVoid) && !(0, lodash_1.isEqual)(expectedReturnType, utils_1.tAny) && !(0, lodash_1.isEqual)(expectedReturnType, utils_1.tVoid)) { // Type error where function does not return anything when it should context.errors.push(new typeErrors_1.FunctionShouldHaveReturnValueError(node)); } else { checkForTypeMismatch(node, actualReturnType, expectedReturnType); } const types = getParamTypes(params); // Return type will always be last item in types array types.push(node.returnType ? expectedReturnType : actualReturnType); return (0, utils_1.tFunc)(...types); } /** * Recurses through the two given types and returns an array of tuples * that map type variable names to the type to substitute. */ function getTypeVariableMappings(node, actualType, expectedType) { // If type variable mapping is found, terminate early if (expectedType.kind === 'variable') { return [[expectedType.name, actualType]]; } // If actual type is a type reference, expand type first if (actualType.kind === 'variable') { actualType = lookupTypeAliasAndRemoveForAllTypes(node, actualType); } const mappings = []; switch (expectedType.kind) { case 'pair': if (actualType.kind === 'list') { if (actualType.typeAsPair !== undefined) { mappings.push(...getTypeVariableMappings(node, actualType.typeAsPair.headType, expectedType.headType)); mappings.push(...getTypeVariableMappings(node, actualType.typeAsPair.tailType, expectedType.tailType)); } else { mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.headType)); mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.tailType)); } } if (actualType.kind === 'pair') { mappings.push(...getTypeVariableMappings(node, actualType.headType, expectedType.headType)); mappings.push(...getTypeVariableMappings(node, actualType.tailType, expectedType.tailType)); } break; case 'list': if (actualType.kind === 'list') { mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.elementType)); } break; case 'function': if (actualType.kind === 'function' && actualType.parameterTypes.length === expectedType.parameterTypes.length) { for (let i = 0; i < actualType.parameterTypes.length; i++) { mappings.push(...getTypeVariableMappings(node, actualType.parameterTypes[i], expectedType.parameterTypes[i])); } mappings.push(...getTypeVariableMappings(node, actualType.returnType, expectedType.returnType)); } break; default: break; } return mappings; } /** * Checks if the actual type matches the expected type. * If not, adds type mismatch error to context. */ function checkForTypeMismatch(node, actualType, expectedType) { const formatAsLiteral = typeContainsLiteralType(expectedType) || typeContainsLiteralType(actualType); if (hasTypeMismatchErrors(node, actualType, expectedType)) { context.errors.push(new typeErrors_1.TypeMismatchError(node, (0, utils_1.formatTypeString)(actualType, formatAsLiteral), (0, utils_1.formatTypeString)(expectedType, formatAsLiteral))); } } /** * Returns true if given type contains literal type, false otherwise. * This is necessary to determine whether types should be formatted as * literal type or primitive type in error messages. */ function typeContainsLiteralType(type) { switch (type.kind) { case 'primitive': case 'variable': return false; case 'literal': return true; case 'function': return (typeContainsLiteralType(type.returnType) || type.parameterTypes.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false)); case 'union': return type.types.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false); default: return false; } } /** * Returns true if the actual type and the expected type do not match, false otherwise. * The two types will not match if the intersection of the two types is empty. * * @param node Current node being checked * @param actualType Type being checked * @param expectedType Type the actual type is being checked against * @param visitedTypeAliasesForActualType Array that keeps track of previously encountered type aliases * for actual type to prevent infinite recursion * @param visitedTypeAliasesForExpectedType Array that keeps track of previously encountered type aliases * for expected type to prevent infinite recursion * @param skipTypeAliasExpansion If true, type aliases are not expanded (e.g. in type alias declarations) * @returns true if the actual type and the expected type do not match, false otherwise */ function hasTypeMismatchErrors(node, actualType, expectedType, visitedTypeAliasesForActualType = [], visitedTypeAliasesForExpectedType = [], skipTypeAliasExpansion = false) { if ((0, lodash_1.isEqual)(actualType, utils_1.tAny) || (0, lodash_1.isEqual)(expectedType, utils_1.tAny)) { // Exit early as "any" is guaranteed not to cause type mismatch errors return false; } if (expectedType.kind !== 'variable' && actualType.kind === 'variable') { // If the expected type is not a variable type but the actual type is a variable type, // Swap the order of the types around // This removes the need to check if the actual type is a variable type in all of the switch cases return hasTypeMismatchErrors(node, expectedType, actualType, visitedTypeAliasesForExpectedType, visitedTypeAliasesForActualType, skipTypeAliasExpansion); } if (expectedType.kind !== 'union' && actualType.kind === 'union') { // If the expected type is not a union type but the actual type is a union type, // Check if the expected type matches any of the actual types // This removes the need to check if the actual type is a union type in all of the switch cases return !containsType(node, actualType.types, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType); } switch (expectedType.kind) { case 'variable': if (actualType.kind === 'variable') { // If both are variable types, types match if both name and type arguments match if (expectedType.name === actualType.name) { if (expectedType.typeArgs === undefined || expectedType.typeArgs.length === 0) { return actualType.typeArgs === undefined ? false : actualType.typeArgs.length !== 0; } if (actualType.typeArgs?.length !== expectedType.typeArgs.length) { return true; } for (let i = 0; i < expectedType.typeArgs.length; i++) { if (hasTypeMismatchErrors(node, actualType.typeArgs[i], expectedType.typeArgs[i], visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion)) { return true; } } return false; } } for (const visitedType of visitedTypeAliasesForExpectedType) { if ((0, lodash_1.isEqual)(visitedType, expectedType)) { // Circular dependency, treat as type mismatch return true; } } // Skips expansion, treat as type mismatch if (skipTypeAliasExpansion) { return true; } visitedTypeAliasesForExpectedType.push(expectedType); // Expand type and continue typechecking const aliasType = lookupTypeAliasAndRemoveForAllTypes(node, expectedType); return hasTypeMismatchErrors(node, actualType, aliasType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); case 'primitive': if (actualType.kind === 'literal') { return expectedType.value === undefined ? typeof actualType.value !== expectedType.name : actualType.value !== expectedType.value; } if (actualType.kind !== 'primitive') { return true; } return actualType.name !== expectedType.name; case 'function': if (actualType.kind !== 'function') { return true; } // Check parameter types const actualParamTypes = actualType.parameterTypes; const expectedParamTypes = expectedType.parameterTypes; if (actualParamTypes.length !== expectedParamTypes.length) { return true; } for (let i = 0; i < actualType.parameterTypes.length; i++) { // Note that actual and expected types are swapped here // to simulate contravariance for function parameter types // This will be useful if type checking in Source Typed were to be made stricter in the future if (hasTypeMismatchErrors(node, expectedParamTypes[i], actualParamTypes[i], visitedTypeAliasesForExpectedType, visitedTypeAliasesForActualType, skipTypeAliasExpansion)) { return true; } } // Check return type return hasTypeMismatchErrors(node, actualType.returnType, expectedType.returnType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); case 'union': // If actual type is not union type, check if actual type matches one of the expected types if (actualType.kind !== 'union') { return !containsType(node, expectedType.types, actualType); } // If both are union types, there are no type errors as long as one of the types match for (const type of actualType.types) { if (containsType(node, expectedType.types, type)) { return false; } } return true; case 'literal': if (actualType.kind !== 'literal' && actualType.kind !== 'primitive') { return true; } if (actualType.kind === 'primitive' && actualType.value === undefined) { return actualType.name !== typeof expectedType.value; } return actualType.value !== expectedType.value; case 'pair': if (actualType.kind === 'list') { // Special case, as lists are pairs if (actualType.typeAsPair !== undefined) { // If pair representation of list is present, check against pair type return hasTypeMismatchErrors(node, actualType.typeAsPair, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); } // Head of pair should match list element type; tail of pair should match list type return (hasTypeMismatchErrors(node, actualType.elementType, expectedType.headType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) || hasTypeMismatchErrors(node, actualType, expectedType.tailType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion)); } if (actualType.kind !== 'pair') { return true; } return (hasTypeMismatchErrors(node, actualType.headType, expectedType.headType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) || hasTypeMismatchErrors(node, actualType.tailType, expectedType.tailType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion)); case 'list': if ((0, lodash_1.isEqual)(actualType, utils_1.tNull)) { // Null matches against any list type as null is empty list return false; } if (actualType.kind === 'pair') { // Special case, as pairs can be lists if (expectedType.typeAsPair !== undefined) { // If pair representation of list is present, check against pair type return hasTypeMismatchErrors(node, actualType, expectedType.typeAsPair, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); } // Head of pair should match list element type; tail of pair should match list type return (hasTypeMismatchErrors(node, actualType.headType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) || hasTypeMismatchErrors(node, actualType.tailType, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion)); } if (actualType.kind !== 'list') { return true; } return hasTypeMismatchErrors(node, actualType.elementType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); case 'array': if (actualType.kind === 'union') { // Special case: number[] | string[] matches with (number | string)[] const types = actualType.types.filter((type) => type.kind === 'array'); if (types.length !== actualType.types.length) { return true; } const combinedType = types.map(type => type.elementType); return hasTypeMismatchErrors(node, (0, utils_1.tUnion)(...combinedType), expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); } if (actualType.kind !== 'array') { return true; } return hasTypeMismatchErrors(node, actualType.elementType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion); default: return true; } } /** * Converts type annotation/type alias declaration node to its corresponding type representation in Source. * If no type annotation exists, returns the "any" primitive type. */ function getTypeAnnotationType(annotationNode) { if (!annotationNode) { return utils_1.tAny; } return getAnnotatedType(annotationNode.typeAnnotation); } /** * Converts type node to its corresponding type representation in Source. */ function getAnnotatedType(typeNode) { switch (typeNode.type) { case 'TSFunctionType': const params = typeNode.parameters; // If the function has variable number of arguments, set function type as any // TODO: Add support for variable number of function arguments const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false); if (hasVarArgs) { return utils_1.tAny; } const fnTypes = getParamTypes(params); // Return type will always be last item in types array fnTypes.push(getTypeAnnotationType(typeNode.typeAnnotation)); return (0, utils_1.tFunc)(...fnTypes); case 'TSLiteralType': const value = typeNode.literal.value; if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {