|
| 1 | +/** |
| 2 | + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. |
| 3 | + * |
| 4 | + * This source code is licensed under the MIT license found in the |
| 5 | + * LICENSE file in the root directory of this source tree. |
| 6 | + * |
| 7 | + */ |
| 8 | + |
| 9 | +// Only used for types |
| 10 | +// eslint-disable-next-line |
| 11 | +import {NodePath, Visitor} from '@babel/traverse'; |
| 12 | +// eslint-disable-next-line |
| 13 | +import {Identifier} from '@babel/types'; |
| 14 | + |
| 15 | +const invariant = (condition: unknown, message: string) => { |
| 16 | + if (!condition) { |
| 17 | + throw new Error('babel-plugin-jest-hoist: ' + message); |
| 18 | + } |
| 19 | +}; |
| 20 | + |
| 21 | +// We allow `jest`, `expect`, `require`, all default Node.js globals and all |
| 22 | +// ES2015 built-ins to be used inside of a `jest.mock` factory. |
| 23 | +// We also allow variables prefixed with `mock` as an escape-hatch. |
| 24 | +const WHITELISTED_IDENTIFIERS: Set<string> = new Set([ |
| 25 | + 'Array', |
| 26 | + 'ArrayBuffer', |
| 27 | + 'Boolean', |
| 28 | + 'DataView', |
| 29 | + 'Date', |
| 30 | + 'Error', |
| 31 | + 'EvalError', |
| 32 | + 'Float32Array', |
| 33 | + 'Float64Array', |
| 34 | + 'Function', |
| 35 | + 'Generator', |
| 36 | + 'GeneratorFunction', |
| 37 | + 'Infinity', |
| 38 | + 'Int16Array', |
| 39 | + 'Int32Array', |
| 40 | + 'Int8Array', |
| 41 | + 'InternalError', |
| 42 | + 'Intl', |
| 43 | + 'JSON', |
| 44 | + 'Map', |
| 45 | + 'Math', |
| 46 | + 'NaN', |
| 47 | + 'Number', |
| 48 | + 'Object', |
| 49 | + 'Promise', |
| 50 | + 'Proxy', |
| 51 | + 'RangeError', |
| 52 | + 'ReferenceError', |
| 53 | + 'Reflect', |
| 54 | + 'RegExp', |
| 55 | + 'Set', |
| 56 | + 'String', |
| 57 | + 'Symbol', |
| 58 | + 'SyntaxError', |
| 59 | + 'TypeError', |
| 60 | + 'URIError', |
| 61 | + 'Uint16Array', |
| 62 | + 'Uint32Array', |
| 63 | + 'Uint8Array', |
| 64 | + 'Uint8ClampedArray', |
| 65 | + 'WeakMap', |
| 66 | + 'WeakSet', |
| 67 | + 'arguments', |
| 68 | + 'console', |
| 69 | + 'expect', |
| 70 | + 'isNaN', |
| 71 | + 'jest', |
| 72 | + 'parseFloat', |
| 73 | + 'parseInt', |
| 74 | + 'require', |
| 75 | + 'undefined', |
| 76 | +]); |
| 77 | +Object.keys(global).forEach(name => { |
| 78 | + WHITELISTED_IDENTIFIERS.add(name); |
| 79 | +}); |
| 80 | + |
| 81 | +const JEST_GLOBAL = {name: 'jest'}; |
| 82 | +// TODO: Should be Visitor<{ids: Set<NodePath<Identifier>>}>, but `ReferencedIdentifier` doesn't exist |
| 83 | +const IDVisitor = { |
| 84 | + ReferencedIdentifier(path: NodePath<Identifier>) { |
| 85 | + // @ts-ignore: passed as Visitor State |
| 86 | + this.ids.add(path); |
| 87 | + }, |
| 88 | + blacklist: ['TypeAnnotation', 'TSTypeAnnotation', 'TSTypeReference'], |
| 89 | +}; |
| 90 | + |
| 91 | +const FUNCTIONS: { |
| 92 | + [key: string]: (args: Array<NodePath>) => boolean; |
| 93 | +} = Object.create(null); |
| 94 | + |
| 95 | +FUNCTIONS.mock = (args: Array<NodePath>) => { |
| 96 | + if (args.length === 1) { |
| 97 | + return args[0].isStringLiteral() || args[0].isLiteral(); |
| 98 | + } else if (args.length === 2 || args.length === 3) { |
| 99 | + const moduleFactory = args[1]; |
| 100 | + invariant( |
| 101 | + moduleFactory.isFunction(), |
| 102 | + 'The second argument of `jest.mock` must be an inline function.', |
| 103 | + ); |
| 104 | + |
| 105 | + const ids: Set<NodePath<Identifier>> = new Set(); |
| 106 | + const parentScope = moduleFactory.parentPath.scope; |
| 107 | + // @ts-ignore: Same as above: ReferencedIdentifier doesn't exist |
| 108 | + moduleFactory.traverse(IDVisitor, {ids}); |
| 109 | + for (const id of ids) { |
| 110 | + const {name} = id.node; |
| 111 | + let found = false; |
| 112 | + let scope = id.scope; |
| 113 | + |
| 114 | + while (scope !== parentScope) { |
| 115 | + if (scope.bindings[name]) { |
| 116 | + found = true; |
| 117 | + break; |
| 118 | + } |
| 119 | + |
| 120 | + scope = scope.parent; |
| 121 | + } |
| 122 | + |
| 123 | + if (!found) { |
| 124 | + invariant( |
| 125 | + (scope.hasGlobal(name) && WHITELISTED_IDENTIFIERS.has(name)) || |
| 126 | + /^mock/i.test(name) || |
| 127 | + // Allow istanbul's coverage variable to pass. |
| 128 | + /^(?:__)?cov/.test(name), |
| 129 | + 'The module factory of `jest.mock()` is not allowed to ' + |
| 130 | + 'reference any out-of-scope variables.\n' + |
| 131 | + 'Invalid variable access: ' + |
| 132 | + name + |
| 133 | + '\n' + |
| 134 | + 'Whitelisted objects: ' + |
| 135 | + Array.from(WHITELISTED_IDENTIFIERS).join(', ') + |
| 136 | + '.\n' + |
| 137 | + 'Note: This is a precaution to guard against uninitialized mock ' + |
| 138 | + 'variables. If it is ensured that the mock is required lazily, ' + |
| 139 | + 'variable names prefixed with `mock` (case insensitive) are permitted.', |
| 140 | + ); |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + return true; |
| 145 | + } |
| 146 | + return false; |
| 147 | +}; |
| 148 | + |
| 149 | +FUNCTIONS.unmock = (args: Array<NodePath>) => |
| 150 | + args.length === 1 && args[0].isStringLiteral(); |
| 151 | +FUNCTIONS.deepUnmock = (args: Array<NodePath>) => |
| 152 | + args.length === 1 && args[0].isStringLiteral(); |
| 153 | +FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = ( |
| 154 | + args: Array<NodePath>, |
| 155 | +) => args.length === 0; |
| 156 | + |
| 157 | +export = () => { |
| 158 | + const shouldHoistExpression = (expr: NodePath): boolean => { |
| 159 | + if (!expr.isCallExpression()) { |
| 160 | + return false; |
| 161 | + } |
| 162 | + |
| 163 | + const callee = expr.get('callee'); |
| 164 | + // TODO: avoid type casts - the types can be arrays (is it possible to ignore that without casting?) |
| 165 | + const object = callee.get('object') as NodePath; |
| 166 | + const property = callee.get('property') as NodePath; |
| 167 | + return ( |
| 168 | + property.isIdentifier() && |
| 169 | + FUNCTIONS[property.node.name] && |
| 170 | + (object.isIdentifier(JEST_GLOBAL) || |
| 171 | + (callee.isMemberExpression() && shouldHoistExpression(object))) && |
| 172 | + FUNCTIONS[property.node.name](expr.get('arguments')) |
| 173 | + ); |
| 174 | + }; |
| 175 | + |
| 176 | + const visitor: Visitor = { |
| 177 | + ExpressionStatement(path) { |
| 178 | + if (shouldHoistExpression(path.get('expression'))) { |
| 179 | + // @ts-ignore: private, magical property |
| 180 | + path.node._blockHoist = Infinity; |
| 181 | + } |
| 182 | + }, |
| 183 | + }; |
| 184 | + |
| 185 | + return {visitor}; |
| 186 | +}; |
0 commit comments