eslint-plugin-jest 26.2.1 → 26.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/docs/rules/prefer-hooks-in-order.md +133 -0
- package/lib/rules/consistent-test-it.js +9 -8
- package/lib/rules/expect-expect.js +1 -1
- package/lib/rules/max-nested-describe.js +2 -2
- package/lib/rules/no-conditional-expect.js +2 -2
- package/lib/rules/no-conditional-in-test.js +2 -2
- package/lib/rules/no-done-callback.js +4 -2
- package/lib/rules/no-duplicate-hooks.js +22 -23
- package/lib/rules/no-export.js +1 -1
- package/lib/rules/no-focused-tests.js +14 -22
- package/lib/rules/no-hooks.js +4 -2
- package/lib/rules/no-identical-title.js +10 -7
- package/lib/rules/no-if.js +4 -2
- package/lib/rules/no-standalone-expect.js +2 -2
- package/lib/rules/no-test-prefixes.js +12 -20
- package/lib/rules/no-test-return-statement.js +4 -1
- package/lib/rules/prefer-expect-assertions.js +2 -2
- package/lib/rules/prefer-hooks-in-order.js +84 -0
- package/lib/rules/prefer-hooks-on-top.js +2 -2
- package/lib/rules/prefer-lowercase-title.js +11 -22
- package/lib/rules/prefer-snapshot-hint.js +2 -2
- package/lib/rules/prefer-todo.js +24 -7
- package/lib/rules/require-hook.js +2 -2
- package/lib/rules/require-top-level-describe.js +9 -4
- package/lib/rules/utils/parseJestFnCall.js +306 -0
- package/lib/rules/utils.js +40 -258
- package/lib/rules/valid-describe-callback.js +4 -2
- package/lib/rules/valid-expect-in-promise.js +9 -11
- package/lib/rules/valid-title.js +7 -6
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _utils = require("./utils");
|
|
9
|
+
|
|
10
|
+
const HooksOrder = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];
|
|
11
|
+
|
|
12
|
+
var _default = (0, _utils.createRule)({
|
|
13
|
+
name: __filename,
|
|
14
|
+
meta: {
|
|
15
|
+
docs: {
|
|
16
|
+
category: 'Best Practices',
|
|
17
|
+
description: 'Prefer having hooks in a consistent order',
|
|
18
|
+
recommended: false
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
reorderHooks: `\`{{ currentHook }}\` hooks should be before any \`{{ previousHook }}\` hooks`
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
type: 'suggestion'
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
|
|
28
|
+
create(context) {
|
|
29
|
+
let previousHookIndex = -1;
|
|
30
|
+
let inHook = false;
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (inHook) {
|
|
34
|
+
// Ignore everything that is passed into a hook
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const jestFnCall = (0, _utils.parseJestFnCall)(node, context.getScope());
|
|
39
|
+
|
|
40
|
+
if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'hook') {
|
|
41
|
+
// Reset the previousHookIndex when encountering something different from a hook
|
|
42
|
+
previousHookIndex = -1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
inHook = true;
|
|
47
|
+
const currentHook = jestFnCall.name;
|
|
48
|
+
const currentHookIndex = HooksOrder.indexOf(currentHook);
|
|
49
|
+
|
|
50
|
+
if (currentHookIndex < previousHookIndex) {
|
|
51
|
+
context.report({
|
|
52
|
+
messageId: 'reorderHooks',
|
|
53
|
+
node,
|
|
54
|
+
data: {
|
|
55
|
+
previousHook: HooksOrder[previousHookIndex],
|
|
56
|
+
currentHook
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
previousHookIndex = currentHookIndex;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
'CallExpression:exit'(node) {
|
|
66
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, context.getScope(), ['hook'])) {
|
|
67
|
+
inHook = false;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (inHook) {
|
|
72
|
+
return;
|
|
73
|
+
} // Reset the previousHookIndex when encountering something different from a hook
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
previousHookIndex = -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
exports.default = _default;
|
|
@@ -29,11 +29,11 @@ var _default = (0, _utils.createRule)({
|
|
|
29
29
|
CallExpression(node) {
|
|
30
30
|
const scope = context.getScope();
|
|
31
31
|
|
|
32
|
-
if (
|
|
32
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, scope, ['test'])) {
|
|
33
33
|
hooksContext[hooksContext.length - 1] = true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
if (hooksContext[hooksContext.length - 1] && (0, _utils.
|
|
36
|
+
if (hooksContext[hooksContext.length - 1] && (0, _utils.isTypeOfJestFnCall)(node, scope, ['hook'])) {
|
|
37
37
|
context.report({
|
|
38
38
|
messageId: 'noHookOnTop',
|
|
39
39
|
node
|
|
@@ -9,18 +9,6 @@ var _utils = require("./utils");
|
|
|
9
9
|
|
|
10
10
|
const hasStringAsFirstArgument = node => node.arguments[0] && (0, _utils.isStringNode)(node.arguments[0]);
|
|
11
11
|
|
|
12
|
-
const findNodeNameAndArgument = (node, scope) => {
|
|
13
|
-
if (!((0, _utils.isTestCaseCall)(node, scope) || (0, _utils.isDescribeCall)(node, scope))) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!hasStringAsFirstArgument(node)) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return [(0, _utils.getNodeName)(node).split('.')[0], node.arguments[0]];
|
|
22
|
-
};
|
|
23
|
-
|
|
24
12
|
const populateIgnores = ignore => {
|
|
25
13
|
const ignores = [];
|
|
26
14
|
|
|
@@ -93,22 +81,23 @@ var _default = (0, _utils.createRule)({
|
|
|
93
81
|
return {
|
|
94
82
|
CallExpression(node) {
|
|
95
83
|
const scope = context.getScope();
|
|
84
|
+
const jestFnCall = (0, _utils.parseJestFnCall)(node, scope);
|
|
96
85
|
|
|
97
|
-
if (
|
|
86
|
+
if (!jestFnCall || !hasStringAsFirstArgument(node)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (jestFnCall.type === 'describe') {
|
|
98
91
|
numberOfDescribeBlocks++;
|
|
99
92
|
|
|
100
93
|
if (ignoreTopLevelDescribe && numberOfDescribeBlocks === 1) {
|
|
101
94
|
return;
|
|
102
95
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const results = findNodeNameAndArgument(node, scope);
|
|
106
|
-
|
|
107
|
-
if (!results) {
|
|
96
|
+
} else if (jestFnCall.type !== 'test') {
|
|
108
97
|
return;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
const [
|
|
100
|
+
const [firstArg] = node.arguments;
|
|
112
101
|
const description = (0, _utils.getStringValue)(firstArg);
|
|
113
102
|
|
|
114
103
|
if (allowedPrefixes.some(name => description.startsWith(name))) {
|
|
@@ -117,7 +106,7 @@ var _default = (0, _utils.createRule)({
|
|
|
117
106
|
|
|
118
107
|
const firstCharacter = description.charAt(0);
|
|
119
108
|
|
|
120
|
-
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase() || ignores.includes(name)) {
|
|
109
|
+
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase() || ignores.includes(jestFnCall.name)) {
|
|
121
110
|
return;
|
|
122
111
|
}
|
|
123
112
|
|
|
@@ -125,7 +114,7 @@ var _default = (0, _utils.createRule)({
|
|
|
125
114
|
messageId: 'unexpectedLowercase',
|
|
126
115
|
node: node.arguments[0],
|
|
127
116
|
data: {
|
|
128
|
-
method: name
|
|
117
|
+
method: jestFnCall.name
|
|
129
118
|
},
|
|
130
119
|
|
|
131
120
|
fix(fixer) {
|
|
@@ -139,7 +128,7 @@ var _default = (0, _utils.createRule)({
|
|
|
139
128
|
},
|
|
140
129
|
|
|
141
130
|
'CallExpression:exit'(node) {
|
|
142
|
-
if ((0, _utils.
|
|
131
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, context.getScope(), ['describe'])) {
|
|
143
132
|
numberOfDescribeBlocks--;
|
|
144
133
|
}
|
|
145
134
|
}
|
|
@@ -108,7 +108,7 @@ var _default = (0, _utils.createRule)({
|
|
|
108
108
|
'CallExpression:exit'(node) {
|
|
109
109
|
const scope = context.getScope();
|
|
110
110
|
|
|
111
|
-
if ((0, _utils.
|
|
111
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, scope, ['describe', 'test'])) {
|
|
112
112
|
var _depths$pop;
|
|
113
113
|
|
|
114
114
|
/* istanbul ignore next */
|
|
@@ -119,7 +119,7 @@ var _default = (0, _utils.createRule)({
|
|
|
119
119
|
CallExpression(node) {
|
|
120
120
|
const scope = context.getScope();
|
|
121
121
|
|
|
122
|
-
if ((0, _utils.
|
|
122
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, scope, ['describe', 'test'])) {
|
|
123
123
|
depths.push(expressionDepth);
|
|
124
124
|
expressionDepth = 0;
|
|
125
125
|
}
|
package/lib/rules/prefer-todo.js
CHANGED
|
@@ -17,12 +17,28 @@ function isEmptyFunction(node) {
|
|
|
17
17
|
return node.body.type === _utils.AST_NODE_TYPES.BlockStatement && !node.body.body.length;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function createTodoFixer(
|
|
21
|
-
const
|
|
22
|
-
|
|
20
|
+
function createTodoFixer(jestFnCall, fixer) {
|
|
21
|
+
const fixes = [fixer.replaceText(jestFnCall.head.node, `${jestFnCall.head.local}.todo`)];
|
|
22
|
+
|
|
23
|
+
if (jestFnCall.members.length) {
|
|
24
|
+
fixes.unshift(fixer.removeRange([jestFnCall.head.node.range[1], jestFnCall.members[0].range[1]]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fixes;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
const isTargetedTestCase =
|
|
30
|
+
const isTargetedTestCase = jestFnCall => {
|
|
31
|
+
if (jestFnCall.members.some(s => (0, _utils2.getAccessorValue)(s) !== 'skip')) {
|
|
32
|
+
return false;
|
|
33
|
+
} // todo: we should support this too (needs custom fixer)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if (jestFnCall.name.startsWith('x')) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return !jestFnCall.name.startsWith('f');
|
|
41
|
+
};
|
|
26
42
|
|
|
27
43
|
var _default = (0, _utils2.createRule)({
|
|
28
44
|
name: __filename,
|
|
@@ -46,8 +62,9 @@ var _default = (0, _utils2.createRule)({
|
|
|
46
62
|
return {
|
|
47
63
|
CallExpression(node) {
|
|
48
64
|
const [title, callback] = node.arguments;
|
|
65
|
+
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context.getScope());
|
|
49
66
|
|
|
50
|
-
if (!title ||
|
|
67
|
+
if (!title || (jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) !== 'test' || !isTargetedTestCase(jestFnCall) || !(0, _utils2.isStringNode)(title)) {
|
|
51
68
|
return;
|
|
52
69
|
}
|
|
53
70
|
|
|
@@ -55,7 +72,7 @@ var _default = (0, _utils2.createRule)({
|
|
|
55
72
|
context.report({
|
|
56
73
|
messageId: 'emptyTest',
|
|
57
74
|
node,
|
|
58
|
-
fix: fixer => [fixer.removeRange([title.range[1], callback.range[1]]), createTodoFixer(
|
|
75
|
+
fix: fixer => [fixer.removeRange([title.range[1], callback.range[1]]), ...createTodoFixer(jestFnCall, fixer)]
|
|
59
76
|
});
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -63,7 +80,7 @@ var _default = (0, _utils2.createRule)({
|
|
|
63
80
|
context.report({
|
|
64
81
|
messageId: 'unimplementedTest',
|
|
65
82
|
node,
|
|
66
|
-
fix: fixer =>
|
|
83
|
+
fix: fixer => createTodoFixer(jestFnCall, fixer)
|
|
67
84
|
});
|
|
68
85
|
}
|
|
69
86
|
}
|
|
@@ -12,7 +12,7 @@ var _utils2 = require("./utils");
|
|
|
12
12
|
const isJestFnCall = (node, scope) => {
|
|
13
13
|
var _getNodeName;
|
|
14
14
|
|
|
15
|
-
if ((0, _utils2.
|
|
15
|
+
if ((0, _utils2.parseJestFnCall)(node, scope)) {
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -100,7 +100,7 @@ var _default = (0, _utils2.createRule)({
|
|
|
100
100
|
},
|
|
101
101
|
|
|
102
102
|
CallExpression(node) {
|
|
103
|
-
if (!(0, _utils2.
|
|
103
|
+
if (!(0, _utils2.isTypeOfJestFnCall)(node, context.getScope(), ['describe']) || node.arguments.length < 2) {
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -47,8 +47,13 @@ var _default = (0, _utils.createRule)({
|
|
|
47
47
|
return {
|
|
48
48
|
CallExpression(node) {
|
|
49
49
|
const scope = context.getScope();
|
|
50
|
+
const jestFnCall = (0, _utils.parseJestFnCall)(node, scope);
|
|
50
51
|
|
|
51
|
-
if (
|
|
52
|
+
if (!jestFnCall) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (jestFnCall.type === 'describe') {
|
|
52
57
|
numberOfDescribeBlocks++;
|
|
53
58
|
|
|
54
59
|
if (numberOfDescribeBlocks === 1) {
|
|
@@ -70,7 +75,7 @@ var _default = (0, _utils.createRule)({
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
if (numberOfDescribeBlocks === 0) {
|
|
73
|
-
if (
|
|
78
|
+
if (jestFnCall.type === 'test') {
|
|
74
79
|
context.report({
|
|
75
80
|
node,
|
|
76
81
|
messageId: 'unexpectedTestCase'
|
|
@@ -78,7 +83,7 @@ var _default = (0, _utils.createRule)({
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
if (
|
|
86
|
+
if (jestFnCall.type === 'hook') {
|
|
82
87
|
context.report({
|
|
83
88
|
node,
|
|
84
89
|
messageId: 'unexpectedHook'
|
|
@@ -89,7 +94,7 @@ var _default = (0, _utils.createRule)({
|
|
|
89
94
|
},
|
|
90
95
|
|
|
91
96
|
'CallExpression:exit'(node) {
|
|
92
|
-
if ((0, _utils.
|
|
97
|
+
if ((0, _utils.isTypeOfJestFnCall)(node, context.getScope(), ['describe'])) {
|
|
93
98
|
numberOfDescribeBlocks--;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getNodeChain = getNodeChain;
|
|
7
|
+
exports.scopeHasLocalReference = exports.parseJestFnCall = exports.isTypeOfJestFnCall = void 0;
|
|
8
|
+
|
|
9
|
+
var _utils = require("@typescript-eslint/utils");
|
|
10
|
+
|
|
11
|
+
var _utils2 = require("../utils");
|
|
12
|
+
|
|
13
|
+
const isTypeOfJestFnCall = (node, scope, types) => {
|
|
14
|
+
const jestFnCall = parseJestFnCall(node, scope);
|
|
15
|
+
return jestFnCall !== null && types.includes(jestFnCall.type);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
exports.isTypeOfJestFnCall = isTypeOfJestFnCall;
|
|
19
|
+
|
|
20
|
+
function getNodeChain(node) {
|
|
21
|
+
if ((0, _utils2.isSupportedAccessor)(node)) {
|
|
22
|
+
return [node];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (node.type) {
|
|
26
|
+
case _utils.AST_NODE_TYPES.TaggedTemplateExpression:
|
|
27
|
+
return getNodeChain(node.tag);
|
|
28
|
+
|
|
29
|
+
case _utils.AST_NODE_TYPES.MemberExpression:
|
|
30
|
+
return [...getNodeChain(node.object), ...getNodeChain(node.property)];
|
|
31
|
+
|
|
32
|
+
case _utils.AST_NODE_TYPES.NewExpression:
|
|
33
|
+
case _utils.AST_NODE_TYPES.CallExpression:
|
|
34
|
+
return getNodeChain(node.callee);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const determineJestFnType = name => {
|
|
41
|
+
// if (name === 'expect') {
|
|
42
|
+
// return 'expect';
|
|
43
|
+
// }
|
|
44
|
+
if (name === 'jest') {
|
|
45
|
+
return 'jest';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (_utils2.DescribeAlias.hasOwnProperty(name)) {
|
|
49
|
+
return 'describe';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (_utils2.TestCaseName.hasOwnProperty(name)) {
|
|
53
|
+
return 'test';
|
|
54
|
+
}
|
|
55
|
+
/* istanbul ignore else */
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if (_utils2.HookName.hasOwnProperty(name)) {
|
|
59
|
+
return 'hook';
|
|
60
|
+
}
|
|
61
|
+
/* istanbul ignore next */
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
return 'unknown';
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ValidJestFnCallChains = ['afterAll', 'afterEach', 'beforeAll', 'beforeEach', 'describe', 'describe.each', 'describe.only', 'describe.only.each', 'describe.skip', 'describe.skip.each', 'fdescribe', 'fdescribe.each', 'xdescribe', 'xdescribe.each', 'it', 'it.concurrent', 'it.concurrent.each', 'it.concurrent.only.each', 'it.concurrent.skip.each', 'it.each', 'it.failing', 'it.only', 'it.only.each', 'it.skip', 'it.skip.each', 'it.todo', 'fit', 'fit.each', 'xit', 'xit.each', 'test', 'test.concurrent', 'test.concurrent.each', 'test.concurrent.only.each', 'test.concurrent.skip.each', 'test.each', 'test.only', 'test.only.each', 'test.skip', 'test.skip.each', 'test.todo', 'xtest', 'xtest.each', // todo: check if actually valid (not in docs)
|
|
68
|
+
'test.concurrent.skip', 'test.concurrent.only', 'it.concurrent.skip', 'it.concurrent.only'];
|
|
69
|
+
|
|
70
|
+
const parseJestFnCall = (node, scope) => {
|
|
71
|
+
var _node$parent, _node$parent2, _resolved$original;
|
|
72
|
+
|
|
73
|
+
// ensure that we're at the "top" of the function call chain otherwise when
|
|
74
|
+
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
|
|
75
|
+
// the full chain is not a valid jest function call chain
|
|
76
|
+
if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.CallExpression || ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === _utils.AST_NODE_TYPES.MemberExpression) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const chain = getNodeChain(node);
|
|
81
|
+
|
|
82
|
+
if (chain.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
} // ensure that the only call expression in the chain is at the end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if (chain.slice(0, chain.length - 1).some(nod => {
|
|
88
|
+
var _nod$parent;
|
|
89
|
+
|
|
90
|
+
return ((_nod$parent = nod.parent) === null || _nod$parent === void 0 ? void 0 : _nod$parent.type) === _utils.AST_NODE_TYPES.CallExpression;
|
|
91
|
+
})) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const [first, ...rest] = chain;
|
|
96
|
+
const lastNode = chain[chain.length - 1]; // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
|
|
97
|
+
|
|
98
|
+
if ((0, _utils2.isSupportedAccessor)(lastNode, 'each')) {
|
|
99
|
+
if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const resolved = resolveToJestFn(scope, (0, _utils2.getAccessorValue)(first)); // we're not a jest function
|
|
105
|
+
|
|
106
|
+
if (!resolved) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const name = (_resolved$original = resolved.original) !== null && _resolved$original !== void 0 ? _resolved$original : resolved.local;
|
|
111
|
+
const links = [name, ...rest.map(link => (0, _utils2.getAccessorValue)(link))];
|
|
112
|
+
|
|
113
|
+
if (name !== 'jest' && !ValidJestFnCallChains.includes(links.join('.'))) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
type: determineJestFnType(name),
|
|
120
|
+
head: { ...resolved,
|
|
121
|
+
node: first
|
|
122
|
+
},
|
|
123
|
+
members: rest
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
exports.parseJestFnCall = parseJestFnCall;
|
|
128
|
+
|
|
129
|
+
const describeImportDefAsImport = def => {
|
|
130
|
+
if (def.parent.type === _utils.AST_NODE_TYPES.TSImportEqualsDeclaration) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (def.node.type !== _utils.AST_NODE_TYPES.ImportSpecifier) {
|
|
135
|
+
return null;
|
|
136
|
+
} // we only care about value imports
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if (def.parent.importKind === 'type') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
source: def.parent.source.value,
|
|
145
|
+
imported: def.node.imported.name,
|
|
146
|
+
local: def.node.local.name
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Attempts to find the node that represents the import source for the
|
|
151
|
+
* given expression node, if it looks like it's an import.
|
|
152
|
+
*
|
|
153
|
+
* If no such node can be found (e.g. because the expression doesn't look
|
|
154
|
+
* like an import), then `null` is returned instead.
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
const findImportSourceNode = node => {
|
|
159
|
+
if (node.type === _utils.AST_NODE_TYPES.AwaitExpression) {
|
|
160
|
+
if (node.argument.type === _utils.AST_NODE_TYPES.ImportExpression) {
|
|
161
|
+
return node.argument.source;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isIdentifier)(node.callee, 'require')) {
|
|
168
|
+
var _node$arguments$;
|
|
169
|
+
|
|
170
|
+
return (_node$arguments$ = node.arguments[0]) !== null && _node$arguments$ !== void 0 ? _node$arguments$ : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const describeVariableDefAsImport = def => {
|
|
177
|
+
var _def$name$parent;
|
|
178
|
+
|
|
179
|
+
// make sure that we've actually being assigned a value
|
|
180
|
+
if (!def.node.init) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const sourceNode = findImportSourceNode(def.node.init);
|
|
185
|
+
|
|
186
|
+
if (!sourceNode || !(0, _utils2.isStringNode)(sourceNode)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (((_def$name$parent = def.name.parent) === null || _def$name$parent === void 0 ? void 0 : _def$name$parent.type) !== _utils.AST_NODE_TYPES.Property) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!(0, _utils2.isSupportedAccessor)(def.name.parent.key)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
source: (0, _utils2.getStringValue)(sourceNode),
|
|
200
|
+
imported: (0, _utils2.getAccessorValue)(def.name.parent.key),
|
|
201
|
+
local: def.name.name
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Attempts to describe a definition as an import if possible.
|
|
206
|
+
*
|
|
207
|
+
* If the definition is an import binding, it's described as you'd expect.
|
|
208
|
+
* If the definition is a variable, then we try and determine if it's either
|
|
209
|
+
* a dynamic `import()` or otherwise a call to `require()`.
|
|
210
|
+
*
|
|
211
|
+
* If it's neither of these, `null` is returned to indicate that the definition
|
|
212
|
+
* is not describable as an import of any kind.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
const describePossibleImportDef = def => {
|
|
217
|
+
if (def.type === 'Variable') {
|
|
218
|
+
return describeVariableDefAsImport(def);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (def.type === 'ImportBinding') {
|
|
222
|
+
return describeImportDefAsImport(def);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const collectReferences = scope => {
|
|
229
|
+
const locals = new Set();
|
|
230
|
+
const imports = new Map();
|
|
231
|
+
const unresolved = new Set();
|
|
232
|
+
let currentScope = scope;
|
|
233
|
+
|
|
234
|
+
while (currentScope !== null) {
|
|
235
|
+
for (const ref of currentScope.variables) {
|
|
236
|
+
if (ref.defs.length === 0) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const def = ref.defs[ref.defs.length - 1];
|
|
241
|
+
const importDetails = describePossibleImportDef(def);
|
|
242
|
+
|
|
243
|
+
if (importDetails) {
|
|
244
|
+
imports.set(importDetails.local, importDetails);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
locals.add(ref.name);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const ref of currentScope.through) {
|
|
252
|
+
unresolved.add(ref.identifier.name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
currentScope = currentScope.upper;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
locals,
|
|
260
|
+
imports,
|
|
261
|
+
unresolved
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const resolveToJestFn = (scope, identifier) => {
|
|
266
|
+
const references = collectReferences(scope);
|
|
267
|
+
const maybeImport = references.imports.get(identifier);
|
|
268
|
+
|
|
269
|
+
if (maybeImport) {
|
|
270
|
+
// the identifier is imported from @jest/globals,
|
|
271
|
+
// so return the original import name
|
|
272
|
+
if (maybeImport.source === '@jest/globals') {
|
|
273
|
+
return {
|
|
274
|
+
original: maybeImport.imported,
|
|
275
|
+
local: maybeImport.local,
|
|
276
|
+
type: 'import'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
} // the identifier was found as a local variable or function declaration
|
|
282
|
+
// meaning it's not a function from jest
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if (references.locals.has(identifier)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
original: null,
|
|
291
|
+
local: identifier,
|
|
292
|
+
type: 'global'
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const scopeHasLocalReference = (scope, referenceName) => {
|
|
297
|
+
const references = collectReferences(scope);
|
|
298
|
+
return (// referenceName was found as a local variable or function declaration.
|
|
299
|
+
references.locals.has(referenceName) || // referenceName was found as an imported identifier
|
|
300
|
+
references.imports.has(referenceName) || // referenceName was not found as an unresolved reference,
|
|
301
|
+
// meaning it is likely not an implicit global reference.
|
|
302
|
+
!references.unresolved.has(referenceName)
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
exports.scopeHasLocalReference = scopeHasLocalReference;
|