@depup/eslint-plugin-jest 29.15.1-depup.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/LICENSE +21 -0
- package/README.md +31 -0
- package/changes.json +10 -0
- package/docs/rules/consistent-test-it.md +96 -0
- package/docs/rules/expect-expect.md +152 -0
- package/docs/rules/max-expects.md +78 -0
- package/docs/rules/max-nested-describe.md +134 -0
- package/docs/rules/no-alias-methods.md +57 -0
- package/docs/rules/no-commented-out-tests.md +68 -0
- package/docs/rules/no-conditional-expect.md +142 -0
- package/docs/rules/no-conditional-in-test.md +123 -0
- package/docs/rules/no-confusing-set-timeout.md +64 -0
- package/docs/rules/no-deprecated-functions.md +59 -0
- package/docs/rules/no-disabled-tests.md +72 -0
- package/docs/rules/no-done-callback.md +100 -0
- package/docs/rules/no-duplicate-hooks.md +79 -0
- package/docs/rules/no-error-equal.md +37 -0
- package/docs/rules/no-export.md +52 -0
- package/docs/rules/no-focused-tests.md +67 -0
- package/docs/rules/no-hooks.md +178 -0
- package/docs/rules/no-identical-title.md +59 -0
- package/docs/rules/no-interpolation-in-snapshots.md +67 -0
- package/docs/rules/no-jasmine-globals.md +69 -0
- package/docs/rules/no-large-snapshots.md +180 -0
- package/docs/rules/no-mocks-import.md +34 -0
- package/docs/rules/no-restricted-jest-methods.md +53 -0
- package/docs/rules/no-restricted-matchers.md +64 -0
- package/docs/rules/no-standalone-expect.md +106 -0
- package/docs/rules/no-test-prefixes.md +42 -0
- package/docs/rules/no-test-return-statement.md +50 -0
- package/docs/rules/no-unnecessary-assertion.md +41 -0
- package/docs/rules/no-unneeded-async-expect-function.md +41 -0
- package/docs/rules/no-untyped-mock-factory.md +74 -0
- package/docs/rules/padding-around-after-all-blocks.md +34 -0
- package/docs/rules/padding-around-after-each-blocks.md +38 -0
- package/docs/rules/padding-around-all.md +20 -0
- package/docs/rules/padding-around-before-all-blocks.md +37 -0
- package/docs/rules/padding-around-before-each-blocks.md +38 -0
- package/docs/rules/padding-around-describe-blocks.md +42 -0
- package/docs/rules/padding-around-expect-groups.md +44 -0
- package/docs/rules/padding-around-test-blocks.md +48 -0
- package/docs/rules/prefer-called-with.md +39 -0
- package/docs/rules/prefer-comparison-matcher.md +62 -0
- package/docs/rules/prefer-each.md +58 -0
- package/docs/rules/prefer-ending-with-an-expect.md +170 -0
- package/docs/rules/prefer-equality-matcher.md +36 -0
- package/docs/rules/prefer-expect-assertions.md +230 -0
- package/docs/rules/prefer-expect-resolves.md +62 -0
- package/docs/rules/prefer-hooks-in-order.md +137 -0
- package/docs/rules/prefer-hooks-on-top.md +124 -0
- package/docs/rules/prefer-importing-jest-globals.md +85 -0
- package/docs/rules/prefer-jest-mocked.md +40 -0
- package/docs/rules/prefer-lowercase-title.md +128 -0
- package/docs/rules/prefer-mock-promise-shorthand.md +41 -0
- package/docs/rules/prefer-mock-return-shorthand.md +50 -0
- package/docs/rules/prefer-snapshot-hint.md +192 -0
- package/docs/rules/prefer-spy-on.md +46 -0
- package/docs/rules/prefer-strict-equal.md +29 -0
- package/docs/rules/prefer-to-be.md +63 -0
- package/docs/rules/prefer-to-contain.md +53 -0
- package/docs/rules/prefer-to-have-been-called-times.md +43 -0
- package/docs/rules/prefer-to-have-been-called.md +35 -0
- package/docs/rules/prefer-to-have-length.md +41 -0
- package/docs/rules/prefer-todo.md +33 -0
- package/docs/rules/require-hook.md +187 -0
- package/docs/rules/require-to-throw-message.md +43 -0
- package/docs/rules/require-top-level-describe.md +86 -0
- package/docs/rules/unbound-method.md +61 -0
- package/docs/rules/valid-describe-callback.md +69 -0
- package/docs/rules/valid-expect-in-promise.md +79 -0
- package/docs/rules/valid-expect-with-promise.md +43 -0
- package/docs/rules/valid-expect.md +155 -0
- package/docs/rules/valid-mock-module-path.md +72 -0
- package/docs/rules/valid-title.md +319 -0
- package/index.d.ts +28 -0
- package/lib/globals.json +15 -0
- package/lib/index.js +85 -0
- package/lib/rules/consistent-test-it.js +101 -0
- package/lib/rules/expect-expect.js +102 -0
- package/lib/rules/max-expects.js +70 -0
- package/lib/rules/max-nested-describe.js +59 -0
- package/lib/rules/no-alias-methods.js +62 -0
- package/lib/rules/no-commented-out-tests.js +44 -0
- package/lib/rules/no-conditional-expect.js +80 -0
- package/lib/rules/no-conditional-in-test.js +63 -0
- package/lib/rules/no-confusing-set-timeout.js +61 -0
- package/lib/rules/no-deprecated-functions.js +81 -0
- package/lib/rules/no-disabled-tests.js +57 -0
- package/lib/rules/no-done-callback.js +120 -0
- package/lib/rules/no-duplicate-hooks.js +52 -0
- package/lib/rules/no-error-equal.js +41 -0
- package/lib/rules/no-export.js +61 -0
- package/lib/rules/no-focused-tests.js +63 -0
- package/lib/rules/no-hooks.js +51 -0
- package/lib/rules/no-identical-title.js +73 -0
- package/lib/rules/no-interpolation-in-snapshots.js +43 -0
- package/lib/rules/no-jasmine-globals.js +140 -0
- package/lib/rules/no-large-snapshots.js +100 -0
- package/lib/rules/no-mocks-import.js +46 -0
- package/lib/rules/no-restricted-jest-methods.js +53 -0
- package/lib/rules/no-restricted-matchers.js +60 -0
- package/lib/rules/no-standalone-expect.js +114 -0
- package/lib/rules/no-test-prefixes.js +48 -0
- package/lib/rules/no-test-return-statement.js +62 -0
- package/lib/rules/no-unnecessary-assertion.js +87 -0
- package/lib/rules/no-unneeded-async-expect-function.js +57 -0
- package/lib/rules/no-untyped-mock-factory.js +61 -0
- package/lib/rules/padding-around-after-all-blocks.js +17 -0
- package/lib/rules/padding-around-after-each-blocks.js +17 -0
- package/lib/rules/padding-around-all.js +15 -0
- package/lib/rules/padding-around-before-all-blocks.js +17 -0
- package/lib/rules/padding-around-before-each-blocks.js +17 -0
- package/lib/rules/padding-around-describe-blocks.js +17 -0
- package/lib/rules/padding-around-expect-groups.js +21 -0
- package/lib/rules/padding-around-test-blocks.js +17 -0
- package/lib/rules/prefer-called-with.js +47 -0
- package/lib/rules/prefer-comparison-matcher.js +111 -0
- package/lib/rules/prefer-each.js +77 -0
- package/lib/rules/prefer-ending-with-an-expect.js +102 -0
- package/lib/rules/prefer-equality-matcher.js +87 -0
- package/lib/rules/prefer-expect-assertions.js +237 -0
- package/lib/rules/prefer-expect-resolves.js +47 -0
- package/lib/rules/prefer-hooks-in-order.js +67 -0
- package/lib/rules/prefer-hooks-on-top.js +41 -0
- package/lib/rules/prefer-importing-jest-globals.js +126 -0
- package/lib/rules/prefer-jest-mocked.js +66 -0
- package/lib/rules/prefer-lowercase-title.js +128 -0
- package/lib/rules/prefer-mock-promise-shorthand.js +88 -0
- package/lib/rules/prefer-mock-return-shorthand.js +122 -0
- package/lib/rules/prefer-snapshot-hint.js +111 -0
- package/lib/rules/prefer-spy-on.js +80 -0
- package/lib/rules/prefer-strict-equal.js +46 -0
- package/lib/rules/prefer-to-be.js +98 -0
- package/lib/rules/prefer-to-contain.js +82 -0
- package/lib/rules/prefer-to-have-been-called-times.js +70 -0
- package/lib/rules/prefer-to-have-been-called.js +59 -0
- package/lib/rules/prefer-to-have-length.js +57 -0
- package/lib/rules/prefer-todo.js +72 -0
- package/lib/rules/require-hook.js +93 -0
- package/lib/rules/require-to-throw-message.js +45 -0
- package/lib/rules/require-top-level-describe.js +86 -0
- package/lib/rules/unbound-method.js +86 -0
- package/lib/rules/utils/accessors.js +127 -0
- package/lib/rules/utils/ast-utils.js +59 -0
- package/lib/rules/utils/detectJestVersion.js +24 -0
- package/lib/rules/utils/followTypeAssertionChain.js +10 -0
- package/lib/rules/utils/index.js +71 -0
- package/lib/rules/utils/misc.js +168 -0
- package/lib/rules/utils/padding.js +308 -0
- package/lib/rules/utils/parseJestFnCall.js +325 -0
- package/lib/rules/utils/ts.js +73 -0
- package/lib/rules/valid-describe-callback.js +94 -0
- package/lib/rules/valid-expect-in-promise.js +311 -0
- package/lib/rules/valid-expect-with-promise.js +54 -0
- package/lib/rules/valid-expect.js +302 -0
- package/lib/rules/valid-mock-module-path.js +90 -0
- package/lib/rules/valid-title.js +252 -0
- package/package.json +164 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _utils = require("@typescript-eslint/utils");
|
|
8
|
+
var _utils2 = require("./utils");
|
|
9
|
+
/*
|
|
10
|
+
* This implementation is ported from from eslint-plugin-jasmine.
|
|
11
|
+
* MIT license, Tom Vincent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Async assertions might be called in Promise
|
|
16
|
+
* methods like `Promise.x(expect1)` or `Promise.x([expect1, expect2])`.
|
|
17
|
+
* If that's the case, Promise node have to be awaited or returned.
|
|
18
|
+
*
|
|
19
|
+
* @Returns CallExpressionNode
|
|
20
|
+
*/
|
|
21
|
+
const getPromiseCallExpressionNode = node => {
|
|
22
|
+
if (node.type === _utils.AST_NODE_TYPES.ArrayExpression && node.parent.type === _utils.AST_NODE_TYPES.CallExpression) {
|
|
23
|
+
node = node.parent;
|
|
24
|
+
}
|
|
25
|
+
if (node.type === _utils.AST_NODE_TYPES.CallExpression && node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.object, 'Promise')) {
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
const findPromiseCallExpressionNode = node => node.parent?.parent && [_utils.AST_NODE_TYPES.CallExpression, _utils.AST_NODE_TYPES.ArrayExpression].includes(node.parent.type) ? getPromiseCallExpressionNode(node.parent) : null;
|
|
31
|
+
const findFirstFunctionExpression = ({
|
|
32
|
+
parent
|
|
33
|
+
}) => {
|
|
34
|
+
if (!parent) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return (0, _utils2.isFunction)(parent) ? parent : findFirstFunctionExpression(parent);
|
|
38
|
+
};
|
|
39
|
+
const getNormalizeFunctionExpression = functionExpression => {
|
|
40
|
+
if (functionExpression.parent.type === _utils.AST_NODE_TYPES.Property && functionExpression.type === _utils.AST_NODE_TYPES.FunctionExpression) {
|
|
41
|
+
return functionExpression.parent;
|
|
42
|
+
}
|
|
43
|
+
return functionExpression;
|
|
44
|
+
};
|
|
45
|
+
const getParentIfThenified = node => {
|
|
46
|
+
const grandParentNode = node.parent?.parent;
|
|
47
|
+
if (grandParentNode && grandParentNode.type === _utils.AST_NODE_TYPES.CallExpression && grandParentNode.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(grandParentNode.callee.property) && ['then', 'catch'].includes((0, _utils2.getAccessorValue)(grandParentNode.callee.property))) {
|
|
48
|
+
// Just in case `then`s are chained look one above.
|
|
49
|
+
return getParentIfThenified(grandParentNode);
|
|
50
|
+
}
|
|
51
|
+
return node;
|
|
52
|
+
};
|
|
53
|
+
const isAcceptableReturnNode = (node, allowReturn) => {
|
|
54
|
+
if (allowReturn && node.type === _utils.AST_NODE_TYPES.ReturnStatement) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (node.type === _utils.AST_NODE_TYPES.ConditionalExpression) {
|
|
58
|
+
return isAcceptableReturnNode(node.parent, allowReturn);
|
|
59
|
+
}
|
|
60
|
+
return [_utils.AST_NODE_TYPES.ArrowFunctionExpression, _utils.AST_NODE_TYPES.AwaitExpression].includes(node.type);
|
|
61
|
+
};
|
|
62
|
+
const promiseArrayExceptionKey = ({
|
|
63
|
+
start,
|
|
64
|
+
end
|
|
65
|
+
}) => `${start.line}:${start.column}-${end.line}:${end.column}`;
|
|
66
|
+
const defaultAsyncMatchers = ['toReject', 'toResolve'];
|
|
67
|
+
var _default = exports.default = (0, _utils2.createRule)({
|
|
68
|
+
name: __filename,
|
|
69
|
+
meta: {
|
|
70
|
+
docs: {
|
|
71
|
+
description: 'Enforce valid `expect()` usage'
|
|
72
|
+
},
|
|
73
|
+
messages: {
|
|
74
|
+
tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}',
|
|
75
|
+
notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}',
|
|
76
|
+
modifierUnknown: 'Expect has an unknown modifier',
|
|
77
|
+
matcherNotFound: 'Expect must have a corresponding matcher call',
|
|
78
|
+
matcherNotCalled: 'Matchers must be called to assert',
|
|
79
|
+
asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}',
|
|
80
|
+
promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}'
|
|
81
|
+
},
|
|
82
|
+
fixable: 'code',
|
|
83
|
+
type: 'suggestion',
|
|
84
|
+
schema: [{
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
alwaysAwait: {
|
|
88
|
+
type: 'boolean',
|
|
89
|
+
default: false
|
|
90
|
+
},
|
|
91
|
+
asyncMatchers: {
|
|
92
|
+
type: 'array',
|
|
93
|
+
items: {
|
|
94
|
+
type: 'string'
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
minArgs: {
|
|
98
|
+
type: 'number',
|
|
99
|
+
minimum: 0
|
|
100
|
+
},
|
|
101
|
+
maxArgs: {
|
|
102
|
+
type: 'number',
|
|
103
|
+
minimum: 1
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: false
|
|
107
|
+
}]
|
|
108
|
+
},
|
|
109
|
+
defaultOptions: [{
|
|
110
|
+
alwaysAwait: false,
|
|
111
|
+
asyncMatchers: defaultAsyncMatchers,
|
|
112
|
+
minArgs: 1,
|
|
113
|
+
maxArgs: 1
|
|
114
|
+
}],
|
|
115
|
+
create(context, [{
|
|
116
|
+
alwaysAwait,
|
|
117
|
+
asyncMatchers = defaultAsyncMatchers,
|
|
118
|
+
minArgs = 1,
|
|
119
|
+
maxArgs = 1
|
|
120
|
+
}]) {
|
|
121
|
+
// Context state
|
|
122
|
+
const arrayExceptions = new Set();
|
|
123
|
+
const descriptors = [];
|
|
124
|
+
const pushPromiseArrayException = loc => arrayExceptions.add(promiseArrayExceptionKey(loc));
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Promise method that accepts an array of promises,
|
|
128
|
+
* (eg. Promise.all), will throw warnings for the each
|
|
129
|
+
* unawaited or non-returned promise. To avoid throwing
|
|
130
|
+
* multiple warnings, we check if there is a warning in
|
|
131
|
+
* the given location.
|
|
132
|
+
*/
|
|
133
|
+
const promiseArrayExceptionExists = loc => arrayExceptions.has(promiseArrayExceptionKey(loc));
|
|
134
|
+
const findTopMostMemberExpression = node => {
|
|
135
|
+
let topMostMemberExpression = node;
|
|
136
|
+
let {
|
|
137
|
+
parent
|
|
138
|
+
} = node;
|
|
139
|
+
while (parent) {
|
|
140
|
+
if (parent.type !== _utils.AST_NODE_TYPES.MemberExpression) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
topMostMemberExpression = parent;
|
|
144
|
+
parent = parent.parent;
|
|
145
|
+
}
|
|
146
|
+
return topMostMemberExpression;
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
CallExpression(node) {
|
|
150
|
+
const jestFnCall = (0, _utils2.parseJestFnCallWithReason)(node, context);
|
|
151
|
+
if (typeof jestFnCall === 'string') {
|
|
152
|
+
const reportingNode = node.parent.type === _utils.AST_NODE_TYPES.MemberExpression ? findTopMostMemberExpression(node.parent).property : node;
|
|
153
|
+
if (jestFnCall === 'matcher-not-found') {
|
|
154
|
+
context.report({
|
|
155
|
+
messageId: 'matcherNotFound',
|
|
156
|
+
node: reportingNode
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (jestFnCall === 'matcher-not-called') {
|
|
161
|
+
context.report({
|
|
162
|
+
messageId: (0, _utils2.isSupportedAccessor)(reportingNode) && _utils2.ModifierName.hasOwnProperty((0, _utils2.getAccessorValue)(reportingNode)) ? 'matcherNotFound' : 'matcherNotCalled',
|
|
163
|
+
node: reportingNode
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (jestFnCall === 'modifier-unknown') {
|
|
167
|
+
context.report({
|
|
168
|
+
messageId: 'modifierUnknown',
|
|
169
|
+
node: reportingNode
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
} else if (jestFnCall?.type !== 'expect') {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const {
|
|
178
|
+
parent: expect
|
|
179
|
+
} = jestFnCall.head.node;
|
|
180
|
+
if (expect.type !== _utils.AST_NODE_TYPES.CallExpression) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (expect.arguments.length < minArgs) {
|
|
184
|
+
const expectLength = (0, _utils2.getAccessorValue)(jestFnCall.head.node).length;
|
|
185
|
+
const loc = {
|
|
186
|
+
start: {
|
|
187
|
+
column: expect.loc.start.column + expectLength,
|
|
188
|
+
line: expect.loc.start.line
|
|
189
|
+
},
|
|
190
|
+
end: {
|
|
191
|
+
column: expect.loc.start.column + expectLength + 1,
|
|
192
|
+
line: expect.loc.start.line
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
context.report({
|
|
196
|
+
messageId: 'notEnoughArgs',
|
|
197
|
+
data: {
|
|
198
|
+
amount: minArgs,
|
|
199
|
+
s: minArgs === 1 ? '' : 's'
|
|
200
|
+
},
|
|
201
|
+
node: expect,
|
|
202
|
+
loc
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (expect.arguments.length > maxArgs) {
|
|
206
|
+
const {
|
|
207
|
+
start
|
|
208
|
+
} = expect.arguments[maxArgs].loc;
|
|
209
|
+
const {
|
|
210
|
+
end
|
|
211
|
+
} = expect.arguments[expect.arguments.length - 1].loc;
|
|
212
|
+
const loc = {
|
|
213
|
+
start,
|
|
214
|
+
end: {
|
|
215
|
+
column: end.column - 1,
|
|
216
|
+
line: end.line
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
context.report({
|
|
220
|
+
messageId: 'tooManyArgs',
|
|
221
|
+
data: {
|
|
222
|
+
amount: maxArgs,
|
|
223
|
+
s: maxArgs === 1 ? '' : 's'
|
|
224
|
+
},
|
|
225
|
+
node: expect,
|
|
226
|
+
loc
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const {
|
|
230
|
+
matcher
|
|
231
|
+
} = jestFnCall;
|
|
232
|
+
const parentNode = matcher.parent.parent;
|
|
233
|
+
const shouldBeAwaited = jestFnCall.modifiers.some(nod => (0, _utils2.getAccessorValue)(nod) !== 'not') || asyncMatchers.includes((0, _utils2.getAccessorValue)(matcher));
|
|
234
|
+
if (!parentNode.parent || !shouldBeAwaited) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* If parent node is an array expression, we'll report the warning,
|
|
239
|
+
* for the array object, not for each individual assertion.
|
|
240
|
+
*/
|
|
241
|
+
const isParentArrayExpression = parentNode.parent.type === _utils.AST_NODE_TYPES.ArrayExpression;
|
|
242
|
+
/**
|
|
243
|
+
* An async assertion can be chained with `then` or `catch` statements.
|
|
244
|
+
* In that case our target CallExpression node is the one with
|
|
245
|
+
* the last `then` or `catch` statement.
|
|
246
|
+
*/
|
|
247
|
+
const targetNode = getParentIfThenified(parentNode);
|
|
248
|
+
const finalNode = findPromiseCallExpressionNode(targetNode) || targetNode;
|
|
249
|
+
if (finalNode.parent &&
|
|
250
|
+
// If node is not awaited or returned
|
|
251
|
+
!isAcceptableReturnNode(finalNode.parent, !alwaysAwait) &&
|
|
252
|
+
// if we didn't warn user already
|
|
253
|
+
!promiseArrayExceptionExists(finalNode.loc)) {
|
|
254
|
+
descriptors.push({
|
|
255
|
+
node: finalNode,
|
|
256
|
+
messageId: targetNode === finalNode ? 'asyncMustBeAwaited' : 'promisesWithAsyncAssertionsMustBeAwaited'
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (isParentArrayExpression) {
|
|
260
|
+
pushPromiseArrayException(finalNode.loc);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
'Program:exit'() {
|
|
264
|
+
const fixes = [];
|
|
265
|
+
descriptors.forEach(({
|
|
266
|
+
node,
|
|
267
|
+
messageId
|
|
268
|
+
}, index) => {
|
|
269
|
+
const orReturned = alwaysAwait ? '' : ' or returned';
|
|
270
|
+
context.report({
|
|
271
|
+
loc: node.loc,
|
|
272
|
+
data: {
|
|
273
|
+
orReturned
|
|
274
|
+
},
|
|
275
|
+
messageId,
|
|
276
|
+
node,
|
|
277
|
+
fix(fixer) {
|
|
278
|
+
const functionExpression = findFirstFunctionExpression(node);
|
|
279
|
+
if (!functionExpression) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const foundAsyncFixer = fixes.some(fix => fix.text === 'async ');
|
|
283
|
+
if (!functionExpression.async && !foundAsyncFixer) {
|
|
284
|
+
const targetFunction = getNormalizeFunctionExpression(functionExpression);
|
|
285
|
+
fixes.push(fixer.insertTextBefore(targetFunction, 'async '));
|
|
286
|
+
}
|
|
287
|
+
const returnStatement = node.parent?.type === _utils.AST_NODE_TYPES.ReturnStatement ? node.parent : null;
|
|
288
|
+
if (alwaysAwait && returnStatement) {
|
|
289
|
+
const sourceCodeText = context.sourceCode.getText(returnStatement);
|
|
290
|
+
const replacedText = sourceCodeText.replace('return', 'await');
|
|
291
|
+
fixes.push(fixer.replaceText(returnStatement, replacedText));
|
|
292
|
+
} else {
|
|
293
|
+
fixes.push(fixer.insertTextBefore(node, 'await '));
|
|
294
|
+
}
|
|
295
|
+
return index === descriptors.length - 1 ? fixes : null;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _fs = require("fs");
|
|
8
|
+
var _path = _interopRequireDefault(require("path"));
|
|
9
|
+
var _utils = require("@typescript-eslint/utils");
|
|
10
|
+
var _utils2 = require("./utils");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
var _default = exports.default = (0, _utils2.createRule)({
|
|
13
|
+
name: __filename,
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow mocking of non-existing module paths'
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
invalidMockModulePath: 'Module path {{ moduleName }} does not exist or is not exported'
|
|
21
|
+
},
|
|
22
|
+
schema: [{
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
moduleFileExtensions: {
|
|
26
|
+
type: 'array',
|
|
27
|
+
items: {
|
|
28
|
+
type: 'string'
|
|
29
|
+
},
|
|
30
|
+
additionalItems: false
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
additionalProperties: false
|
|
34
|
+
}]
|
|
35
|
+
},
|
|
36
|
+
defaultOptions: [{
|
|
37
|
+
moduleFileExtensions: ['.js', '.ts', '.tsx', '.jsx', '.json']
|
|
38
|
+
}],
|
|
39
|
+
create(context, [{
|
|
40
|
+
moduleFileExtensions = ['.js', '.ts', '.tsx', '.jsx', '.json']
|
|
41
|
+
}]) {
|
|
42
|
+
return {
|
|
43
|
+
CallExpression(node) {
|
|
44
|
+
if (node.callee.type !== _utils.AST_NODE_TYPES.MemberExpression) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!node.arguments.length || !(0, _utils2.isTypeOfJestFnCall)(node, context, ['jest']) || !((0, _utils2.isSupportedAccessor)(node.callee.property) && ['mock', 'doMock'].includes((0, _utils2.getAccessorValue)(node.callee.property)))) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const moduleName = (0, _utils2.findModuleName)(node.arguments[0]);
|
|
51
|
+
if (!moduleName) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
if (!moduleName.value.startsWith('.')) {
|
|
56
|
+
require.resolve(moduleName.value);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const resolvedModulePath = _path.default.resolve(_path.default.dirname(context.filename), moduleName.value);
|
|
60
|
+
const hasPossiblyModulePaths = ['', ...moduleFileExtensions].some(ext => {
|
|
61
|
+
try {
|
|
62
|
+
(0, _fs.statSync)(`${resolvedModulePath}${ext}`);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (hasPossiblyModulePaths) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const castedErr = err;
|
|
73
|
+
|
|
74
|
+
// Reports unexpected issues when attempt to verify mocked module path.
|
|
75
|
+
// The list of possible errors is non-exhaustive.
|
|
76
|
+
if (castedErr.code !== 'MODULE_NOT_FOUND' && castedErr.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
77
|
+
throw new Error(`Error when trying to validate mock module path from \`jest.mock\`: ${err}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
context.report({
|
|
81
|
+
messageId: 'invalidMockModulePath',
|
|
82
|
+
data: {
|
|
83
|
+
moduleName: moduleName.raw
|
|
84
|
+
},
|
|
85
|
+
node
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _utils = require("@typescript-eslint/utils");
|
|
8
|
+
var _utils2 = require("./utils");
|
|
9
|
+
const trimFXprefix = word => ['f', 'x'].includes(word.charAt(0)) ? word.substring(1) : word;
|
|
10
|
+
const doesBinaryExpressionContainStringNode = binaryExp => {
|
|
11
|
+
if ((0, _utils2.isStringNode)(binaryExp.right)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (binaryExp.left.type === _utils.AST_NODE_TYPES.BinaryExpression) {
|
|
15
|
+
return doesBinaryExpressionContainStringNode(binaryExp.left);
|
|
16
|
+
}
|
|
17
|
+
return (0, _utils2.isStringNode)(binaryExp.left);
|
|
18
|
+
};
|
|
19
|
+
const quoteStringValue = node => node.type === _utils.AST_NODE_TYPES.TemplateLiteral ? `\`${node.quasis[0].value.raw}\`` : node.raw;
|
|
20
|
+
const compileMatcherPattern = matcherMaybeWithMessage => {
|
|
21
|
+
const [matcher, message] = Array.isArray(matcherMaybeWithMessage) ? matcherMaybeWithMessage : [matcherMaybeWithMessage];
|
|
22
|
+
return [new RegExp(matcher, 'u'), message];
|
|
23
|
+
};
|
|
24
|
+
const compileMatcherPatterns = matchers => {
|
|
25
|
+
if (typeof matchers === 'string' || Array.isArray(matchers)) {
|
|
26
|
+
const compiledMatcher = compileMatcherPattern(matchers);
|
|
27
|
+
return {
|
|
28
|
+
describe: compiledMatcher,
|
|
29
|
+
test: compiledMatcher,
|
|
30
|
+
it: compiledMatcher
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
describe: matchers.describe ? compileMatcherPattern(matchers.describe) : null,
|
|
35
|
+
test: matchers.test ? compileMatcherPattern(matchers.test) : null,
|
|
36
|
+
it: matchers.it ? compileMatcherPattern(matchers.it) : null
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// %p - pretty-format.
|
|
41
|
+
// %s- String.
|
|
42
|
+
// %d- Number.
|
|
43
|
+
// %i - Integer.
|
|
44
|
+
// %f - Floating point value.
|
|
45
|
+
// %j - JSON.
|
|
46
|
+
// %o - Object.
|
|
47
|
+
// %# - Index of the test case.
|
|
48
|
+
// %$ - Number of the test case.
|
|
49
|
+
// %% - single percent sign ('%'). This does not consume an argument.
|
|
50
|
+
|
|
51
|
+
// const re = /%([^psdifjo#$%])/u;
|
|
52
|
+
|
|
53
|
+
const MatcherAndMessageSchema = {
|
|
54
|
+
type: 'array',
|
|
55
|
+
items: {
|
|
56
|
+
type: 'string'
|
|
57
|
+
},
|
|
58
|
+
minItems: 1,
|
|
59
|
+
maxItems: 2,
|
|
60
|
+
additionalItems: false
|
|
61
|
+
};
|
|
62
|
+
var _default = exports.default = (0, _utils2.createRule)({
|
|
63
|
+
name: __filename,
|
|
64
|
+
meta: {
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Enforce valid titles'
|
|
67
|
+
},
|
|
68
|
+
messages: {
|
|
69
|
+
titleMustBeString: 'Title must be a string',
|
|
70
|
+
emptyTitle: '{{ jestFunctionName }} should not have an empty title',
|
|
71
|
+
duplicatePrefix: 'should not have duplicate prefix',
|
|
72
|
+
accidentalSpace: 'should not have leading or trailing spaces',
|
|
73
|
+
disallowedWord: '"{{ word }}" is not allowed in test titles',
|
|
74
|
+
mustNotMatch: '{{ jestFunctionName }} should not match {{ pattern }}',
|
|
75
|
+
mustMatch: '{{ jestFunctionName }} should match {{ pattern }}',
|
|
76
|
+
mustNotMatchCustom: '{{ message }}',
|
|
77
|
+
mustMatchCustom: '{{ message }}',
|
|
78
|
+
invalidEachSpecifier: '"{{ specifier }}" is not a valid format specifier'
|
|
79
|
+
},
|
|
80
|
+
type: 'suggestion',
|
|
81
|
+
schema: [{
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
ignoreSpaces: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: false
|
|
87
|
+
},
|
|
88
|
+
ignoreTypeOfDescribeName: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
default: false
|
|
91
|
+
},
|
|
92
|
+
ignoreTypeOfTestName: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
default: false
|
|
95
|
+
},
|
|
96
|
+
disallowedWords: {
|
|
97
|
+
type: 'array',
|
|
98
|
+
items: {
|
|
99
|
+
type: 'string'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
patternProperties: {
|
|
104
|
+
[/^must(?:Not)?Match$/u.source]: {
|
|
105
|
+
oneOf: [{
|
|
106
|
+
type: 'string'
|
|
107
|
+
}, MatcherAndMessageSchema, {
|
|
108
|
+
type: 'object',
|
|
109
|
+
// @ts-expect-error https://github.com/eslint/eslint/discussions/17573
|
|
110
|
+
propertyNames: {
|
|
111
|
+
enum: ['describe', 'test', 'it']
|
|
112
|
+
},
|
|
113
|
+
additionalProperties: {
|
|
114
|
+
oneOf: [{
|
|
115
|
+
type: 'string'
|
|
116
|
+
}, MatcherAndMessageSchema]
|
|
117
|
+
}
|
|
118
|
+
}]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
additionalProperties: false
|
|
122
|
+
}],
|
|
123
|
+
fixable: 'code'
|
|
124
|
+
},
|
|
125
|
+
defaultOptions: [{
|
|
126
|
+
ignoreSpaces: false,
|
|
127
|
+
ignoreTypeOfDescribeName: false,
|
|
128
|
+
ignoreTypeOfTestName: false,
|
|
129
|
+
disallowedWords: []
|
|
130
|
+
}],
|
|
131
|
+
create(context, [{
|
|
132
|
+
ignoreSpaces,
|
|
133
|
+
ignoreTypeOfDescribeName,
|
|
134
|
+
ignoreTypeOfTestName,
|
|
135
|
+
disallowedWords = [],
|
|
136
|
+
mustNotMatch,
|
|
137
|
+
mustMatch
|
|
138
|
+
}]) {
|
|
139
|
+
const disallowedWordsRegexp = new RegExp(`\\b(${disallowedWords.join('|')})\\b`, 'iu');
|
|
140
|
+
const mustNotMatchPatterns = compileMatcherPatterns(mustNotMatch ?? {});
|
|
141
|
+
const mustMatchPatterns = compileMatcherPatterns(mustMatch ?? {});
|
|
142
|
+
return {
|
|
143
|
+
CallExpression(node) {
|
|
144
|
+
const jestFnCall = (0, _utils2.parseJestFnCall)(node, context);
|
|
145
|
+
if (jestFnCall?.type !== 'describe' && jestFnCall?.type !== 'test') {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const [argument] = node.arguments;
|
|
149
|
+
if (!argument) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!(0, _utils2.isStringNode)(argument)) {
|
|
153
|
+
if (argument.type === _utils.AST_NODE_TYPES.BinaryExpression && doesBinaryExpressionContainStringNode(argument)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!(jestFnCall.type === 'describe' && ignoreTypeOfDescribeName || jestFnCall.type === 'test' && ignoreTypeOfTestName) && argument.type !== _utils.AST_NODE_TYPES.TemplateLiteral) {
|
|
157
|
+
context.report({
|
|
158
|
+
messageId: 'titleMustBeString',
|
|
159
|
+
loc: argument.loc
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const title = (0, _utils2.getStringValue)(argument);
|
|
165
|
+
if (!title) {
|
|
166
|
+
context.report({
|
|
167
|
+
messageId: 'emptyTitle',
|
|
168
|
+
data: {
|
|
169
|
+
jestFunctionName: jestFnCall.type === 'describe' ? _utils2.DescribeAlias.describe : _utils2.TestCaseName.test
|
|
170
|
+
},
|
|
171
|
+
node
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// check array-based .each titles for invalid printf specifiers
|
|
177
|
+
if (jestFnCall.members.find(s => (0, _utils2.isSupportedAccessor)(s, 'each'))?.parent.parent.type === _utils.AST_NODE_TYPES.CallExpression) {
|
|
178
|
+
const [unknownSpecifier] = /%[^psdifjo#$%]/u.exec(title.replaceAll('%%', '')) ?? [];
|
|
179
|
+
if (unknownSpecifier) {
|
|
180
|
+
context.report({
|
|
181
|
+
messageId: 'invalidEachSpecifier',
|
|
182
|
+
data: {
|
|
183
|
+
specifier: unknownSpecifier
|
|
184
|
+
},
|
|
185
|
+
node: argument
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (disallowedWords.length > 0) {
|
|
190
|
+
const disallowedMatch = disallowedWordsRegexp.exec(title);
|
|
191
|
+
if (disallowedMatch) {
|
|
192
|
+
context.report({
|
|
193
|
+
data: {
|
|
194
|
+
word: disallowedMatch[1]
|
|
195
|
+
},
|
|
196
|
+
messageId: 'disallowedWord',
|
|
197
|
+
node: argument
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (ignoreSpaces === false && title.trim().length !== title.length) {
|
|
203
|
+
context.report({
|
|
204
|
+
messageId: 'accidentalSpace',
|
|
205
|
+
node: argument,
|
|
206
|
+
fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]) +?/u, '$1').replace(/ +?([`'"])$/u, '$1'))]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const unprefixedName = trimFXprefix(jestFnCall.name);
|
|
210
|
+
const [firstWord] = title.split(' ');
|
|
211
|
+
if (firstWord.toLowerCase() === unprefixedName) {
|
|
212
|
+
context.report({
|
|
213
|
+
messageId: 'duplicatePrefix',
|
|
214
|
+
node: argument,
|
|
215
|
+
fix: fixer => [fixer.replaceTextRange(argument.range, quoteStringValue(argument).replace(/^([`'"]).+? /u, '$1'))]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const jestFunctionName = unprefixedName;
|
|
219
|
+
const [mustNotMatchPattern, mustNotMatchMessage] = mustNotMatchPatterns[jestFunctionName] ?? [];
|
|
220
|
+
if (mustNotMatchPattern) {
|
|
221
|
+
if (mustNotMatchPattern.test(title)) {
|
|
222
|
+
context.report({
|
|
223
|
+
messageId: mustNotMatchMessage ? 'mustNotMatchCustom' : 'mustNotMatch',
|
|
224
|
+
node: argument,
|
|
225
|
+
data: {
|
|
226
|
+
jestFunctionName,
|
|
227
|
+
pattern: mustNotMatchPattern,
|
|
228
|
+
message: mustNotMatchMessage
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const [mustMatchPattern, mustMatchMessage] = mustMatchPatterns[jestFunctionName] ?? [];
|
|
235
|
+
if (mustMatchPattern) {
|
|
236
|
+
if (!mustMatchPattern.test(title)) {
|
|
237
|
+
context.report({
|
|
238
|
+
messageId: mustMatchMessage ? 'mustMatchCustom' : 'mustMatch',
|
|
239
|
+
node: argument,
|
|
240
|
+
data: {
|
|
241
|
+
jestFunctionName,
|
|
242
|
+
pattern: mustMatchPattern,
|
|
243
|
+
message: mustMatchMessage
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|