UNPKG

derw

Version:

An Elm-inspired language that transpiles to TypeScript

1,183 lines 68.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateType = exports.getValuesInTopLevelScope = exports.findReplacement = exports.validateObjectLiteralType = exports.validateAllCasesCovered = exports.getCasesFromFunction = exports.inferType = exports.isSameType = void 0; const result_1 = require("@eeue56/ts-core/build/main/lib/result"); const builtins_1 = require("./builtins"); const distance_1 = require("./errors/distance"); const Derw_1 = require("./generators/Derw"); const types_1 = require("./types"); function isSameGenericType(first, second, topLevel) { if (topLevel) return true; // todo: figure this out //return first.name === second.name; return true; } function isSameFixedType(first, second, topLevel) { if ((first.name === "any" && first.args.length === 0) || (second.name === "any" && second.args.length === 0)) { return true; } if (first.args.length !== second.args.length) { return false; } if (first.name !== second.name) return false; for (var i = 0; i < first.args.length; i++) { if (!isSameType(first.args[i], second.args[i], topLevel)) { return false; } } return true; } function isSameFunctionType(first, second, topLevel) { if (first.args.length !== second.args.length) { return false; } for (var i = 0; i < first.args.length; i++) { if (!isSameType(first.args[i], second.args[i], topLevel)) { return false; } } return true; } function doesFunctionTypeContainType(first, second, topLevel) { switch (second.kind) { case "GenericType": { for (const arg of first.args) { if (isSameType(arg, second, topLevel)) { return true; } } } case "FixedType": { for (const arg of first.args) { if (isSameType(arg, second, topLevel)) { return true; } } } } return false; } function isSameObjectLiteralType(first, second) { const processedNames = []; for (const firstPropertyName of Object.keys(first.properties)) { if (second.properties[firstPropertyName]) { // when the types don't match between first and second if (!isSameType(first.properties[firstPropertyName], second.properties[firstPropertyName], false)) { return false; } processedNames.push(firstPropertyName); } else { // when one property exists on first but not second return false; } } for (const secondPropertyName of Object.keys(second.properties)) { if (!processedNames.includes(secondPropertyName)) { // when one property exists on second but not first return false; } } return true; } function isSameObjectLiteralTypeAlias(objectLiteral, expectedType, typedBlocks) { if (expectedType.kind === "GenericType") return true; const expectedTypeAlias = getTypeAlias(expectedType, typedBlocks); if (expectedTypeAlias.kind === "Err") return false; const typeAlias = expectedTypeAlias.value; const processedNames = []; for (const property of typeAlias.properties) { if (objectLiteral.properties[property.name]) { if (!isSameType(property.type, objectLiteral.properties[property.name], false)) { return false; } else { processedNames.push(property.name); } } else { return false; } } for (const property of Object.keys(objectLiteral.properties)) { if (!processedNames.includes(property)) { // when one property exists on second but not first return false; } } return true; } function tagToFixedType(tag) { return (0, types_1.FixedType)(tag.name, tag.args .filter((tag) => tag.type.kind === "GenericType") .map((arg) => arg.type)); } function isATag(type_, typedBlocks) { for (const block of typedBlocks) { switch (block.kind) { case "UnionType": { for (const tag of block.tags) { const tagType = tagToFixedType(tag); if (isSameType(type_, tagType, false)) return (0, result_1.Ok)(block.type); } } } } return (0, result_1.Err)(null); } function isSameType(first, second, topLevel, typedBlocks = []) { if (first.kind === "ObjectLiteralType" && second.kind === "ObjectLiteralType") { return isSameObjectLiteralType(first, second); } if ((first.kind !== "FunctionType" && first.kind !== "ObjectLiteralType" && first.name === "any") || (second.kind !== "FunctionType" && second.kind !== "ObjectLiteralType" && second.name === "any")) { return true; } if ((first.kind === "ObjectLiteralType" && second.kind === "GenericType") || (second.kind === "ObjectLiteralType" && first.kind === "GenericType")) { return true; } if (first.kind === "ObjectLiteralType" || second.kind === "ObjectLiteralType") { return false; } if (first.kind !== second.kind) { if (first.kind === "FunctionType" && second.kind !== "FunctionType") { return doesFunctionTypeContainType(first, second, topLevel); } if (first.kind === "FixedType" && second.kind === "GenericType") { return true; } if (second.kind === "FixedType" && first.kind === "GenericType") { return true; } return false; } switch (first.kind) { case "FixedType": { if (first.name.indexOf(".") > -1) { const split = first.name.split("."); first = Object.assign(Object.assign({}, first), { name: split[split.length - 1] }); } second = second; if (second.name.indexOf(".") > -1) { const split = second.name.split("."); second = Object.assign(Object.assign({}, second), { name: split[split.length - 1] }); } if (isSameFixedType(first, second, topLevel)) { return true; } const isFirstATag = isATag(first, typedBlocks); const isSecondATag = isATag(second, typedBlocks); if (isFirstATag.kind === "Ok") { if (isSameType(isFirstATag.value, second, topLevel, typedBlocks)) { return true; } } if (isSecondATag.kind === "Ok") { if (isSameType(first, isSecondATag.value, topLevel, typedBlocks)) { return true; } } return false; } case "GenericType": { return isSameGenericType(first, second, topLevel); } case "FunctionType": { return isSameFunctionType(first, second, topLevel); } } } exports.isSameType = isSameType; function inferValue(value, expectedType, typedBlocks, imports, valuesInScope) { if (parseInt(value.body, 10)) { return (0, types_1.FixedType)("number", []); } if (value.body === "true" || value.body === "false") { return (0, types_1.FixedType)("boolean", []); } if (value.body === "toString") { if (valuesInScope[`_${value.body}`]) { return valuesInScope[`_${value.body}`]; } } else { if (valuesInScope[value.body]) { return valuesInScope[value.body]; } } return (0, types_1.FixedType)("any", []); } function inferStringValue(value) { return (0, types_1.FixedType)("string", []); } function inferFormatStringValue(value) { return (0, types_1.FixedType)("string", []); } function reduceTypes(types) { return types.reduce((uniques, type) => { if (uniques.filter((unique) => isSameType(unique, type, false)) .length === 0) { uniques.push(type); } return uniques; }, []); } function inferListValue(value, expectedType, typedBlocks, imports, valuesInScope) { if (value.items.length === 0) return (0, result_1.Ok)((0, types_1.FixedType)("List", [(0, types_1.FixedType)("any", [])])); let types = []; let actualExpectedType = (0, types_1.FixedType)("_Inferred", []); if (expectedType.kind === "FixedType" && expectedType.name === "List" && expectedType.args.length > 0) { actualExpectedType = expectedType.args[0]; } for (const item of value.items) { const inferred = inferType(item, actualExpectedType, typedBlocks, imports, valuesInScope); if (inferred.kind === "Err") return inferred; types.push(inferred.value); } const uniqueTypes = reduceTypes(types); return (0, result_1.Ok)((0, types_1.FixedType)("List", uniqueTypes)); } function inferListRange(value) { return (0, types_1.FixedType)("List", [(0, types_1.FixedType)("number", [])]); } function objectLiteralTypeAlias(value, expectedType, typedBlocks, imports, valuesInScope) { const expectedTypeAlias = getTypeAlias(expectedType, typedBlocks); const typeAlias = (0, types_1.TypeAlias)((0, types_1.FixedType)("Inferred", []), value.fields.map((field) => { const listOfExpected = expectedTypeAlias.kind === "Ok" ? expectedTypeAlias.value.properties.filter((prop) => prop.name === field.name) : []; const expected = listOfExpected.length === 0 ? (0, types_1.FixedType)("_Inferred", []) : listOfExpected[0].type; const inferred = inferType(field.value, expected, typedBlocks, imports, valuesInScope); if (inferred.kind === "Err") { return (0, types_1.Property)(field.name, (0, types_1.GenericType)("any")); } return (0, types_1.Property)(field.name, inferred.value); })); return typeAlias; } function objectLiteralType(typeAlias) { const fields = {}; for (const prop of typeAlias.properties) { fields[prop.name] = prop.type; } return (0, types_1.ObjectLiteralType)(fields); } function typeAliasFromObjectLiteralType(objectLiteral) { const fields = []; for (const name of Object.keys(objectLiteral.properties)) { const type_ = objectLiteral.properties[name]; fields.push((0, types_1.Property)(name, type_)); } return (0, types_1.TypeAlias)((0, types_1.FixedType)("Inferred", []), fields); } function inferObjectLiteral(value, expectedType, typedBlocks, imports, valuesInScope) { if (value.base !== null) { return (0, result_1.Ok)((0, types_1.FixedType)("any", [])); } const typeAlias = objectLiteralTypeAlias(value, expectedType, typedBlocks, imports, valuesInScope); if (expectedType.kind !== "FixedType" || expectedType.name === "_Inferred") { return (0, result_1.Ok)(objectLiteralType(typeAlias)); } for (const block of typedBlocks) { if (block.kind != "TypeAlias" || block.properties.length !== typeAlias.properties.length || expectedType.name !== block.type.name) { continue; } let blockMatches = true; for (const inferredProperty of typeAlias.properties) { const hasMatchingBlockProperty = block.properties.filter((prop) => { return (prop.name === inferredProperty.name && isSameType(prop.type, inferredProperty.type, false)); }).length > 0; if (!hasMatchingBlockProperty) { blockMatches = false; break; } } if (blockMatches) { return (0, result_1.Ok)(block.type); } } return (0, result_1.Ok)(objectLiteralType(typeAlias)); } function inferIfStatement(value, expectedType, typedBlocks, imports, valuesInScope) { const ifBranch = inferType(value.ifBody, expectedType, typedBlocks, imports, valuesInScope); const elseBranch = inferType(value.elseBody, expectedType, typedBlocks, imports, valuesInScope); if (ifBranch.kind === "Err") return ifBranch; if (elseBranch.kind === "Err") return elseBranch; if (isSameType(ifBranch.value, elseBranch.value, false)) return (0, result_1.Ok)(ifBranch.value); return (0, result_1.Err)(`Conflicting types: ${typeToString(ifBranch.value)}, ${typeToString(elseBranch.value)}`); } function inferBranch(value, expectedType, typedBlocks, imports, valuesInScope) { return inferType(value.body, expectedType, typedBlocks, imports, valuesInScope); } function inferCaseStatement(value, expectedType, typedBlocks, imports, valuesInScope) { const typesToReduce = []; for (const branch of value.branches) { const inf = inferBranch(branch, expectedType, typedBlocks, imports, valuesInScope); if (inf.kind === "Err") return inf; typesToReduce.push(inf.value); } const branches = reduceTypes(typesToReduce); if (branches.length === 1) return (0, result_1.Ok)(branches[0]); return (0, result_1.Err)(`Conflicting types: ${branches.map(typeToString).join(", ")}`); } function inferAddition(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) { let maybeStringErrorMessage = ""; if (value.left.kind === "StringValue" || value.left.kind === "FormatStringValue") { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`${value.left.body}\${${(0, Derw_1.generateExpression)(value.right)}}\``; } else if (value.right.kind === "StringValue" || value.right.kind === "FormatStringValue") { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${(0, Derw_1.generateExpression)(value.left)}}${value.right.body}\``; } else if (left.value.kind === "FixedType" && left.value.name === "string") { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${(0, Derw_1.generateExpression)(value.left)}}\${${(0, Derw_1.generateExpression)(value.right)}}\``; } else if (right.value.kind === "FixedType" && right.value.name === "string") { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${(0, Derw_1.generateExpression)(value.left)}}\${${(0, Derw_1.generateExpression)(value.right)}}\``; } return (0, result_1.Err)(`Mismatching types between the left of the addition: ${typeToString(left.value)} and the right of the addition: ${typeToString(right.value)}\nIn Derw, types of both sides of an addition must be the same.${maybeStringErrorMessage}`); } return left; } function inferSubtraction(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`); return left; } function inferMultiplication(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`); return left; } function inferDivision(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`); return left; } function inferMod(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`); return left; } function inferLeftPipe(value, expectedType, typedBlocks, imports, valuesInScope) { const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); return right; } function inferRightPipe(value, expectedType, typedBlocks, imports, valuesInScope) { const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); return left; } function getTypeAliasAtPath(value, expectedType, typedBlocks, imports, valuesInScope) { let currentType = valuesInScope[value.path[0]]; if (!currentType) return (0, result_1.Err)(""); let currentTypeAlias = getTypeAlias(currentType, typedBlocks); for (const path of value.path.slice(1)) { if (currentTypeAlias.kind === "Err") return (0, result_1.Err)(""); let found = false; for (const prop of currentTypeAlias.value.properties) { if (prop.name === path) { currentType = prop.type; found = true; break; } } if (!found) { return (0, result_1.Err)(""); } currentTypeAlias = getTypeAlias(currentType, typedBlocks); } if (currentTypeAlias.kind === "Err") return (0, result_1.Err)(""); return (0, result_1.Ok)(currentTypeAlias.value); } function inferModuleReference(value, expectedType, typedBlocks, imports, valuesInScope) { if (value.path.length > 0) { const isAVariablePath = value.path[0][0].toLowerCase() === value.path[0][0]; if (isAVariablePath && value.value.kind === "Value") { const typeAlias = getTypeAliasAtPath(value, expectedType, typedBlocks, imports, valuesInScope); if (typeAlias.kind === "Ok") { for (const prop of typeAlias.value.properties) { if (prop.name === value.value.body) { return (0, result_1.Ok)(prop.type); } } } } } return (0, result_1.Ok)((0, types_1.FixedType)("any", [])); } function inferFunctionCall(value) { return (0, types_1.FixedType)("any", []); } function inferLambda(value) { return (0, types_1.FixedType)("any", []); } function inferLambdaCall(value) { return (0, types_1.FixedType)("any", []); } function tagNames(typedBlocks) { const names = []; for (const block of typedBlocks) { switch (block.kind) { case "TypeAlias": { break; } case "UnionType": { for (const tag of block.tags) { names.push(tag.name); } break; } case "UnionUntaggedType": { break; } } } return names; } function replaceGenerics(type_, replacements) { if (type_.kind === "FunctionType" || type_.kind === "ObjectLiteralType" || type_.kind === "GenericType") { return type_; } return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => { if (arg.kind === "GenericType" && arg.name in replacements) { return replacements[arg.name]; } else { return arg; } }) }); } function inferConstructor(value, expectedType, typedBlocks, imports, valuesInScope) { let seenNameInOtherBlock = false; for (const block of typedBlocks) { if (block.kind === "UnionType") { for (const tag of block.tags) { if (value.constructor === tag.name) { const valid = validateConstructor(value.pattern, expectedType, tag, block, typedBlocks, imports, valuesInScope); const inferredGenericTypes = {}; for (const arg of tag.args) { if (arg.type.kind === "GenericType") { for (const field of value.pattern.fields) { if (arg.name === field.name) { const fieldIsValid = inferType(field.value, arg.type, typedBlocks, imports, valuesInScope); if (fieldIsValid.kind === "Ok") { if (inferredGenericTypes[arg.type.name] !== fieldIsValid.value) { inferredGenericTypes[arg.type.name] = fieldIsValid.value; } } } } } } if (valid.kind === "Err") return valid; return (0, result_1.Ok)(replaceGenerics(block.type, inferredGenericTypes)); } } } else if (block.kind === "TypeAlias") { if (value.constructor === block.type.name) { seenNameInOtherBlock = true; } } } if (isImportedConstructor(value, imports)) { return (0, result_1.Ok)((0, types_1.GenericType)("any")); } const suggestions = (0, distance_1.suggestName)(value.constructor, tagNames(typedBlocks)); const suggestionsErrorMessage = suggestions.length === 0 ? "" : `\nPerhaps you meant one of these? ${suggestions.join(", ")}`; const hasBeenSeenErrorMesssage = seenNameInOtherBlock ? `\n${value.constructor} refers to a type alias, not a union type constructor.` : ""; return (0, result_1.Err)(`Did not find constructor ${value.constructor} in scope.${hasBeenSeenErrorMesssage}${suggestionsErrorMessage}`); } function inferEquality(value) { return (0, types_1.FixedType)("boolean", []); } function inferInEquality(value) { return (0, types_1.FixedType)("boolean", []); } function inferLessThan(value) { return (0, types_1.FixedType)("boolean", []); } function inferLessThanOrEqual(value) { return (0, types_1.FixedType)("boolean", []); } function inferGreaterThan(value) { return (0, types_1.FixedType)("boolean", []); } function inferGreaterThanOrEqual(value) { return (0, types_1.FixedType)("boolean", []); } function inferAnd(value) { return (0, types_1.FixedType)("boolean", []); } function inferOr(value) { return (0, types_1.FixedType)("boolean", []); } function inferListPrepend(value, expectedType, typedBlocks, imports, valuesInScope) { const leftInfer = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope); const rightInfer = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope); if (leftInfer.kind === "Err") { if (value.left.kind === "ObjectLiteral") { const err = validateObjectLiteral(value.left, (0, types_1.FixedType)("_Inferred", []), typedBlocks, imports, valuesInScope); if (err.kind === "Err") return err; } return leftInfer; } if (rightInfer.kind === "Err") return rightInfer; if (rightInfer.value.kind === "GenericType" || (rightInfer.value.kind === "FixedType" && rightInfer.value.name === "any")) return (0, result_1.Ok)((0, types_1.FixedType)("List", [(0, types_1.GenericType)("any")])); if (rightInfer.value.kind === "FunctionType") { return (0, result_1.Err)("Inferred list on right hand side of :: to be a function, not a list"); } if (rightInfer.value.kind === "ObjectLiteralType") { return (0, result_1.Err)("Inferred list on right hand side of :: to be an object literal, not a list"); } if (rightInfer.value.name === "List" && rightInfer.value.args.length > 0) { const isEmptyList = value.right.kind === "ListValue" && value.right.items.length === 0; if (isEmptyList) { return (0, result_1.Ok)((0, types_1.FixedType)("List", [leftInfer.value])); } const listElementType = rightInfer.value.args[0]; if (isSameType(leftInfer.value, listElementType, false)) { return (0, result_1.Ok)(rightInfer.value); } return (0, result_1.Err)(`Invalid types in :: - lefthand (${typeToString(leftInfer.value)}) must match elements of righthand (${typeToString(listElementType)})`); } return (0, result_1.Err)(`Expected list on righthand side of :: but got ${typeToString(rightInfer.value)}.`); } function inferType(expression, expectedType, typedBlocks, imports, valuesInScope) { switch (expression.kind) { case "Value": return (0, result_1.Ok)(inferValue(expression, expectedType, typedBlocks, imports, valuesInScope)); case "StringValue": return (0, result_1.Ok)(inferStringValue(expression)); case "FormatStringValue": return (0, result_1.Ok)(inferFormatStringValue(expression)); case "ListValue": return inferListValue(expression, expectedType, typedBlocks, imports, valuesInScope); case "ListRange": return (0, result_1.Ok)(inferListRange(expression)); case "ObjectLiteral": return inferObjectLiteral(expression, expectedType, typedBlocks, imports, valuesInScope); case "IfStatement": return inferIfStatement(expression, expectedType, typedBlocks, imports, valuesInScope); case "CaseStatement": return inferCaseStatement(expression, expectedType, typedBlocks, imports, valuesInScope); case "Addition": return inferAddition(expression, expectedType, typedBlocks, imports, valuesInScope); case "Subtraction": return inferSubtraction(expression, expectedType, typedBlocks, imports, valuesInScope); case "Multiplication": return inferMultiplication(expression, expectedType, typedBlocks, imports, valuesInScope); case "Division": return inferDivision(expression, expectedType, typedBlocks, imports, valuesInScope); case "Mod": return inferMod(expression, expectedType, typedBlocks, imports, valuesInScope); case "And": return (0, result_1.Ok)(inferAnd(expression)); case "Or": return (0, result_1.Ok)(inferOr(expression)); case "ListPrepend": return inferListPrepend(expression, expectedType, typedBlocks, imports, valuesInScope); case "LeftPipe": return inferLeftPipe(expression, expectedType, typedBlocks, imports, valuesInScope); case "RightPipe": return inferRightPipe(expression, expectedType, typedBlocks, imports, valuesInScope); case "ModuleReference": return inferModuleReference(expression, expectedType, typedBlocks, imports, valuesInScope); case "FunctionCall": return (0, result_1.Ok)(inferFunctionCall(expression)); case "Lambda": return (0, result_1.Ok)(inferLambda(expression)); case "LambdaCall": return (0, result_1.Ok)(inferLambdaCall(expression)); case "Constructor": return inferConstructor(expression, expectedType, typedBlocks, imports, valuesInScope); case "Equality": return (0, result_1.Ok)(inferEquality(expression)); case "InEquality": return (0, result_1.Ok)(inferInEquality(expression)); case "LessThan": return (0, result_1.Ok)(inferLessThan(expression)); case "LessThanOrEqual": return (0, result_1.Ok)(inferLessThanOrEqual(expression)); case "GreaterThan": return (0, result_1.Ok)(inferGreaterThan(expression)); case "GreaterThanOrEqual": return (0, result_1.Ok)(inferGreaterThanOrEqual(expression)); } } exports.inferType = inferType; function typeToString(type) { switch (type.kind) { case "GenericType": { return type.name; } case "FixedType": { const typeArgs = type.args.length === 0 ? "" : " (" + type.args.map(typeToString).join(" ") + ")"; return `${type.name}${typeArgs}`.trim(); } case "FunctionType": { return type.args.map(typeToString).join("->"); } case "ObjectLiteralType": { const out = []; for (const name of Object.keys(type.properties)) { out.push(`${name}: ${typeToString(type.properties[name])}`); } return "{ " + out.join(", ") + " }"; } } } function typeExistsInNamespace(type, blocks, imports) { if (type.kind === "FunctionType") return true; if (type.kind === "ObjectLiteralType") return true; if ((0, builtins_1.isBuiltinType)(type.name)) return true; if (type.name === "List") return true; if (type.kind === "GenericType") return true; for (const block of blocks) { if (isSameType(type, block.type, true)) return true; switch (block.kind) { case "UnionType": { for (const tag of block.tags) { if (isSameType(type, tagToFixedType(tag), true)) return true; } } } } for (const import_ of imports) { for (const module of import_.modules) { for (const exposed of module.exposing) { if (type.name === exposed) return true; } if (type.name.indexOf(".") > -1 && module.alias.kind === "Just" && type.name.split(".")[0] === module.alias.value) { return true; } } } return false; } function finalExpressions(expression) { switch (expression.kind) { case "Value": return []; case "StringValue": return [expression.body]; case "FormatStringValue": return []; case "ListValue": return []; case "ListRange": return []; case "ObjectLiteral": return []; case "IfStatement": return finalExpressions(expression.ifBody).concat(finalExpressions(expression.elseBody)); case "CaseStatement": let expressions = []; for (const branch of expression.branches) { expressions = expressions.concat(finalExpressions(branch.body)); } return expressions; case "Addition": return []; case "Subtraction": return []; case "Multiplication": return []; case "Division": return []; case "Mod": return []; case "And": return []; case "Or": return []; case "ListPrepend": return []; case "LeftPipe": return []; case "RightPipe": return []; case "ModuleReference": return []; case "FunctionCall": return []; case "Lambda": return []; case "LambdaCall": return []; case "Constructor": return []; case "Equality": return []; case "InEquality": return []; case "LessThan": return []; case "LessThanOrEqual": return []; case "GreaterThan": return []; case "GreaterThanOrEqual": return []; } } function allFinalExpressions(block) { switch (block.kind) { case "Const": { return finalExpressions(block.value); } case "Function": { return finalExpressions(block.body); } default: { return []; } } } function validateAllBranchesCovered(typedBlocks, containingBlock, expression) { const hasDefault = expression.branches.filter((b) => b.pattern.kind === "Default").length > 0; const casePattern = expression.predicate; let predicateType = null; if (casePattern.kind === "Value") { if (containingBlock.kind === "Function") { for (const arg of containingBlock.args) { if (arg.kind === "FunctionArg") { if (arg.name === casePattern.body) { predicateType = arg.type; } } } } } if (predicateType && (predicateType === null || predicateType === void 0 ? void 0 : predicateType.kind) === "FixedType") { const matchingBlocks = typedBlocks.filter((b) => isSameType(b.type, predicateType, false)); if (matchingBlocks.length > 0) { const matchingBlock = matchingBlocks[0]; if (matchingBlock.kind === "UnionUntaggedType") { const strings = matchingBlock.values.map((s) => s.body); const seenStrings = []; for (const branch of expression.branches) { if (branch.pattern.kind === "StringValue") { seenStrings.push(branch.pattern.body); } } const missingBranches = strings.filter((s) => seenStrings.indexOf(s) === -1); const extraBranches = seenStrings.filter((s) => strings.indexOf(s) === -1); let errors = []; if (missingBranches.length > 0 && !hasDefault) { errors.push(`All possible branches of a untagged union must be covered. I expected a branch for ${missingBranches .map((s) => `"${s}"`) .join(" | ")} but they were missing. If you don't need one, have a default branch`); } if (extraBranches.length > 0) { errors.push(`I got too many branches. The branches for ${extraBranches .map((s) => `"${s}"`) .join(" | ")} aren't part of the untagged union so will never be true. Remove them.`); } if (errors.length > 0) { errors = [ `The case statement did not match the untagged union ${typeToString(predicateType)}`, ...errors, ]; return (0, result_1.Err)(errors.join("\n")); } return (0, result_1.Ok)(true); } else if (matchingBlock.kind === "UnionType") { const names = matchingBlock.tags.map((t) => t.name); const seenNames = []; for (const branch of expression.branches) { if (branch.pattern.kind === "Destructure") { seenNames.push(branch.pattern.constructor); } } const missingBranches = names.filter((s) => seenNames.indexOf(s) === -1); const extraBranches = seenNames.filter((s) => names.indexOf(s) === -1); let errors = []; if (missingBranches.length > 0 && !hasDefault) { errors.push(`All possible branches of a union must be covered. I expected a branch for ${missingBranches.join(" | ")} but they were missing. If you don't need one, have a default branch`); } if (extraBranches.length > 0) { errors.push(`I got too many branches. The branches for ${extraBranches.join(" | ")} aren't part of the union so will never be true. Remove them.`); } if (errors.length > 0) { errors = [ `The case statement did not match the union ${typeToString(predicateType)}`, ...errors, ]; return (0, result_1.Err)(errors.join("\n")); } return (0, result_1.Ok)(true); } } } return (0, result_1.Ok)(true); } function getCasesFromFunction(block) { const body = block.body; const statements = []; if (body.kind === "CaseStatement") statements.push(body); return statements; } exports.getCasesFromFunction = getCasesFromFunction; function validateAllCasesCovered(block, typedBlocks) { if (block.kind !== "Function") { return []; } const cases = getCasesFromFunction(block); const invalidBranches = []; for (const case_ of cases) { const valid = validateAllBranchesCovered(typedBlocks, block, case_); if (valid.kind === "Err") { invalidBranches.push(valid.error); } } return invalidBranches; } exports.validateAllCasesCovered = validateAllCasesCovered; function validateObjectLiteralType(objectLiteralType, expectedType, typedBlocks, imports) { const typeAlias = typeAliasFromObjectLiteralType(objectLiteralType); if (expectedType.kind !== "FixedType") return (0, result_1.Ok)(null); for (const typeBlock of typedBlocks) { if (typeBlock.kind === "UnionType" || typeBlock.kind === "UnionUntaggedType" || typeBlock.type.name !== expectedType.name) continue; const missingPropertyFromTypeAlias = []; const addedProperties = []; const incorrectProperties = []; for (const property of typeAlias.properties) { let found = false; for (const typeProperty of typeBlock.properties) { if (property.name === typeProperty.name) { if (isImportedType(typeProperty.type, imports) || isImportedType(property.type, imports)) { found = true; continue; } else if (!isSameType(property.type, typeProperty.type, false)) { incorrectProperties.push(`${property.name}: Expected ${typeToString(typeProperty.type)} but got ${typeToString(property.type)}`); } found = true; break; } } if (!found) { addedProperties.push(property); } } for (const typeProperty of typeBlock.properties) { let found = false; for (const property of typeAlias.properties) { if (property.name === typeProperty.name) { found = true; break; } } if (!found) { missingPropertyFromTypeAlias.push(typeProperty); } } if (missingPropertyFromTypeAlias.length > 0 || addedProperties.length > 0 || incorrectProperties.length > 0) { let errorMessage = ""; if (missingPropertyFromTypeAlias.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The type alias had these properties which are missing in this object literal: ${missingPropertyFromTypeAlias .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (addedProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The object literal had these properties which aren't in the type alias: ${addedProperties .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (incorrectProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `Invalid properties: ${incorrectProperties.join(" | ")}`; } return (0, result_1.Err)(`Object literal type alias ${typeToString(typeBlock.type)} did not match the value due to:\n${errorMessage}`); } } return (0, result_1.Ok)(null); } exports.validateObjectLiteralType = validateObjectLiteralType; function getUntaggedUnion(type_, typedBlocks) { if (type_.kind !== "FixedType") return (0, result_1.Err)(null); for (const block of typedBlocks) { if (block.kind !== "UnionUntaggedType") continue; if (block.type.name === type_.name) { return (0, result_1.Ok)(block); } } return (0, result_1.Err)(null); } function getTypeAlias(type_, typedBlocks) { if (type_.kind !== "FixedType") return (0, result_1.Err)(null); for (const block of typedBlocks) { if (block.kind !== "TypeAlias") continue; if (block.type.name === type_.name) { return (0, result_1.Ok)(block); } } return (0, result_1.Err)(null); } function validateObjectLiteral(objectLiteral, expectedType, typedBlocks, imports, valuesInScope) { if (objectLiteral.base !== null) { return (0, result_1.Ok)(null); } if (expectedType.kind !== "FixedType") return (0, result_1.Ok)(null); const typeAlias = objectLiteralTypeAlias(objectLiteral, expectedType, typedBlocks, imports, valuesInScope); for (const typeBlock of typedBlocks) { if (typeBlock.kind === "UnionType" || typeBlock.kind === "UnionUntaggedType" || typeBlock.type.name !== expectedType.name) continue; const missingPropertyFromTypeAlias = []; const addedProperties = []; const incorrectProperties = []; for (const property of typeAlias.properties) { let found = false; for (const typeProperty of typeBlock.properties) { if (property.name === typeProperty.name) { const maybeUntaggedUnionBlock = getUntaggedUnion(typeProperty.type, typedBlocks); if (maybeUntaggedUnionBlock.kind === "Ok") { const fieldValue = objectLiteral.fields.filter((field) => field.name === property.name)[0]; if (!fieldValue) continue; if (fieldValue.value.kind !== "StringValue") continue; const stringFieldValue = fieldValue.value.body; const isCovered = maybeUntaggedUnionBlock.value.values.filter((v) => v.body === stringFieldValue).length > 0; if (!isCovered) { incorrectProperties.push(`${fieldValue.name}: Expected ${typeToString(maybeUntaggedUnionBlock.value.type)}, composed of ${maybeUntaggedUnionBlock.value.values .map((v) => `"${v.body}"`) .join(" | ")}\` but got "${stringFieldValue}"`); } } else if (!isSameType(property.type, typeProperty.type, false)) { incorrectProperties.push(`${property.name}: Expected ${typeToString(typeProperty.type)} but got ${typeToString(property.type)}`); } found = true; break; } } if (!found) { addedProperties.push(property); } } for (const typeProperty of typeBlock.properties) { let found = false; for (const property of typeAlias.properties) { if (property.name === typeProperty.name) { found = true; break; } } if (!found) { missingPropertyFromTypeAlias.push(typeProperty); } } if (missingPropertyFromTypeAlias.length > 0 || addedProperties.length > 0 || incorrectProperties.length > 0) { let errorMessage = ""; if (missingPropertyFromTypeAlias.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The type alias had these properties which are missing in this object literal: ${missingPropertyFromTypeAlias .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (addedProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The object literal had these properties which aren't in the type alias: ${addedProperties .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (incorrectProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `Invalid properties: ${incorrectProperties.join(" | ")}`; } return (0, result_1.Err)(`Mismatching type for type alias ${typeToString(typeBlock.type)}\n${errorMessage}`); } } return (0, result_1.Ok)(null); } function unifyType(type_, typeReplacements) { switch (type_.kind) { case "FixedType": { return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => { return unifyType(arg, typeReplacements); }) }); } case "FunctionType": { return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => { return unifyType(arg, typeReplacements); }) }); } case "GenericType": { return typeReplacements[type_.name] || type_; } case "ObjectLiteralType": { const newProperties = {}; for (const propName of Object.keys(type_.properties)) { const propValue = type_.properties[propName]; newProperties[propName] = unifyType(propValue, typeReplacements); } return Object.assign(Object.assign({}, type_), { properties: newProperties }); } } } function unifyTag(tag, typeReplacements) { return Object.assign(Object.assign({}, tag), { args: tag.args.map((arg) => { return Object.assign(Object.assign({}, arg), { type: unifyType(arg.type, typeReplacements) }); }) }); } function unifyUnionType(unionType, expectedType) { if (expectedType.kind !== "FixedType") return unionType; if (unionType.type.name !== expectedType.name) { return unionType; } const toBeReplaced = {}; for (var i = 0; i < expectedType.args.length; i++) { const currentGenericArg = unionType.type.args[i]; if (currentGenericArg.kind !== "GenericType") { continue; } const actualFixedArg = expectedType.args[i]; toBeReplaced[currentGenericArg.name] = actualFixedArg; } return Object.assign(Object.assign({}, unionType), { tags: unionType.tags.map((tag) => { return unifyTag(tag, toBeReplaced); }) }); } function tagTypeAlias(tag, genericUnionType, expectedType, typedBlocks) { const unionType = unifyUnionType(genericUnionType, expectedType); const tagToUse = unionType.tags.filter((localTag) => localTag.name === tag.name)[0] || tag; const typeAlias = (0, types_1.TypeAlias)((0, types_1.FixedType)("Inferred", []), tagToUse.args.map((arg) => { return (0, types_1.Property)(arg.name, arg.type); })); return typeAlias; } function findReplacement(inferredType, expectedType, typedBlocks) { switch (inferredType.kind) { case "FixedType": { if (expectedType.kind !== "FixedType" || expectedType.name !== inferredType.name || expectedType.args.length !== inferredType.args.length) { return inferredType; } const args = []; for (let i = 0; i < inferredType.args.length; i++) { const inferredArg = inferredType.args[i]; const expectedArg = expectedType.args[i]; args.push(findReplacement(inferredArg, expectedArg, typedBlocks)); } return Object.assign(