UNPKG

babel-plugin-typecheck

Version:

Transforms flow type annotations into runtime type checks.

1,643 lines (1,537 loc) 65.1 kB
import generate from "babel-generator"; type Node = { type: string; }; type Identifier = { type: string; name: string; }; type QualifiedTypeIdentifier = { id: Identifier; qualification: Identifier|QualifiedTypeIdentifier; }; type TypeAnnotation = { type: string; }; interface StringLiteralTypeAnnotation extends TypeAnnotation { type: 'StringLiteralTypeAnnotation'; } interface NumericLiteralTypeAnnotation extends TypeAnnotation { type: 'NumericLiteralTypeAnnotation'; } interface BooleanLiteralTypeAnnotation extends TypeAnnotation { type: 'BooleanLiteralTypeAnnotation'; } type Scope = {}; type NodePath = { type: string; node: Node; scope: Scope; }; /** * # Typecheck Transformer */ export default function ({types: t, template}): Object { // constants used when statically verifying types const TYPE_INVALID = 0; const TYPE_VALID = 1; const TYPE_UNKNOWN = 2; /** * Binary Operators that can only produce boolean results. */ const BOOLEAN_BINARY_OPERATORS: string[] = [ '==', '===', '>=', '<=', '>', '<', 'instanceof' ]; const checks: Object = createChecks(); const staticChecks: Object = createStaticChecks(); const checkIsArray: (() => Node) = expression(`Array.isArray(input)`); const checkIsMap: (() => Node) = expression(`input instanceof Map`); const checkIsSet: (() => Node) = expression(`input instanceof Set`); const checkIsObject: (() => Node) = expression(`input != null && typeof input === 'object'`); const checkNotNull: (() => Node) = expression(`input != null`); const checkEquals: (() => Node) = expression(`input === expected`); const declareTypeChecker: (() => Node) = template(` const id = function id (input) { return check; }; `); const guard: (() => Node) = template(` if (!check) { throw new TypeError(message); } `); const thrower: (() => Node) = template(` if (check) { ret; } else { throw new TypeError(message); } `); const readableName: (() => Node) = expression(` input === null ? 'null' : typeof input === 'object' && input.constructor ? input.constructor.name || '[Unknown Object]' : typeof input `); const checkMapKeys: (() => Node) = expression(` input instanceof Map && Array.from(input.keys()).every(key => keyCheck) `); const checkMapValues: (() => Node) = expression(` input instanceof Map && Array.from(input.values()).every(value => valueCheck) `); const checkMapEntries: (() => Node) = expression(` input instanceof Map && Array.from(input).every(([key, value]) => keyCheck && valueCheck) `); const checkSetEntries: (() => Node) = expression(` input instanceof Set && Array.from(input).every(value => valueCheck) `); const PRAGMA_IGNORE_STATEMENT = /typecheck:\s*ignore\s+statement/i; const PRAGMA_IGNORE_FILE = /typecheck:\s*ignore\s+file/i; const visitors = { Statement (path: NodePath): void { maybeSkip(path); }, TypeAlias (path: NodePath): void { if (maybeSkip(path)) { return; } path.replaceWith(createTypeAliasChecks(path)); }, InterfaceDeclaration (path: NodePath): void { if (maybeSkip(path)) { return; } path.replaceWith(createInterfaceChecks(path)); }, ExportNamedDeclaration (path: NodePath): void { if (maybeSkip(path)) { return; } const {node, scope} = path; if (node.declaration && node.declaration.type === 'TypeAlias') { path.replaceWith(t.exportNamedDeclaration( createTypeAliasChecks(path.get('declaration')), [], null )); } }, ImportDeclaration (path: NodePath): void { if (maybeSkip(path)) { return; } if (path.node.importKind !== 'type') { return; } const [declarators, specifiers] = path.get('specifiers') .map(specifier => { const local = specifier.get('local'); const tmpId = path.scope.generateUidIdentifierBasedOnNode(local.node); const replacement = t.importSpecifier(tmpId, specifier.node.imported); const id = t.identifier(local.node.name); id.isTypeChecker = true; const declarator = t.variableDeclarator(id, tmpId); declarator.isTypeChecker = true; return [declarator, replacement]; }) .reduce(([declarators, specifiers], [declarator, specifier]) => { declarators.push(declarator); specifiers.push(specifier); return [declarators, specifiers]; }, [[], []]); const declaration = t.variableDeclaration('var', declarators); declaration.isTypeChecker = true; path.replaceWithMultiple([ t.importDeclaration(specifiers, path.node.source), declaration ]); }, Function: { enter (path: NodePath): void { if (maybeSkip(path)) { return; } const {node, scope} = path; const paramChecks = collectParamChecks(path); if (node.type === "ArrowFunctionExpression" && node.expression) { node.expression = false; node.body = t.blockStatement([t.returnStatement(node.body)]); } node.body.body.unshift(...paramChecks); node.savedTypeAnnotation = node.returnType; node.returnCount = 0; }, exit (path: NodePath): void { const {node, scope} = path; const isVoid = node.savedTypeAnnotation ? maybeNullableAnnotation(node.savedTypeAnnotation) : null; if (!node.returnCount && isVoid === false) { throw path.buildCodeFrameError(`Function ${node.id ? `"${node.id.name}" ` : ''}did not return a value, expected ${humanReadableType(node.savedTypeAnnotation, scope)}`); } } }, ReturnStatement (path: NodePath): void { if (maybeSkip(path)) { return; } const {node, parent, scope} = path; const fn = path.getFunctionParent(); if (!fn) { return; } fn.node.returnCount++; if (node.isTypeChecked) { return; } const {returnType} = fn.node; if (!returnType) { return; } if (!node.argument) { if (maybeNullableAnnotation(returnType) === false) { throw path.buildCodeFrameError(`Function ${fn.node.id ? `"${fn.node.id.name}" ` : ''}did not return a value, expected ${humanReadableType(returnType, path.scope)}`); } return; } let id; if (node.argument.type === 'Identifier' || t.isLiteral(node.argument)) { id = node.argument; } else { id = scope.generateUidIdentifierBasedOnNode(node.argument); } const ok = staticCheckAnnotation(path.get("argument"), returnType); if (ok === true) { return; } else if (ok === false) { throw path.buildCodeFrameError(`Invalid return type, expected ${humanReadableType(returnType, scope)}`); } const check = checkAnnotation(id, returnType, scope); if (!check) { return; } if (parent.type !== 'BlockStatement' && parent.type !== 'Program') { const block = []; if (node.argument.type !== 'Identifier' && !t.isLiteral(node.argument)) { scope.push({id: id}); block.push(t.expressionStatement( t.assignmentExpression( '=', id, node.argument )) ); } const ret = t.returnStatement(id); ret.isTypeChecked = true; block.push(thrower({ check, ret, message: returnTypeErrorMessage(path, fn.node, id) })); path.replaceWith(t.blockStatement(block)); } else { if (node.argument.type !== 'Identifier' && !t.isLiteral(node.argument)) { scope.push({id: id}); path.insertBefore(t.expressionStatement( t.assignmentExpression( '=', id, node.argument )) ); } const ret = t.returnStatement(id); ret.isTypeChecked = true; path.replaceWith(thrower({ check, ret, message: returnTypeErrorMessage(path, fn.node, id) })); } }, VariableDeclaration (path: NodePath): void { if (maybeSkip(path)) { return; } const {node, scope} = path; const collected = []; const declarations = path.get("declarations"); for (let i = 0; i < node.declarations.length; i++) { const declaration = node.declarations[i]; const {id, init} = declaration; if (!id.typeAnnotation || id.hasBeenTypeChecked) { continue; } id.savedTypeAnnotation = id.typeAnnotation; id.hasBeenTypeChecked = true; const ok = staticCheckAnnotation(declarations[i], id.typeAnnotation); if (ok === true) { continue; } else if (ok === false) { throw path.buildCodeFrameError(`Invalid assignment value, expected ${humanReadableType(id.typeAnnotation, scope)}`); } const check = checkAnnotation(id, id.typeAnnotation, scope); if (check) { collected.push(guard({ check, message: varTypeErrorMessage(id, scope) })); } } if (collected.length > 0) { const check = collected.reduce((check, branch) => { branch.alternate = check; return branch; }); if (path.parent.type === 'Program' || path.parent.type === 'BlockStatement') { path.insertAfter(check); } else if (path.parent.type === 'ExportNamedDeclaration' || path.parent.type === 'ExportDefaultDeclaration' || path.parent.type === 'ExportAllDeclaration' || path.parentPath.isForXStatement()) { path.parentPath.insertAfter(check); } else { path.replaceWith(t.blockStatement([node, check])); } } }, AssignmentExpression (path: NodePath): void { if (maybeSkip(path)) { return; } const {node, scope} = path; if (node.hasBeenTypeChecked || node.left.hasBeenTypeChecked || !t.isIdentifier(node.left)) { return; } const binding = scope.getBinding(node.left.name); if (!binding) { return; } else if (binding.path.type !== 'VariableDeclarator') { return; } let annotation = path.get('left').getTypeAnnotation(); if (annotation.type === 'AnyTypeAnnotation') { const item = binding.path.get('id'); annotation = item.node.savedTypeAnnotation || item.getTypeAnnotation(); } node.hasBeenTypeChecked = true; node.left.hasBeenTypeChecked = true; if (annotation.type === 'AnyTypeAnnotation') { annotation = getAnnotation(path.get('right')); if (allowsAny(annotation)) { return; } } const id = node.left; const right = path.get('right'); const ok = staticCheckAnnotation(right, annotation); if (ok === true) { return; } else if (ok === false) { throw path.buildCodeFrameError(`Invalid assignment value, expected ${humanReadableType(annotation, scope)}`); } const check = checkAnnotation(id, annotation, scope); if (!id.typeAnnotation) { id.typeAnnotation = annotation; } id.hasBeenTypeChecked = true; if (check) { path.getStatementParent().insertAfter(guard({ check, message: varTypeErrorMessage(id, scope) })); } }, TypeCastExpression (path: NodePath): void { const {node} = path; let target; switch (node.expression.type) { case 'Identifier': target = node.expression; break; case 'AssignmentExpression': target = node.expression.left; break; default: // unsupported. return; } const id = path.scope.getBindingIdentifier(target.name); if (!id) { return; } id.savedTypeAnnotation = path.getTypeAnnotation(); } }; return { visitor: { Program (path: NodePath) { for (let child of path.get('body')) { if (maybeSkipFile(child)) { return; } } path.traverse(visitors); } } } function createChecks (): Object { return { number: expression(`typeof input === 'number'`), numericLiteral: checkNumericLiteral, boolean: expression(`typeof input === 'boolean'`), booleanLiteral: checkBooleanLiteral, function: expression(`typeof input === 'function'`), string: expression(`typeof input === 'string'`), stringLiteral: checkStringLiteral, symbol: expression(`typeof input === 'symbol'`), undefined: expression(`input === undefined`), null: expression(`input === null`), void: expression(`input == null`), instanceof: expression(`input instanceof type`), type: expression(`type(input)`), mixed: () => null, any: () => null, union: checkUnion, intersection: checkIntersection, array: checkArray, map: checkMap, set: checkSet, tuple: checkTuple, object: checkObject, nullable: checkNullable, typeof: checkTypeof }; } function createStaticChecks (): Object { return { symbol (path: NodePath): ?boolean { return maybeSymbolAnnotation(getAnnotation(path)); }, instanceof ({path, type}): ?boolean { const {node, scope} = path; if (type.name === 'Object' && node.type === 'ObjectExpression' && !scope.hasBinding('Object')) { return true; } else if (type.name === 'Map' && !scope.hasBinding('Map')) { return null; } else if (type.name === 'Set' && !scope.hasBinding('Set')) { return null; } return maybeInstanceOfAnnotation(getAnnotation(path), type); }, type ({path, type}): ?boolean { return null; }, }; } function compareAnnotations (a: TypeAnnotation, b: TypeAnnotation): ?boolean { if (a.type === 'TypeAnnotation') { a = a.typeAnnotation; } if (b.type === 'TypeAnnotation') { b = b.typeAnnotation; } switch (a.type) { case 'StringTypeAnnotation': return maybeStringAnnotation(b); case 'StringLiteralTypeAnnotation': return compareStringLiteralAnnotations(a, b); case 'NumberTypeAnnotation': return maybeNumberAnnotation(b); case 'NumericLiteralTypeAnnotation': return compareNumericLiteralAnnotations(a, b); case 'BooleanTypeAnnotation': return maybeBooleanAnnotation(b); case 'BooleanLiteralTypeAnnotation': return compareBooleanLiteralAnnotations(a, b); case 'FunctionTypeAnnotation': return maybeFunctionAnnotation(b); case 'AnyTypeAnnotation': return null; case 'MixedTypeAnnotation': return null; case 'ObjectTypeAnnotation': return compareObjectAnnotation(a, b); case 'ArrayTypeAnnotation': return compareArrayAnnotation(a, b); case 'GenericTypeAnnotation': return compareGenericAnnotation(a, b); case 'TupleTypeAnnotation': return compareTupleAnnotation(a, b); case 'UnionTypeAnnotation': return compareUnionAnnotation(a, b); case 'IntersectionTypeAnnotation': return compareIntersectionAnnotation(a, b); case 'NullableTypeAnnotation': return compareNullableAnnotation(a, b); default: return null; } } function compareStringLiteralAnnotations (a: StringLiteralTypeAnnotation, b: TypeAnnotation): ?boolean { if (b.type === 'StringLiteralTypeAnnotation') { return a.value === b.value; } else { return maybeStringAnnotation(b); } } function compareBooleanLiteralAnnotations (a: BooleanLiteralTypeAnnotation, b: TypeAnnotation): ?boolean { if (b.type === 'BooleanLiteralTypeAnnotation') { return a.value === b.value; } else { return maybeBooleanAnnotation(b); } } function compareNumericLiteralAnnotations (a: NumericLiteralTypeAnnotation, b: TypeAnnotation): ?boolean { if (b.type === 'NumericLiteralTypeAnnotation') { return a.value === b.value; } else { return maybeNumberAnnotation(b); } } function unionComparer (a: TypeAnnotation, b: TypeAnnotation, comparator: (a:TypeAnnotation, b:TypeAnnotation) => ?boolean): ?boolean { let falseCount = 0; let trueCount = 0; if (!a.types) { return null; } for (let type of a.types) { const result = comparator(type, b); if (result === true) { if (b.type !== 'UnionTypeAnnotation') { return true; } trueCount++; } else if (result === false) { if (b.type === 'UnionTypeAnnotation') { return false; } falseCount++; } } if (falseCount === a.types.length) { return false; } else if (trueCount === a.types.length) { return true; } else { return null; } } function intersectionComparer (a: TypeAnnotation, b: TypeAnnotation, comparator: (a:TypeAnnotation, b:TypeAnnotation) => ?boolean): ?boolean { let falseCount = 0; let trueCount = 0; if (!a.types) { return null; } for (let type of a.types) { const result = comparator(type, b); if (result === true) { trueCount++; } else if (result === false) { return false; } } if (trueCount === a.types.length) { return true; } else { return null; } } function compareObjectAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return compareObjectAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareObjectAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareObjectAnnotation); case 'VoidTypeAnnotation': case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': case 'StringTypeAnnotation': case 'StringLiteralTypeAnnotation': case 'NumberTypeAnnotation': case 'NumericLiteralTypeAnnotation': case 'FunctionTypeAnnotation': return false; default: return null; } } function compareArrayAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return compareArrayAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareArrayAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareArrayAnnotation); case 'VoidTypeAnnotation': case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': case 'StringTypeAnnotation': case 'StringLiteralTypeAnnotation': case 'NumberTypeAnnotation': case 'NumericLiteralTypeAnnotation': case 'FunctionTypeAnnotation': return false; default: return null; } } function compareGenericAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return compareGenericAnnotation(a, b.typeAnnotation); case 'GenericTypeAnnotation': if (b.id.name === a.id.name) { return true; } else { return null; } case 'UnionTypeAnnotation': return unionComparer(a, b, compareGenericAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareGenericAnnotation); default: return null; } } function compareTupleAnnotation (a: Node, b: Node): ?boolean { if (b.type === 'TupleTypeAnnotation') { if (b.types.length === 0) { return null; } else if (b.types.length < a.types.length) { return false; } return a.types.every((type, index) => compareAnnotations(type, b.types[index])); } switch (b.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return compareTupleAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareTupleAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareTupleAnnotation); case 'VoidTypeAnnotation': case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': case 'StringTypeAnnotation': case 'StringLiteralTypeAnnotation': case 'NumberTypeAnnotation': case 'NumericLiteralTypeAnnotation': case 'FunctionTypeAnnotation': return false; default: return null; } } function compareUnionAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'NullableTypeAnnotation': return compareUnionAnnotation(a, b.typeAnnotation); case 'AnyTypeAnnotation': case 'MixedTypeAnnotation': return null; default: return unionComparer(a, b, compareAnnotations); } } function compareNullableAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'TypeAnnotation': case 'FunctonTypeParam': return compareNullableAnnotation(a, b.typeAnnotation); case 'NullableTypeAnnotation': case 'VoidTypeAnnotation': return null; } if (compareAnnotations(a.typeAnnotation, b) === true) { return true; } else { return null; } } function arrayExpressionToTupleAnnotation (path: NodePath): TypeAnnotation { const elements = path.get('elements'); return t.tupleTypeAnnotation(elements.map(element => getAnnotation(element))); } function checkNullable ({input, type, scope}): ?Node { const check = checkAnnotation(input, type, scope); if (!check) { return; } return t.logicalExpression( "||", checks.void({input}), check ); } function checkTypeof ({input, annotation, scope}): ?Node { switch (annotation.type) { case 'GenericTypeAnnotation': const {id} = annotation; const path = Object.assign({}, input, {type: id.type, node: id, scope}); return checkAnnotation(input, getAnnotation(path), scope); default: return checkAnnotation(input, annotation, scope); } } function checkStringLiteral ({input, annotation}): ?Node { return checkEquals({input, expected: t.stringLiteral(annotation.value)}); } function checkNumericLiteral ({input, annotation}): ?Node { return checkEquals({input, expected: t.numericLiteral(annotation.value)}); } function checkBooleanLiteral ({input, annotation}): ?Node { return checkEquals({input, expected: t.booleanLiteral(annotation.value)}); } function checkUnion ({input, types, scope}): ?Node { const checks = types.map(type => checkAnnotation(input, type, scope)).filter(identity); return checks.reduce((last, check, index) => { if (last == null) { return check; } return t.logicalExpression( "||", last, check ); }, null); } function checkIntersection ({input, types, scope}): ?Node { const checks = types.map(type => checkAnnotation(input, type, scope)).filter(identity); return checks.reduce((last, check, index) => { if (last == null) { return check; } return t.logicalExpression( "&&", last, check ); }, null); } function checkMap ({input, types, scope}): Node { const [keyType, valueType] = types; const key = t.identifier('key'); const value = t.identifier('value'); const keyCheck = keyType ? checkAnnotation(key, keyType, scope) : null; const valueCheck = valueType ? checkAnnotation(value, valueType, scope) : null; if (!keyCheck) { if (!valueCheck) { return checkIsMap({input}); } else { return checkMapValues({input, value, valueCheck}); } } else { if (!valueCheck) { return checkMapKeys({input, key, keyCheck}); } else { return checkMapEntries({input, key, value, keyCheck, valueCheck}); } } } function checkSet ({input, types, scope}): Node { const [valueType] = types; const value = t.identifier('value'); const valueCheck = valueType ? checkAnnotation(value, valueType, scope) : null; if (!valueCheck) { return checkIsSet({input}); } else { return checkSetEntries({input, value, valueCheck}); } } function checkArray ({input, types, scope}): Node { if (!types || types.length === 0) { return checkIsArray({input}); } else if (types.length === 1) { const item = t.identifier('item'); const type = types[0]; const check = checkAnnotation(item, type, scope); if (!check) { return checkIsArray({input}); } return t.logicalExpression( '&&', checkIsArray({input}), t.callExpression( t.memberExpression(input, t.identifier('every')), [t.functionExpression(null, [item], t.blockStatement([ t.returnStatement(check) ]))] ) ); } else { // This is a tuple const checks = types.map( (type, index) => checkAnnotation( t.memberExpression( input, t.numericLiteral(index), true ), type, scope ) ).filter(identity); const checkLength = t.binaryExpression( '>=', t.memberExpression( input, t.identifier('length') ), t.numericLiteral(types.length) ); return checks.reduce((last, check, index) => { return t.logicalExpression( "&&", last, check ); }, t.logicalExpression( '&&', checkIsArray({input}), checkLength )); } } function checkTuple ({input, types, scope}): Node { if (types.length === 0) { return checkIsArray({input}); } // This is a tuple const checks = types.map( (type, index) => checkAnnotation( t.memberExpression( input, t.numericLiteral(index), true ), type, scope ) ).filter(identity); const checkLength = t.binaryExpression( '>=', t.memberExpression( input, t.identifier('length') ), t.numericLiteral(types.length) ); return checks.reduce((last, check, index) => { return t.logicalExpression( "&&", last, check ); }, t.logicalExpression( '&&', checkIsArray({input}), checkLength )); } function checkObject ({input, properties, scope}): Node { const check = properties.reduce((expr, prop, index) => { const target = t.memberExpression(input, prop.key); let check = checkAnnotation(target, prop.value, scope); if (check) { if (prop.optional) { check = t.logicalExpression( '||', checks.undefined({input: target}), check ); } return t.logicalExpression( "&&", expr, check ); } else { return expr; } }, checkIsObject({input})); return check; } function createTypeAliasChecks (path: NodePath): Node { const {node, scope} = path; const {id, right: annotation} = node; const input = t.identifier('input'); const check = checkAnnotation(input, annotation, scope) || t.booleanLiteral(true); const declaration = declareTypeChecker({id, check}); declaration.isTypeChecker = true; return declaration; } function createInterfaceChecks (path: NodePath): Node { const {node, scope} = path; const {id, body: annotation} = node; const input = t.identifier('input'); const check = node.extends.reduce( (check, extender) => { return t.logicalExpression( '&&', check, checkAnnotation(input, t.genericTypeAnnotation(extender.id), path.scope) ); return check; }, checkAnnotation(input, annotation, scope) || t.booleanLiteral(true) ); const declaration = declareTypeChecker({id, check}); declaration.isTypeChecker = true; return declaration; } function checkAnnotation (input: Node, annotation: TypeAnnotation, scope: Scope): ?Node { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': return checkAnnotation(input, annotation.typeAnnotation, scope); case 'TypeofTypeAnnotation': return checks.typeof({input, annotation: annotation.argument, scope}); case 'GenericTypeAnnotation': if (annotation.id.name === 'Array') { return checks.array({input, types: annotation.typeParameters ? annotation.typeParameters.params : [], scope}); } else if (annotation.id.name === 'Map' && !scope.hasBinding('Map')) { return checks.map({input, types: annotation.typeParameters ? annotation.typeParameters.params : [], scope}); } else if (annotation.id.name === 'Set' && !scope.hasBinding('Set')) { return checks.set({input, types: annotation.typeParameters ? annotation.typeParameters.params : [], scope}); } else if (annotation.id.name === 'Function') { return checks.function({input}); } else if (annotation.id.name === 'Symbol') { return checks.symbol({input}); } else if (isTypeChecker(annotation.id, scope)) { return checks.type({input, type: annotation.id}); } else if (isPolymorphicType(annotation.id, scope)) { return; } else { return checks.instanceof({input, type: createTypeExpression(annotation.id)}); } case 'TupleTypeAnnotation': return checks.tuple({input, types: annotation.types, scope}); case 'NumberTypeAnnotation': return checks.number({input}); case 'NumericLiteralTypeAnnotation': return checks.numericLiteral({input, annotation}); case 'BooleanTypeAnnotation': return checks.boolean({input}); case 'BooleanLiteralTypeAnnotation': return checks.booleanLiteral({input, annotation}); case 'StringTypeAnnotation': return checks.string({input}); case 'StringLiteralTypeAnnotation': return checks.stringLiteral({input, annotation}); case 'UnionTypeAnnotation': return checks.union({input, types: annotation.types, scope}); case 'IntersectionTypeAnnotation': return checks.intersection({input, types: annotation.types, scope}); case 'ObjectTypeAnnotation': return checks.object({input, properties: annotation.properties, indexers: annotation.indexers, scope}); case 'ArrayTypeAnnotation': return checks.array({input, types: [annotation.elementType || t.anyTypeAnnotation()], scope}); case 'FunctionTypeAnnotation': return checks.function({input, params: annotation.params, returnType: annotation.returnType}); case 'MixedTypeAnnotation': return checks.mixed({input}); case 'AnyTypeAnnotation': case 'ExistentialTypeParam': return checks.any({input}); case 'NullableTypeAnnotation': return checks.nullable({input, type: annotation.typeAnnotation, scope}); case 'VoidTypeAnnotation': return checks.void({input}); } } function staticCheckAnnotation (path: NodePath, annotation: TypeAnnotation): ?boolean { const other = getAnnotation(path); switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': return staticCheckAnnotation(path, annotation.typeAnnotation); case 'GenericTypeAnnotation': if (isTypeChecker(annotation.id, path.scope)) { return staticChecks.type({path, type: annotation.id}); } else if (isPolymorphicType(annotation.id, path.scope)) { return; } else if (annotation.id.name === 'Symbol') { return staticChecks.symbol(path); } else { return staticChecks.instanceof({path, type: createTypeExpression(annotation.id)}); } } return compareAnnotations(annotation, other); } /** * Get the type annotation for a given node. */ function getAnnotation (path: NodePath): TypeAnnotation { let annotation; try { annotation = getAnnotationShallow(path); } catch (e) {} while (annotation && annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } return annotation || t.anyTypeAnnotation(); } function getAnnotationShallow (path: NodePath): ?TypeAnnotation { const {node, scope} = path; if (node.type === 'TypeAlias') { return node.right; } else if (!node.typeAnnotation && !node.savedTypeAnnotation && !node.returnType) { switch (path.type) { case 'Identifier': const id = scope.getBindingIdentifier(node.name); if (!id) { break; } if (id.savedTypeAnnotation) { return id.savedTypeAnnotation; } else if (id.returnType) { return id.returnType; } else if (id.typeAnnotation) { return id.typeAnnotation; } else if (isPolymorphicType(id, scope)) { return t.anyTypeAnnotation(); } return path.getTypeAnnotation(); case 'StringLiteral': case 'NumericLiteral': case 'BooleanLiteral': return createLiteralTypeAnnotation(path); case 'CallExpression': const callee = path.get('callee'); if (callee.type === 'Identifier') { if (callee.name === 'Symbol') { return t.genericTypeAnnotation('Symbol'); } const fn = getFunctionForIdentifier(callee); if (fn) { return getAnnotation(fn); } } break; case 'AssignmentExpression': return getAssignmentExpressionAnnotation(path); case 'MemberExpression': return getMemberExpressionAnnotation(path); case 'ArrayExpression': return getArrayExpressionAnnotation(path); case 'ObjectExpression': return getObjectExpressionAnnotation(path); case 'BinaryExpression': return getBinaryExpressionAnnotation(path); case 'BinaryExpression': return getBinaryExpressionAnnotation(path); case 'LogicalExpression': return getLogicalExpressionAnnotation(path); case 'ConditionalExpression': return getConditionalExpressionAnnotation(path); default: return path.getTypeAnnotation(); } } return node.savedTypeAnnotation || node.returnType || node.typeAnnotation || path.getTypeAnnotation(); } function createLiteralTypeAnnotation (path: NodePath): ?TypeAnnotation { let annotation; if (path.isStringLiteral()) { annotation = t.stringLiteralTypeAnnotation(); } else if (path.isNumericLiteral()) { annotation = t.numericLiteralTypeAnnotation(); } else if (path.isBooleanLiteral()) { annotation = t.booleanLiteralTypeAnnotation(); } else { return path.getTypeAnnotation(); } annotation.value = path.node.value; return annotation; } function getObjectMethodAnnotation (path: NodePath): ?TypeAnnotation { const {node} = path; return t.functionTypeAnnotation( null, node.params.map(param => param.savedTypeAnnotation || param.typeAnnotation), null, node.savedTypeAnnotation || node.returnType || node.typeAnnotation || t.anyTypeAnnotation() ); } function getAssignmentExpressionAnnotation (path: NodePath): ?TypeAnnotation { if (path.node.operator === '=') { return getAnnotation(path.get('right')); } } function getBinaryExpressionAnnotation (path: NodePath): TypeAnnotation { const {node} = path; if (isBooleanExpression(node)) { return t.booleanTypeAnnotation(); } else { return t.anyTypeAnnotation(); } } function getLogicalExpressionAnnotation (path: NodePath): TypeAnnotation { const {node} = path; if (isBooleanExpression(node)) { return t.booleanTypeAnnotation(); } else { let left = path.get('left'); let right = path.get('right'); switch (node.operator) { case '&&': case '||': ([left, right] = [getAnnotation(left), getAnnotation(right)]); if (t.isUnionTypeAnnotation(left)) { if (t.isUnionTypeAnnotation(right)) { return t.unionTypeAnnotation(left.types.concat(right.types)); } else { return t.unionTypeAnnotation(left.types.concat(right)); } } else { return t.unionTypeAnnotation([left, right]); } } return t.anyTypeAnnotation(); } } function getConditionalExpressionAnnotation (path: NodePath): TypeAnnotation { const {node} = path; const consequent = getAnnotation(path.get('consequent')); const alternate = getAnnotation(path.get('alternate')); if (t.isUnionTypeAnnotation(consequent)) { if (t.isUnionTypeAnnotation(alternate)) { return t.unionTypeAnnotation(consequent.types.concat(alternate.types)); } else { return t.unionTypeAnnotation(consequent.types.concat(alternate)); } } else { return t.unionTypeAnnotation([consequent, alternate]); } } function getArrayExpressionAnnotation (path: NodePath): TypeAnnotation { return t.genericTypeAnnotation( t.identifier('Array'), t.typeParameters(path.get('elements').map(getAnnotation)) ); } function getObjectExpressionAnnotation (path: NodePath): TypeAnnotation { const annotation = t.objectTypeAnnotation( path.get('properties').map(property => { if (property.computed) { return; } switch (property.type) { case 'ObjectMethod': return t.objectTypeProperty( t.identifier(property.node.key.name), getObjectMethodAnnotation(property) ); case 'ObjectProperty': return t.objectTypeProperty( t.identifier(property.node.key.name), property.node.value.savedTypeAnnotation || property.node.value.typeAnnotation || t.anyTypeAnnotation() ); } }).filter(identity) ); return annotation; } function getMemberExpressionAnnotation (path: NodePath): TypeAnnotation { if (path.node.computed) { return getComputedMemberExpressionAnnotation(path); } const object = path.get('object'); const {node: id} = path.get('property'); const {name} = id; let annotation = getAnnotation(object); if (annotation.type === 'NullableTypeAnnotation') { annotation = annotation.typeAnnotation; } if (annotation.type === 'GenericTypeAnnotation') { const target = path.scope.getBinding(annotation.id.name); if (target) { annotation = getAnnotation(target.path); } } switch (annotation.type) { case 'ObjectTypeAnnotation': for (let {key, value} of annotation.properties) { if (key.name === id.name) { return value; } } break; } return path.getTypeAnnotation(); } function getComputedMemberExpressionAnnotation (path: NodePath): TypeAnnotation { const object = path.get('object'); const property = path.get('property'); let objectAnnotation = getAnnotation(object); if (objectAnnotation.type === 'TypeAnnotation' || objectAnnotation.type === 'NullableTypeAnnotation') { objectAnnotation = objectAnnotation.typeAnnotation; } let propertyAnnotation = getAnnotation(property); if (propertyAnnotation.type === 'TypeAnnotation' || propertyAnnotation.type === 'NullableTypeAnnotation') { propertyAnnotation = propertyAnnotation.typeAnnotation; } const {confident, value} = property.evaluate(); if (!confident) { return path.getTypeAnnotation(); } switch (objectAnnotation.type) { case 'TupleTypeAnnotation': if (objectAnnotation.types.length === 0) { break; } else if (typeof value === 'number') { if (!objectAnnotation.types[value]) { throw path.buildCodeFrameError(`Invalid computed member expression for tuple: ` + humanReadableType(objectAnnotation, path.scope)); } return objectAnnotation.types[value]; } else { throw path.buildCodeFrameError(`Invalid computed member expression for tuple: ` + humanReadableType(objectAnnotation, path.scope)); } break; } return path.getTypeAnnotation(); } function getFunctionForIdentifier (path: NodePath): boolean|Node { if (path.type !== 'Identifier') { return false; } const ref = path.scope.getBinding(path.node.name); if (!ref) { return false; } return t.isFunction(ref.path.parent) && ref.path.parentPath; } /** * Returns `true` if the annotation is definitely for an array, * otherwise `false`. */ function isStrictlyArrayAnnotation (annotation: TypeAnnotation): boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': return isStrictlyArrayAnnotation(annotation.typeAnnotation); case 'GenericTypeAnnotation': return annotation.id.name === 'Array'; case 'UnionTypeAnnotation': return annotation.types.every(isStrictlyArrayAnnotation); default: return false; } } function compareMaybeUnion (annotation: TypeAnnotation, comparator: (node: TypeAnnotation) => ?boolean): ?boolean { let falseCount = 0; for (let type of annotation.types) { const result = comparator(type); if (result === true) { return true; } else if (result === false) { falseCount++; } } if (falseCount === annotation.types.length) { return false; } else { return null; } } /** * Returns `true` if the annotation is compatible with a number, * `false` if it definitely isn't, or `null` if we're not sure. */ function maybeNumberAnnotation (annotation: TypeAnnotation): ?boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return maybeNumberAnnotation(annotation.typeAnnotation); case 'NumberTypeAnnotation': case 'NumericLiteralTypeAnnotation': return true; case 'GenericTypeAnnotation': switch (annotation.id.name) { case 'Array': case 'Function': case 'Object': case 'String': case 'Boolean': case 'Date': case 'RegExp': return false; default: return null; } case 'UnionTypeAnnotation': return compareMaybeUnion(annotation, maybeNumberAnnotation); case 'AnyTypeAnnotation': case 'MixedTypeAnnotation': case 'IntersectionTypeAnnotation': return null; default: return false; } } /** * Returns `true` if the annotation is compatible with a string, * `false` if it definitely isn't, or `null` if we're not sure. */ function maybeStringAnnotation (annotation: TypeAnnotation): ?boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return maybeStringAnnotation(annotation.typeAnnotation); case 'StringTypeAnnotation': return true; case 'StringLiteralTypeAnnotation': return null; case 'GenericTypeAnnotation': switch (annotation.id.name) { case 'Array': case 'Function': case 'Object': case 'Number': case 'Boolean': case 'Date': case 'RegExp': return false; default: return null; } case 'UnionTypeAnnotation': let falseCount = 0; for (let type of annotation.types) { const result = maybeStringAnnotation(type); if (result === true) { return true; } else if (result === false) { falseCount++; } } if (falseCount === annotation.types.length) { return false; } else { return null; } case 'AnyTypeAnnotation': case 'MixedTypeAnnotation': case 'IntersectionTypeAnnotation': return null; default: return false; } } /** * Returns `true` if the annotation is compatible with a symbol, * `false` if it definitely isn't, or `null` if we're not sure. */ function maybeSymbolAnnotation (annotation: TypeAnnotation): ?boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return maybeSymbolAnnotation(annotation.typeAnnotation); case 'GenericTypeAnnotation': switch (annotation.id.name) { case 'Array': case 'Function': case 'Object': case 'Number': case 'Boolean': case 'Date': case 'RegExp': return false; case 'Symbol': return true; default: return null; } case 'UnionTypeAnnotation': let falseCount = 0; for (let type of annotation.types) { const result = maybeSymbolAnnotation(type); if (result === true) { return true; } else if (result === false) { falseCount++; } } if (falseCount === annotation.types.length) { return false; } else { return null; } case 'AnyTypeAnnotation': case 'MixedTypeAnnotation': case 'IntersectionTypeAnnotation': return null; default: return false; } } /** * Returns `true` if the annotation is compatible with a boolean, * `false` if it definitely isn't, or `null` if we're not sure. */ function maybeBooleanAnnotation (annotation: TypeAnnotation): ?boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return maybeBooleanAnnotation(annotation.typeAnnotation); case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': return true; case 'GenericTypeAnnotation': switch (annotation.id.name) { case 'Array': case 'Function': case 'Object': case 'String': case 'Number': case 'Date': case 'RegExp': return false; default: return null; } case 'UnionTypeAnnotation': let falseCount = 0; for (let type of annotation.types) { const result = maybeBooleanAnnotation(type); if (result === true) { return true; } else if (result === false) { falseCount++; } } if (falseCount === annotation.types.length) { return false; } else { return null; } case 'AnyTypeAnnotation': case 'MixedTypeAnnotation': case 'IntersectionTypeAnnotation': return null; default: return false; } } /** * Returns `true` if the annotation is compatible with a function, * `false` if it definitely isn't, or `null` if we're not sure. */ function maybeFunctionAnnotation (annotation: TypeAnnotation): ?boolean { switch (annotation.type) { case 'TypeAnnotation': case 'FunctonTypeParam': case 'NullableTypeAnnotation': return maybeFunctionAnnotation(annotation.typeAnnotation); case 'FunctionTypeAnnotation': return true; case 'GenericTypeAnnotation': switch (annotation.id.name) { case 'Array': case 'Number': case 'Object': case 'String': case 'Boolean': case 'Date': case 'RegExp': return false; default: return null; } case 'UnionTypeAnnotation': let falseCount = 0; for (let type of annotation.types) { const result = maybeFunctionAnnotation(type); if (result === true) { return true; } else if (result === false) { falseCount++; } } if (falseCount === annotation.types.length) { return false; } else { return null; } case 'AnyTypeAnnotation': case 'MixedTypeAnnotation':