UNPKG

babel-plugin-typecheck

Version:

Transforms flow type annotations into runtime type checks.

1,563 lines (1,452 loc) 106 kB
import generate from "babel-generator"; type Node = { type: string; }; type Literal = { type: 'StringLiteral' | 'BooleanLiteral' | 'NumericLiteral' | 'NullLiteral' | 'RegExpLiteral' }; type Identifier = { type: string; name: string; }; type QualifiedTypeIdentifier = { id: Identifier; qualification: Identifier|QualifiedTypeIdentifier; }; type TypeAnnotation = { type: string; }; type VisitorContext = { inspect: Identifier; }; 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 { /** * 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 checkIsClass: (() => Node) = expression(`typeof input === 'function' && input.prototype && input.prototype.constructor === input`); const checkIsGenerator: (() => Node) = expression(`typeof input === 'function' && input.generator`); const checkIsIterable: (() => Node) = expression(`input && (typeof input[Symbol.iterator] === 'function' || Array.isArray(input))`); 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 () { function id (input) { return check; }; Object.defineProperty(id, Symbol.hasInstance, { value: function (input) { return id(input); } }); return id; })(); `); const guard: (() => Node) = template(` if (!check) { throw new TypeError(message); } `); const thrower: (() => Node) = template(` if (check) { ret; } else { throw new TypeError(message); } `); const guardInline: (() => Node) = expression(` (id => { if (!check) { throw new TypeError(message); } return id; })(input) `); const guardFn: (() => Node) = expression(` function name (id) { if (!check) { throw new TypeError(message); } return id; } `); const readableName: (() => Node) = expression(` inspect(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 checkObjectIndexers: (() => Node) = expression(` Object.keys(input).every(key => { const value = input[key]; if (~fixedKeys.indexOf(key)) { return true; } else { return check; } }); `); const checkObjectIndexersNoFixed: (() => Node) = expression(` Object.keys(input).every(key => { const value = input[key]; return check; }); `); const propType: (() => Node) = expression(` (function(props, name, component) { var prop = props[name]; if(!check) { return new Error( "Invalid prop \`" + name + "\` supplied to \`" + component + "\`.\\n\\nExpected:\\n" + expected + "\\n\\nGot:\\n" + got + "\\n\\n" ); } }) `); const PRAGMA_IGNORE_STATEMENT = /typecheck:\s*ignore\s+statement/i; const PRAGMA_IGNORE_FILE = /typecheck:\s*ignore\s+file/i; function skipEnvironment(comments, opts) { if (!opts.only) { return false; } const envs = pragmaEnvironments(comments); return !opts.only.some(env => envs[env]); } function pragmaEnvironments(comments) { const pragma = /@typecheck:\s*(.+)/; const environments = {}; comments.forEach(comment => { const m = comment.value.match(pragma); if (m) { m[1].split(',').forEach(env => environments[env.trim()] = true); } }) return environments; } 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') { const declaration = path.get('declaration'); declaration.replaceWith(createTypeAliasChecks(declaration)); node.exportKind = 'value'; } }, 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 ]); }, ArrowFunctionExpression (path: NodePath) { // Look for destructuring args with annotations. const params: NodePath[] = path.get('params'); for (let param of params) { if (param.isObjectPattern() && param.node.typeAnnotation) { const {scope} = path.get('body'); const id = scope.generateUidIdentifier(`arg${param.key}`); const pattern = param.node; param.replaceWith(id); if (path.node.expression) { const block = t.blockStatement([ t.variableDeclaration('var', [ t.variableDeclarator(pattern, id) ]), t.returnStatement(path.get('body').node) ]); path.node.body = block; path.node.expression = false; } else { path.get('body.body')[0].insertBefore(t.variableDeclaration('var', [ t.variableDeclarator(pattern, id) ])); } } } }, Function: { enter (path: NodePath, context: VisitorContext): void { if (maybeSkip(path)) { return; } const {node, scope} = path; const paramChecks = collectParamChecks(path, context); if (node.type === "ArrowFunctionExpression" && node.expression) { node.expression = false; node.body = t.blockStatement([t.returnStatement(node.body)]); } if (node.returnType) { createFunctionReturnGuard(path, context); createFunctionYieldGuards(path, context); } node.body.body.unshift(...paramChecks); node.savedTypeAnnotation = node.returnType; node.returnCount = 0; node.yieldCount = 0; }, exit (path: NodePath): void { const {node, scope} = path; const isVoid = node.savedTypeAnnotation ? maybeNullableAnnotation(node.savedTypeAnnotation) : null; if (!node.returnCount && isVoid === false) { let annotation = node.savedTypeAnnotation; if (annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (node.generator && isGeneratorAnnotation(annotation) && annotation.typeParameters && annotation.typeParameters.params.length > 1) { annotation = annotation.typeParameters.params[1]; } throw path.buildCodeFrameError( buildErrorMessage( `Function ${node.id ? `"${node.id.name}" ` : ''}did not return a value.`, annotation ) ); } if (node.nextGuardCount) { path.get('body').get('body')[0].insertBefore(node.nextGuard); } if (node.yieldGuardCount) { path.get('body').get('body')[0].insertBefore(node.yieldGuard); } if (node.returnGuardCount) { path.get('body').get('body')[0].insertBefore(node.returnGuard); } } }, YieldExpression (path: NodePath, context: VisitorContext): void { const fn = path.getFunctionParent(); if (!fn) { return; } fn.node.yieldCount++; if (!isGeneratorAnnotation(fn.node.returnType) || maybeSkip(path)) { return; } const {node, parent, scope} = path; let annotation = fn.node.returnType; if (annotation.type === 'NullableTypeAnnotation' || annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (!annotation.typeParameters || annotation.typeParameters.params.length === 0) { return; } const yieldType = annotation.typeParameters.params[0]; const nextType = annotation.typeParameters.params[2]; const ok = staticCheckAnnotation(path.get("argument"), yieldType); if (ok === true && !nextType) { return; } else if (ok === false) { throw path.buildCodeFrameError( buildErrorMessage( `Function ${fn.node.id ? `"${fn.node.id.name}" ` : ''}yielded an invalid type.`, yieldType, getAnnotation(path.get('argument')) ) ); } fn.node.yieldGuardCount++; if (fn.node.yieldGuard) { const yielder = t.yieldExpression( t.callExpression(fn.node.yieldGuardName, [node.argument || t.identifier('undefined')]) ); yielder.hasBeenTypeChecked = true; if (fn.node.nextGuard) { fn.node.nextGuardCount++; path.replaceWith(t.callExpression(fn.node.nextGuardName, [yielder])); } else { path.replaceWith(yielder); } } else if (fn.node.nextGuard) { fn.node.nextGuardCount++; path.replaceWith(t.callExpression(fn.node.nextGuardName, [yielder])); } }, ReturnStatement (path: NodePath, context: VisitorContext): void { const fn = path.getFunctionParent(); if (!fn) { return; } fn.node.returnCount++; if (maybeSkip(path)) { return; } const {node, parent, scope} = path; const {returnType, returnGuardName} = fn.node; if (!returnType || !returnGuardName) { return; } if (!node.argument) { if (maybeNullableAnnotation(returnType) === false) { throw path.buildCodeFrameError( buildErrorMessage( `Function ${fn.node.id ? `"${fn.node.id.name}" ` : ''}did not return a value.`, returnType ) ); } return; } let annotation = returnType; if (annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (isGeneratorAnnotation(annotation)) { annotation = annotation.typeParameters && annotation.typeParameters.params.length > 1 ? annotation.typeParameters.params[1] : t.anyTypeAnnotation(); } else if (node.async && annotation.type === 'GenericTypeAnnotation' && annotation.id.name === 'Promise') { annotation = (annotation.typeParameters && annotation.typeParameters[0]) || t.anyTypeAnnotation(); } const ok = staticCheckAnnotation(path.get("argument"), annotation); if (ok === true) { return; } else if (ok === false) { throw path.buildCodeFrameError( buildErrorMessage( `Function ${fn.node.id ? `"${fn.node.id.name}" ` : ''}returned an invalid type.`, annotation, getAnnotation(path.get('argument')) ) ); } fn.node.returnGuardCount++; const returner = t.returnStatement(t.callExpression(fn.node.returnGuardName, [node.argument])); returner.hasBeenTypeChecked = true; path.replaceWith(returner); }, VariableDeclaration (path: NodePath, context: VisitorContext): 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( buildErrorMessage( `Invalid assignment value for "${id.name}".`, id.typeAnnotation, getAnnotation(declarations[i]) ) ); } const check = checkAnnotation(id, id.typeAnnotation, scope); if (check) { collected.push(guard({ check, message: varTypeErrorMessage(id, context) })); } } 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.parentPath.isForXStatement() || path.parentPath.isForStatement() || path.parentPath.isForInStatement()) { let body = path.parentPath.get('body'); if (body.type !== 'BlockStatement') { const block = t.blockStatement([body.node]); body.replaceWith(block); body = path.parentPath.get('body'); } const children = body.get('body'); if (children.length === 0) { body.replaceWith(check); } else { children[0].insertBefore(check); } } else if (path.parent.type === 'ExportNamedDeclaration' || path.parent.type === 'ExportDefaultDeclaration' || path.parent.type === 'ExportAllDeclaration') { path.parentPath.insertAfter(check); } else { path.replaceWith(t.blockStatement([node, check])); } } }, AssignmentExpression (path: NodePath, context: VisitorContext): void { if (maybeSkip(path)) { return; } const {node, scope} = path; const left = path.get('left'); let annotation; if (node.hasBeenTypeChecked || node.left.hasBeenTypeChecked) { return; } else if (left.isMemberExpression()) { annotation = getAnnotation(left); } else if (t.isIdentifier(node.left)) { const binding = scope.getBinding(node.left.name); if (!binding) { return; } else if (binding.path.type !== 'VariableDeclarator') { return; } annotation = left.getTypeAnnotation(); if (annotation.type === 'AnyTypeAnnotation') { const item = binding.path.get('id'); annotation = item.node.savedTypeAnnotation || item.getTypeAnnotation(); } } else { return; } node.hasBeenTypeChecked = true; node.left.hasBeenTypeChecked = true; const id = node.left; const right = path.get('right'); if (annotation.type === 'AnyTypeAnnotation') { return; } const ok = staticCheckAnnotation(right, annotation); if (ok === true) { return; } else if (ok === false) { throw path.buildCodeFrameError( buildErrorMessage( `Invalid assignment value for "${humanReadableType(id)}".`, annotation, getAnnotation(right) ) ); } const check = checkAnnotation(id, annotation, scope); if (!id.typeAnnotation) { id.typeAnnotation = annotation; } id.hasBeenTypeChecked = true; if (check) { const parent = path.getStatementParent(); parent.insertAfter(guard({ check, message: varTypeErrorMessage(id, context) })); } }, 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(); }, ForOfStatement (path: NodePath, context: VisitorContext): void { if (maybeSkip(path)) { return; } const left: NodePath = path.get('left'); const right: NodePath = path.get('right'); const rightAnnotation: TypeAnnotation = getAnnotation(right); const leftAnnotation: TypeAnnotation = left.isVariableDeclaration() ? getAnnotation(left.get('declarations')[0].get('id')) : getAnnotation(left); if (rightAnnotation.type !== 'VoidTypeAnnotation' && rightAnnotation.type !== 'NullLiteralTypeAnnotation') { const ok: ?boolean = maybeIterableAnnotation(rightAnnotation); if (ok === false) { throw path.buildCodeFrameError(`Cannot iterate ${humanReadableType(rightAnnotation)}.`); } } let id: ?Identifier; if (right.isIdentifier()) { id = right.node; } else { id = path.scope.generateUidIdentifierBasedOnNode(right.node); path.scope.push({id}); const replacement: Node = t.expressionStatement(t.assignmentExpression('=', id, right.node)); path.insertBefore(replacement); right.replaceWith(id); } path.insertBefore(guard({ check: checks.iterable({input: id}), message: t.binaryExpression( '+', t.stringLiteral(`Expected ${humanReadableType(right.node)} to be iterable, got `), readableName({inspect: context.inspect, input: id}) ) })); if (rightAnnotation.type !== 'GenericTypeAnnotation' || rightAnnotation.id.name !== 'Iterable' || !rightAnnotation.typeParameters || !rightAnnotation.typeParameters.params.length) { return; } const annotation: TypeAnnotation = rightAnnotation.typeParameters.params[0]; if (compareAnnotations(annotation, leftAnnotation) === false) { throw path.buildCodeFrameError( buildErrorMessage( `Invalid iterator type.`, annotation, leftAnnotation ) ); } }, ClassDeclaration (path: NodePath, context: VisitorContext) { // Convert React props to propTypes if (!path.node.superClass) { return; } let props: ?NodePath; let hasRenderMethod = false; for (let memberPath of path.get('body.body')) { const classMember = memberPath.node; if (t.isClassProperty(classMember)) { if (classMember.key.name === 'propTypes' && classMember.static) { return; } else if (classMember.key.name === 'props' && !classMember.static) { props = memberPath; } } if (t.isClassMethod(classMember) && classMember.key.name === 'render') { hasRenderMethod = true; } } let type: ?Node; if (path.node.superTypeParameters) { if (path.node.superTypeParameters.params.length !== 3) { return; } type = path.node.superTypeParameters.params[1]; } if (props) { type = props.node.typeAnnotation.typeAnnotation; } if (!type || !hasRenderMethod) { return; } if (t.isGenericTypeAnnotation(type)) { const binding = path.scope.getBinding(type.id.name); type = getAnnotation(binding.path); } if (!t.isObjectTypeAnnotation(type)) { return; } // Now we have a class that has a superclass, an instance method called 'render' // and some property type annotations. We can be reasonably sure it's a React component. const propTypes = t.objectExpression( type.properties.map( prop => t.objectProperty( t.identifier(prop.key.name), generatePropType(prop.value, path.scope, context) ) ) ); if (path.node.decorators) { const property = t.classProperty(t.identifier('propTypes'), propTypes); property.static = true; props.insertAfter(property); } else { const root:NodePath = path.parentPath.isExportDeclaration() ? path.parentPath : path; root.insertAfter( t.expressionStatement( t.assignmentExpression( "=", t.memberExpression(path.node.id, t.identifier("propTypes")), propTypes ) ) ); } } }; /** * Collect all the type declarations in the given path and add references to them for retreival later. */ function collectTypes (path: NodePath): void { path.traverse({ InterfaceDeclaration (path: NodePath) { path.scope.setData(`typechecker:${path.node.id.name}`, path); }, TypeAlias (path: NodePath) { path.scope.setData(`typechecker:${path.node.id.name}`, path); }, ImportDeclaration (path: NodePath) { if (path.node.importKind !== 'type') { return; } path.get('specifiers') .forEach(specifier => { const local = specifier.get('local'); if (local.isIdentifier()) { path.scope.setData(`typechecker:${local.node.name}`, specifier); } else { path.scope.setData(`typechecker:${local.node.id.name}`, specifier); } }); }, "Function|Class" (path: NodePath) { const node = path.node; if (node.typeParameters && node.typeParameters.params) { path.get('typeParameters').get('params').forEach(typeParam => { path.get('body').scope.setData(`typeparam:${typeParam.node.name}`, typeParam); }); } } }); } return { visitor: { Program (path: NodePath, {opts}) { if (opts && opts.disable && opts.disable[process.env.NODE_ENV]) { return; } let checkFile = false; for (let child of path.get('body')) { if (mustCheckFile(child, opts)) { checkFile = true; break; } } if (!checkFile) { for (let child of path.get('body')) { if (maybeSkipFile(child, opts)) { return; } } } collectTypes(path); const inspect = path.scope.generateUidIdentifier('inspect'); const requiresHelpers = { inspect: false }; const context = { get inspect () { requiresHelpers.inspect = true; return inspect; } }; path.traverse(visitors, context); if (requiresHelpers.inspect) { const body = path.get('body'); body[body.length - 1].insertAfter(template(` function id (input, depth) { const maxDepth = 4; const maxKeys = 15; if (depth === undefined) { depth = 0; } depth += 1; if (input === null) { return 'null'; } else if (input === undefined) { return 'void'; } else if (typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean') { return typeof input; } else if (Array.isArray(input)) { if (input.length > 0) { if (depth > maxDepth) return '[...]'; const first = id(input[0], depth); if (input.every(item => id(item, depth) === first)) { return first.trim() + '[]'; } else { return '[' + input.slice(0, maxKeys).map(item => id(item, depth)).join(', ') + (input.length >= maxKeys ? ', ...' : '') + ']'; } } else { return 'Array'; } } else { const keys = Object.keys(input); if (!keys.length) { if (input.constructor && input.constructor.name && input.constructor.name !== 'Object') { return input.constructor.name; } else { return 'Object'; } } if (depth > maxDepth) return '{...}'; const indent = ' '.repeat(depth - 1); let entries = keys.slice(0, maxKeys).map(key => { return (/^([A-Z_$][A-Z0-9_$]*)$/i.test(key) ? key : JSON.stringify(key)) + ': ' + id(input[key], depth) + ';'; }).join('\\n ' + indent); if (keys.length >= maxKeys) { entries += '\\n ' + indent + '...'; } if (input.constructor && input.constructor.name && input.constructor.name !== 'Object') { return input.constructor.name + ' {\\n ' + indent + entries + '\\n' + indent + '}'; } else { return '{\\n ' + indent + entries + '\\n' + indent + '}'; } } } `)({id: inspect})); } } } } /** * Create a function which can verify the return type for a function. */ function createFunctionReturnGuard (path: NodePath, context: VisitorContext): void { const {node, scope} = path; let annotation = node.returnType; if (annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (isGeneratorAnnotation(annotation)) { annotation = annotation.typeParameters && annotation.typeParameters.params.length > 1 ? annotation.typeParameters.params[1] : t.anyTypeAnnotation(); } else if (node.async && annotation.type === 'GenericTypeAnnotation' && annotation.id.name === 'Promise') { annotation = (annotation.typeParameters && annotation.typeParameters[0]) || t.anyTypeAnnotation(); } const name = scope.generateUidIdentifierBasedOnNode(node); const id = scope.generateUidIdentifier('id'); const check = checkAnnotation(id, annotation, scope); if (check) { node.returnGuard = guardFn({ id, name, check, message: returnTypeErrorMessage(path, path.node, id, context) }); node.returnGuard.hasBeenTypeChecked = true; node.returnGuardName = name; node.returnGuardCount = 0; } } function createFunctionYieldGuards (path: NodePath, context: VisitorContext) { const {node, scope} = path; let annotation = node.returnType; if (annotation.type === 'NullableTypeAnnotation' || annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (!annotation.typeParameters || annotation.typeParameters.params.length === 0) { return; } if (annotation.type === 'TypeAnnotation') { annotation = annotation.typeAnnotation; } if (!isGeneratorAnnotation(annotation)) { return; } const yieldType = annotation.typeParameters.params[0]; const nextType = annotation.typeParameters.params[2]; if (yieldType) { const name = scope.generateUidIdentifier(`check${node.id ? node.id.name.slice(0, 1).toUpperCase() + node.id.name.slice(1) : ''}Yield`); const id = scope.generateUidIdentifier('id'); const check = checkAnnotation(id, yieldType, scope); if (check) { node.yieldGuard = guardFn({ id, name, check, message: yieldTypeErrorMessage(node, yieldType, id, context) }); node.yieldGuardName = name; node.yieldGuardCount = 0; } } if (nextType) { const name = scope.generateUidIdentifier(`check${node.id ? node.id.name.slice(0, 1).toUpperCase() + node.id.name.slice(1) : ''}Next`); const id = scope.generateUidIdentifier('id'); const check = checkAnnotation(id, nextType, scope); if (check) { node.nextGuard = guardFn({ id, name, check, message: yieldNextTypeErrorMessage(node, nextType, id, context) }); node.nextGuardName = name; node.nextGuardCount = 0; } } } function isThisMemberExpression (path: NodePath): boolean { const {node} = path; if (node.type === 'ThisExpression') { return true; } else if (node.type === 'MemberExpression') { return isThisMemberExpression(path.get('object')); } else { return false; } } function isGeneratorAnnotation (annotation: ?TypeAnnotation): boolean { if (!annotation) { return false; } if (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation') { annotation = annotation.typeAnnotation; } return annotation.type === 'GenericTypeAnnotation' && annotation.id.name === 'Generator'; } function buildErrorMessage (message: string, expected: TypeAnnotation, got: ?Node) { if (got) { return message + '\n\nExpected:\n' + humanReadableType(expected) + '\n\nGot:\n' + humanReadableType(got); } else { return message + '\n\nExpected:\n' + humanReadableType(expected); } } function createChecks (): Object { return { number: expression(`typeof input === 'number'`), numericLiteral: checkNumericLiteral, boolean: expression(`typeof input === 'boolean'`), booleanLiteral: checkBooleanLiteral, class: checkClass, 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, generator: checkGenerator, iterable: checkIterable, tuple: checkTuple, object: checkObject, nullable: checkNullable, typeof: checkTypeof, int8: expression(`typeof input === 'number' && !isNaN(input) && input >= -128 && input <= 127 && input === Math.floor(input)`), uint8: expression(`typeof input === 'number' && !isNaN(input) && input >= 0 && input <= 255 && input === Math.floor(input)`), int16: expression(`typeof input === 'number' && !isNaN(input) && input >= -32768 && input <= 32767 && input === Math.floor(input)`), uint16: expression(`typeof input === 'number' && !isNaN(input) && input >= 0 && input <= 65535 && input === Math.floor(input)`), int32: expression(`typeof input === 'number' && !isNaN(input) && input >= -2147483648 && input <= 2147483647 && input === Math.floor(input)`), uint32: expression(`typeof input === 'number' && !isNaN(input) && input >= 0 && input <= 4294967295 && input === Math.floor(input)`), float32: expression(`typeof input === 'number' && !isNaN(input) && input >= -3.40282347e+38 && input <= 3.40282347e+38`), float64: expression(`typeof input === 'number' && !isNaN(input)`), double: expression(`typeof input === 'number' && !isNaN(input)`) }; } function createStaticChecks (): Object { return { symbol (path: NodePath): ?boolean { return maybeSymbolAnnotation(getAnnotation(path)); }, instanceof ({path, annotation}): ?boolean { const type = createTypeExpression(annotation.id); const {node, scope} = path; if (type.name === 'Object' && node.type === 'ObjectExpression' && !scope.getBinding('Object')) { return true; } else if (type.name === 'Map' && !scope.getBinding('Map')) { return null; } else if (type.name === 'Set' && !scope.getBinding('Set')) { return null; } else if (type.name === 'Class' && !scope.hasBinding('Class')) { return null; } else if (type.name === 'int8' && !scope.hasBinding('int8')) { return null; } else if (type.name === 'uint8' && !scope.hasBinding('uint8')) { return null; } else if (type.name === 'int16' && !scope.hasBinding('int16')) { return null; } else if (type.name === 'uint16' && !scope.hasBinding('uint16')) { return null; } else if (type.name === 'int32' && !scope.hasBinding('int32')) { return null; } else if (type.name === 'uint32' && !scope.hasBinding('uint32')) { return null; } else if (type.name === 'float32' && !scope.hasBinding('float32')) { return null; } else if (type.name === 'float64' && !scope.hasBinding('float64')) { return null; } else if (type.name === 'double' && !scope.hasBinding('double')) { return null; } return maybeInstanceOfAnnotation(getAnnotation(path), type, annotation.typeParameters ? annotation.typeParameters.params : []); }, 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 'StringLiteral': case 'StringLiteralTypeAnnotation': return compareStringLiteralAnnotations(a, b); case 'NumberTypeAnnotation': return maybeNumberAnnotation(b); case 'NumericLiteral': case 'NumericLiteralTypeAnnotation': return compareNumericLiteralAnnotations(a, b); case 'BooleanTypeAnnotation': return maybeBooleanAnnotation(b); case 'BooleanLiteral': 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' || b.type === 'StringLiteral') { return a.value === b.value; } else { return maybeStringAnnotation(b) === false ? false : null; } } function compareBooleanLiteralAnnotations (a: BooleanLiteralTypeAnnotation, b: TypeAnnotation): ?boolean { if (b.type === 'BooleanLiteralTypeAnnotation' || b.type === 'BooleanLiteral') { return a.value === b.value; } else { return maybeBooleanAnnotation(b) === false ? false : null; } } function compareNumericLiteralAnnotations (a: NumericLiteralTypeAnnotation, b: TypeAnnotation): ?boolean { if (b.type === 'NumericLiteralTypeAnnotation' || b.type === 'NumericLiteral') { return a.value === b.value; } else { return maybeNumberAnnotation(b) === false ? false : null; } } function unionComparer (a: TypeAnnotation, b: TypeAnnotation, comparator: (a:TypeAnnotation, b:TypeAnnotation) => ?boolean): ?boolean { if (!a.types || a.types.length === 0) { return null; } 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 'ObjectTypeAnnotation': break; case 'TypeAnnotation': case 'FunctionTypeParam': case 'NullableTypeAnnotation': return compareObjectAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareObjectAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareObjectAnnotation); case 'VoidTypeAnnotation': case 'NullLiteralTypeAnnotation': case 'BooleanTypeAnnotation': case 'BooleanLiteralTypeAnnotation': case 'StringTypeAnnotation': case 'StringLiteralTypeAnnotation': case 'NumberTypeAnnotation': case 'NumericLiteralTypeAnnotation': case 'FunctionTypeAnnotation': return false; default: return null; } // We're comparing two object annotations. let allTrue = true; for (let aprop of a.properties) { let found = false; for (let bprop of b.properties) { if (bprop.key.name === aprop.key.name) { const result = compareAnnotations(aprop.value, bprop.value); if (result === false && !(aprop.optional && (bprop.optional || maybeNullableAnnotation(bprop.value) === true))) { return false; } else { found = result; } break; } } if (found === false && !aprop.optional) { return false; } allTrue = allTrue && found === true; } return allTrue ? true : null; } function compareArrayAnnotation (a: Node, b: Node): ?boolean { switch (b.type) { case 'TypeAnnotation': case 'FunctionTypeParam': case 'NullableTypeAnnotation': return compareArrayAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareArrayAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareArrayAnnotation); case 'VoidTypeAnnotation': case 'NullLiteralTypeAnnotation': 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 'FunctionTypeParam': 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 'FunctionTypeParam': case 'NullableTypeAnnotation': return compareTupleAnnotation(a, b.typeAnnotation); case 'UnionTypeAnnotation': return unionComparer(a, b, compareTupleAnnotation); case 'IntersectionTypeAnnotation': return intersectionComparer(a, b, compareTupleAnnotation); case 'VoidTypeAnnotation': case 'NullLiteralTypeAnnotation': 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 'FunctionTypeParam': return compareNullableAnnotation(a, b.typeAnnotation); case 'NullableTypeAnnotation': case 'VoidTypeAnnotation': case 'NullLiteralTypeAnnotation': 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 checkGenerator ({input, types, scope}): Node { return checkIsGenerator({input}); } function checkIterable ({input, types, scope}): Node { return checkIsIterable({input}); } function checkClass ({input, types, scope}): Node { return checkIsClass({input}); } 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(