Skip to content

Commit

Permalink
feat: show better errors from babel-plugin-jest-hoist (#8865)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Aug 22, 2019
1 parent b8a4a85 commit 93555aa
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 92 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[babel-plugin-jest-hoist]` Show codeframe on static hoisting issues ([#8865](https://github.com/facebook/jest/pull/8865))
- `[jest-config]` [**BREAKING**] Set default display name color based on runner ([#8689](https://github.com/facebook/jest/pull/8689))

### Fixes
Expand Down
183 changes: 91 additions & 92 deletions packages/babel-plugin-jest-hoist/src/index.ts
Expand Up @@ -7,76 +7,70 @@
*/

// Only used for types
// eslint-disable-next-line
/* eslint-disable import/no-extraneous-dependencies */
import {NodePath, Visitor} from '@babel/traverse';
// eslint-disable-next-line
import {Identifier} from '@babel/types';

const invariant = (condition: unknown, message: string) => {
if (!condition) {
throw new Error('babel-plugin-jest-hoist: ' + message);
}
};
/* eslint-enable */

// We allow `jest`, `expect`, `require`, all default Node.js globals and all
// ES2015 built-ins to be used inside of a `jest.mock` factory.
// We also allow variables prefixed with `mock` as an escape-hatch.
const WHITELISTED_IDENTIFIERS: Set<string> = new Set([
'Array',
'ArrayBuffer',
'Boolean',
'DataView',
'Date',
'Error',
'EvalError',
'Float32Array',
'Float64Array',
'Function',
'Generator',
'GeneratorFunction',
'Infinity',
'Int16Array',
'Int32Array',
'Int8Array',
'InternalError',
'Intl',
'JSON',
'Map',
'Math',
'NaN',
'Number',
'Object',
'Promise',
'Proxy',
'RangeError',
'ReferenceError',
'Reflect',
'RegExp',
'Set',
'String',
'Symbol',
'SyntaxError',
'TypeError',
'URIError',
'Uint16Array',
'Uint32Array',
'Uint8Array',
'Uint8ClampedArray',
'WeakMap',
'WeakSet',
'arguments',
'console',
'expect',
'isNaN',
'jest',
'parseFloat',
'parseInt',
'require',
'undefined',
]);
Object.getOwnPropertyNames(global).forEach(name => {
WHITELISTED_IDENTIFIERS.add(name);
});
const WHITELISTED_IDENTIFIERS = new Set<string>(
[
'Array',
'ArrayBuffer',
'Boolean',
'DataView',
'Date',
'Error',
'EvalError',
'Float32Array',
'Float64Array',
'Function',
'Generator',
'GeneratorFunction',
'Infinity',
'Int16Array',
'Int32Array',
'Int8Array',
'InternalError',
'Intl',
'JSON',
'Map',
'Math',
'NaN',
'Number',
'Object',
'Promise',
'Proxy',
'RangeError',
'ReferenceError',
'Reflect',
'RegExp',
'Set',
'String',
'Symbol',
'SyntaxError',
'TypeError',
'URIError',
'Uint16Array',
'Uint32Array',
'Uint8Array',
'Uint8ClampedArray',
'WeakMap',
'WeakSet',
'arguments',
'console',
'expect',
'isNaN',
'jest',
'parseFloat',
'parseInt',
'require',
'undefined',
...Object.getOwnPropertyNames(global),
].sort(),
);

const JEST_GLOBAL = {name: 'jest'};
// TODO: Should be Visitor<{ids: Set<NodePath<Identifier>>}>, but `ReferencedIdentifier` doesn't exist
Expand All @@ -93,15 +87,18 @@ const FUNCTIONS: Record<
(args: Array<NodePath>) => boolean
> = Object.create(null);

FUNCTIONS.mock = (args: Array<NodePath>) => {
FUNCTIONS.mock = args => {
if (args.length === 1) {
return args[0].isStringLiteral() || args[0].isLiteral();
} else if (args.length === 2 || args.length === 3) {
const moduleFactory = args[1];
invariant(
moduleFactory.isFunction(),
'The second argument of `jest.mock` must be an inline function.',
);

if (!moduleFactory.isFunction()) {
throw moduleFactory.buildCodeFrameError(
'The second argument of `jest.mock` must be an inline function.\n',
TypeError,
);
}

const ids: Set<NodePath<Identifier>> = new Set();
const parentScope = moduleFactory.parentPath.scope;
Expand All @@ -122,23 +119,28 @@ FUNCTIONS.mock = (args: Array<NodePath>) => {
}

if (!found) {
invariant(
const isAllowedIdentifier =
(scope.hasGlobal(name) && WHITELISTED_IDENTIFIERS.has(name)) ||
/^mock/i.test(name) ||
// Allow istanbul's coverage variable to pass.
/^(?:__)?cov/.test(name),
'The module factory of `jest.mock()` is not allowed to ' +
'reference any out-of-scope variables.\n' +
'Invalid variable access: ' +
name +
'\n' +
'Whitelisted objects: ' +
Array.from(WHITELISTED_IDENTIFIERS).join(', ') +
'.\n' +
'Note: This is a precaution to guard against uninitialized mock ' +
'variables. If it is ensured that the mock is required lazily, ' +
'variable names prefixed with `mock` (case insensitive) are permitted.',
);
/^mock/i.test(name) ||
// Allow istanbul's coverage variable to pass.
/^(?:__)?cov/.test(name);

if (!isAllowedIdentifier) {
throw id.buildCodeFrameError(
'The module factory of `jest.mock()` is not allowed to ' +
'reference any out-of-scope variables.\n' +
'Invalid variable access: ' +
name +
'\n' +
'Whitelisted objects: ' +
Array.from(WHITELISTED_IDENTIFIERS).join(', ') +
'.\n' +
'Note: This is a precaution to guard against uninitialized mock ' +
'variables. If it is ensured that the mock is required lazily, ' +
'variable names prefixed with `mock` (case insensitive) are permitted.\n',
ReferenceError,
);
}
}
}

Expand All @@ -147,13 +149,10 @@ FUNCTIONS.mock = (args: Array<NodePath>) => {
return false;
};

FUNCTIONS.unmock = (args: Array<NodePath>) =>
args.length === 1 && args[0].isStringLiteral();
FUNCTIONS.deepUnmock = (args: Array<NodePath>) =>
args.length === 1 && args[0].isStringLiteral();
FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = (
args: Array<NodePath>,
) => args.length === 0;
FUNCTIONS.unmock = args => args.length === 1 && args[0].isStringLiteral();
FUNCTIONS.deepUnmock = args => args.length === 1 && args[0].isStringLiteral();
FUNCTIONS.disableAutomock = FUNCTIONS.enableAutomock = args =>
args.length === 0;

export = () => {
const shouldHoistExpression = (expr: NodePath): boolean => {
Expand Down

0 comments on commit 93555aa

Please sign in to comment.