Skip to content

Commit c14a45d

Browse files
deneuv34SimenB
authored andcommitted
chore: Migrate babel-plugin-jest-hoist to Typescript (#7898)
1 parent 544984e commit c14a45d

File tree

7 files changed

+215
-180
lines changed

7 files changed

+215
-180
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
- `[@jest/reporter]`: New package extracted from `jest-cli` ([#7902](https://github.com/facebook/jest/pull/7902))
4242
- `[jest-snapshot]`: Migrate to TypeScript ([#7899](https://github.com/facebook/jest/pull/7899))
4343
- `[@jest/transform]`: New package extracted from `jest-runtime` ([#7915](https://github.com/facebook/jest/pull/7915))
44+
- `[babel-plugin-jest-hoist]`: Migrate to TypeScript ([#7898](https://github.com/facebook/jest/pull/7898))
4445

4546
### Performance
4647

packages/babel-jest/tsconfig.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"rootDir": "src",
55
"outDir": "build"
66
},
7-
// TODO: include `babel-preset-jest` even though we don't care about its types
8-
"references": [{"path": "../jest-types"}]
7+
// TODO: include `babel-preset-jest` if it's ever in TS even though we don't care about its types
8+
"references": [
9+
{"path": "../jest-types"}
10+
]
911
}

packages/babel-plugin-jest-hoist/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@
1111
},
1212
"license": "MIT",
1313
"main": "build/index.js",
14+
"types": "build/index.d.ts",
15+
"dependencies": {
16+
"@types/babel__traverse": "^7.0.6"
17+
},
18+
"devDependencies": {
19+
"@babel/types": "^7.3.3"
20+
},
1421
"gitHead": "b16789230fd45056a7f2fa199bae06c7a1780deb"
1522
}

packages/babel-plugin-jest-hoist/src/index.js

Lines changed: 0 additions & 168 deletions
This file was deleted.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "src",
5+
"outDir": "build"
6+
}
7+
}

0 commit comments

Comments
 (0)