eslint-plugin-jest 26.3.0 → 26.4.2

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.
@@ -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 ((0, _utils.isDescribeCall)(node, scope)) {
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 [name, firstArg] = results;
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.isDescribeCall)(node, context.getScope())) {
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.isDescribeCall)(node, scope) || (0, _utils.isTestCaseCall)(node, scope)) {
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.isDescribeCall)(node, scope) || (0, _utils.isTestCaseCall)(node, scope)) {
122
+ if ((0, _utils.isTypeOfJestFnCall)(node, scope, ['describe', 'test'])) {
123
123
  depths.push(expressionDepth);
124
124
  expressionDepth = 0;
125
125
  }
@@ -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(node, fixer) {
21
- const testName = (0, _utils2.getNodeName)(node).split('.').shift();
22
- return fixer.replaceText(node.callee, `${testName}.todo`);
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 = (node, scope) => (0, _utils2.isTestCaseCall)(node, scope) && [_utils2.TestCaseName.it, _utils2.TestCaseName.test, 'it.skip', 'test.skip'].includes((0, _utils2.getNodeName)(node));
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 || !isTargetedTestCase(node, context.getScope()) || !(0, _utils2.isStringNode)(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(node, fixer)]
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 => [createTodoFixer(node, 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.isDescribeCall)(node, scope) || (0, _utils2.isTestCaseCall)(node, scope) || (0, _utils2.isHookCall)(node, scope)) {
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.isDescribeCall)(node, context.getScope()) || node.arguments.length < 2) {
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 ((0, _utils.isDescribeCall)(node, scope)) {
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 ((0, _utils.isTestCaseCall)(node, scope)) {
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 ((0, _utils.isHookCall)(node, scope)) {
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.isDescribeCall)(node, context.getScope())) {
97
+ if ((0, _utils.isTypeOfJestFnCall)(node, context.getScope(), ['describe'])) {
93
98
  numberOfDescribeBlocks--;
94
99
  }
95
100
  }
@@ -0,0 +1,305 @@
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.only.failing', 'it.skip', 'it.skip.each', 'it.skip.failing', 'it.todo', 'fit', 'fit.each', 'fit.failing', 'xit', 'xit.each', 'xit.failing', 'test', 'test.concurrent', 'test.concurrent.each', 'test.concurrent.only.each', 'test.concurrent.skip.each', 'test.each', 'test.failing', 'test.only', 'test.only.each', 'test.only.failing', 'test.skip', 'test.skip.each', 'test.skip.failing', 'test.todo', 'xtest', 'xtest.each', 'xtest.failing'];
68
+
69
+ const parseJestFnCall = (node, scope) => {
70
+ var _node$parent, _node$parent2, _resolved$original;
71
+
72
+ // ensure that we're at the "top" of the function call chain otherwise when
73
+ // parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
74
+ // the full chain is not a valid jest function call chain
75
+ 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) {
76
+ return null;
77
+ }
78
+
79
+ const chain = getNodeChain(node);
80
+
81
+ if (chain.length === 0) {
82
+ return null;
83
+ } // ensure that the only call expression in the chain is at the end
84
+
85
+
86
+ if (chain.slice(0, chain.length - 1).some(nod => {
87
+ var _nod$parent;
88
+
89
+ return ((_nod$parent = nod.parent) === null || _nod$parent === void 0 ? void 0 : _nod$parent.type) === _utils.AST_NODE_TYPES.CallExpression;
90
+ })) {
91
+ return null;
92
+ }
93
+
94
+ const [first, ...rest] = chain;
95
+ const lastNode = chain[chain.length - 1]; // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
96
+
97
+ if ((0, _utils2.isSupportedAccessor)(lastNode, 'each')) {
98
+ if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ const resolved = resolveToJestFn(scope, (0, _utils2.getAccessorValue)(first)); // we're not a jest function
104
+
105
+ if (!resolved) {
106
+ return null;
107
+ }
108
+
109
+ const name = (_resolved$original = resolved.original) !== null && _resolved$original !== void 0 ? _resolved$original : resolved.local;
110
+ const links = [name, ...rest.map(link => (0, _utils2.getAccessorValue)(link))];
111
+
112
+ if (name !== 'jest' && !ValidJestFnCallChains.includes(links.join('.'))) {
113
+ return null;
114
+ }
115
+
116
+ return {
117
+ name,
118
+ type: determineJestFnType(name),
119
+ head: { ...resolved,
120
+ node: first
121
+ },
122
+ members: rest
123
+ };
124
+ };
125
+
126
+ exports.parseJestFnCall = parseJestFnCall;
127
+
128
+ const describeImportDefAsImport = def => {
129
+ if (def.parent.type === _utils.AST_NODE_TYPES.TSImportEqualsDeclaration) {
130
+ return null;
131
+ }
132
+
133
+ if (def.node.type !== _utils.AST_NODE_TYPES.ImportSpecifier) {
134
+ return null;
135
+ } // we only care about value imports
136
+
137
+
138
+ if (def.parent.importKind === 'type') {
139
+ return null;
140
+ }
141
+
142
+ return {
143
+ source: def.parent.source.value,
144
+ imported: def.node.imported.name,
145
+ local: def.node.local.name
146
+ };
147
+ };
148
+ /**
149
+ * Attempts to find the node that represents the import source for the
150
+ * given expression node, if it looks like it's an import.
151
+ *
152
+ * If no such node can be found (e.g. because the expression doesn't look
153
+ * like an import), then `null` is returned instead.
154
+ */
155
+
156
+
157
+ const findImportSourceNode = node => {
158
+ if (node.type === _utils.AST_NODE_TYPES.AwaitExpression) {
159
+ if (node.argument.type === _utils.AST_NODE_TYPES.ImportExpression) {
160
+ return node.argument.source;
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ if (node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isIdentifier)(node.callee, 'require')) {
167
+ var _node$arguments$;
168
+
169
+ return (_node$arguments$ = node.arguments[0]) !== null && _node$arguments$ !== void 0 ? _node$arguments$ : null;
170
+ }
171
+
172
+ return null;
173
+ };
174
+
175
+ const describeVariableDefAsImport = def => {
176
+ var _def$name$parent;
177
+
178
+ // make sure that we've actually being assigned a value
179
+ if (!def.node.init) {
180
+ return null;
181
+ }
182
+
183
+ const sourceNode = findImportSourceNode(def.node.init);
184
+
185
+ if (!sourceNode || !(0, _utils2.isStringNode)(sourceNode)) {
186
+ return null;
187
+ }
188
+
189
+ if (((_def$name$parent = def.name.parent) === null || _def$name$parent === void 0 ? void 0 : _def$name$parent.type) !== _utils.AST_NODE_TYPES.Property) {
190
+ return null;
191
+ }
192
+
193
+ if (!(0, _utils2.isSupportedAccessor)(def.name.parent.key)) {
194
+ return null;
195
+ }
196
+
197
+ return {
198
+ source: (0, _utils2.getStringValue)(sourceNode),
199
+ imported: (0, _utils2.getAccessorValue)(def.name.parent.key),
200
+ local: def.name.name
201
+ };
202
+ };
203
+ /**
204
+ * Attempts to describe a definition as an import if possible.
205
+ *
206
+ * If the definition is an import binding, it's described as you'd expect.
207
+ * If the definition is a variable, then we try and determine if it's either
208
+ * a dynamic `import()` or otherwise a call to `require()`.
209
+ *
210
+ * If it's neither of these, `null` is returned to indicate that the definition
211
+ * is not describable as an import of any kind.
212
+ */
213
+
214
+
215
+ const describePossibleImportDef = def => {
216
+ if (def.type === 'Variable') {
217
+ return describeVariableDefAsImport(def);
218
+ }
219
+
220
+ if (def.type === 'ImportBinding') {
221
+ return describeImportDefAsImport(def);
222
+ }
223
+
224
+ return null;
225
+ };
226
+
227
+ const collectReferences = scope => {
228
+ const locals = new Set();
229
+ const imports = new Map();
230
+ const unresolved = new Set();
231
+ let currentScope = scope;
232
+
233
+ while (currentScope !== null) {
234
+ for (const ref of currentScope.variables) {
235
+ if (ref.defs.length === 0) {
236
+ continue;
237
+ }
238
+
239
+ const def = ref.defs[ref.defs.length - 1];
240
+ const importDetails = describePossibleImportDef(def);
241
+
242
+ if (importDetails) {
243
+ imports.set(importDetails.local, importDetails);
244
+ continue;
245
+ }
246
+
247
+ locals.add(ref.name);
248
+ }
249
+
250
+ for (const ref of currentScope.through) {
251
+ unresolved.add(ref.identifier.name);
252
+ }
253
+
254
+ currentScope = currentScope.upper;
255
+ }
256
+
257
+ return {
258
+ locals,
259
+ imports,
260
+ unresolved
261
+ };
262
+ };
263
+
264
+ const resolveToJestFn = (scope, identifier) => {
265
+ const references = collectReferences(scope);
266
+ const maybeImport = references.imports.get(identifier);
267
+
268
+ if (maybeImport) {
269
+ // the identifier is imported from @jest/globals,
270
+ // so return the original import name
271
+ if (maybeImport.source === '@jest/globals') {
272
+ return {
273
+ original: maybeImport.imported,
274
+ local: maybeImport.local,
275
+ type: 'import'
276
+ };
277
+ }
278
+
279
+ return null;
280
+ } // the identifier was found as a local variable or function declaration
281
+ // meaning it's not a function from jest
282
+
283
+
284
+ if (references.locals.has(identifier)) {
285
+ return null;
286
+ }
287
+
288
+ return {
289
+ original: null,
290
+ local: identifier,
291
+ type: 'global'
292
+ };
293
+ };
294
+
295
+ const scopeHasLocalReference = (scope, referenceName) => {
296
+ const references = collectReferences(scope);
297
+ return (// referenceName was found as a local variable or function declaration.
298
+ references.locals.has(referenceName) || // referenceName was found as an imported identifier
299
+ references.imports.has(referenceName) || // referenceName was not found as an unresolved reference,
300
+ // meaning it is likely not an implicit global reference.
301
+ !references.unresolved.has(referenceName)
302
+ );
303
+ };
304
+
305
+ exports.scopeHasLocalReference = scopeHasLocalReference;