@cjser/eslint-node-test 0.2.0-cjser.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.
- package/dist-cjser/index.cjs +8561 -0
- package/index.d.ts +11 -0
- package/index.js +49 -0
- package/license +9 -0
- package/package.json +141 -0
- package/readme.md +172 -0
- package/rules/assertion-arguments.js +147 -0
- package/rules/ast/call-or-new-expression.js +94 -0
- package/rules/ast/function-types.js +7 -0
- package/rules/ast/index.js +17 -0
- package/rules/ast/is-expression-statement.js +7 -0
- package/rules/ast/is-function.js +5 -0
- package/rules/ast/is-loop.js +5 -0
- package/rules/ast/is-member-expression.js +92 -0
- package/rules/ast/is-method-call.js +62 -0
- package/rules/ast/literal.js +32 -0
- package/rules/ast/loop-types.js +9 -0
- package/rules/consistent-assert-style.js +169 -0
- package/rules/consistent-assert-throws-callback-style.js +241 -0
- package/rules/consistent-modifier-style.js +95 -0
- package/rules/consistent-test-context-name.js +75 -0
- package/rules/consistent-test-filename.js +70 -0
- package/rules/consistent-test-it.js +84 -0
- package/rules/fix/index.js +5 -0
- package/rules/fix/remove-argument.js +58 -0
- package/rules/fix/replace-member-expression-property.js +25 -0
- package/rules/hooks-order.js +147 -0
- package/rules/index.js +84 -0
- package/rules/max-assertions.js +88 -0
- package/rules/max-nested-describe.js +70 -0
- package/rules/no-assert-in-describe.js +61 -0
- package/rules/no-assert-in-hook.js +61 -0
- package/rules/no-assert-match-string.js +166 -0
- package/rules/no-assert-throws-async.js +113 -0
- package/rules/no-assert-throws-call.js +106 -0
- package/rules/no-assert-throws-multiple-statements.js +194 -0
- package/rules/no-assert-throws-string.js +78 -0
- package/rules/no-async-describe.js +50 -0
- package/rules/no-async-fn-without-await.js +74 -0
- package/rules/no-callback-and-promise.js +56 -0
- package/rules/no-commented-tests.js +64 -0
- package/rules/no-compound-assertion.js +131 -0
- package/rules/no-conditional-assertion.js +108 -0
- package/rules/no-conditional-in-test.js +66 -0
- package/rules/no-conditional-tests.js +75 -0
- package/rules/no-conflicting-modifiers.js +73 -0
- package/rules/no-constant-assertion.js +218 -0
- package/rules/no-done-callback.js +58 -0
- package/rules/no-duplicate-assertions.js +172 -0
- package/rules/no-duplicate-hooks.js +75 -0
- package/rules/no-duplicate-plan.js +209 -0
- package/rules/no-export.js +45 -0
- package/rules/no-identical-assertion-arguments.js +84 -0
- package/rules/no-identical-title.js +101 -0
- package/rules/no-incorrect-deep-equal.js +113 -0
- package/rules/no-incorrect-strict-equal.js +99 -0
- package/rules/no-loop-static-title.js +93 -0
- package/rules/no-misused-concurrency.js +85 -0
- package/rules/no-mock-timers-destructured-import.js +150 -0
- package/rules/no-nested-tests.js +71 -0
- package/rules/no-only-test.js +11 -0
- package/rules/no-parent-test-context.js +244 -0
- package/rules/no-process-env-mutation.js +495 -0
- package/rules/no-process-exit-in-test.js +96 -0
- package/rules/no-skip-test.js +11 -0
- package/rules/no-skip-without-reason.js +90 -0
- package/rules/no-skip-without-return.js +123 -0
- package/rules/no-sleep-in-test.js +482 -0
- package/rules/no-snapshot-in-loop.js +132 -0
- package/rules/no-standalone-assert.js +51 -0
- package/rules/no-test-inside-hook.js +68 -0
- package/rules/no-test-return-statement.js +217 -0
- package/rules/no-todo-test.js +11 -0
- package/rules/no-unawaited-promise-assertion.js +366 -0
- package/rules/no-unawaited-rejects.js +87 -0
- package/rules/no-unawaited-subtest.js +66 -0
- package/rules/no-unknown-test-options.js +77 -0
- package/rules/no-useless-assertion.js +60 -0
- package/rules/prefer-assert-match.js +253 -0
- package/rules/prefer-assert-throws.js +97 -0
- package/rules/prefer-async-await.js +203 -0
- package/rules/prefer-context-mock.js +55 -0
- package/rules/prefer-diagnostic.js +94 -0
- package/rules/prefer-equality-assertion.js +124 -0
- package/rules/prefer-hooks-on-top.js +75 -0
- package/rules/prefer-lowercase-title.js +119 -0
- package/rules/prefer-mock-method.js +115 -0
- package/rules/prefer-strict-assert.js +82 -0
- package/rules/prefer-test-context-assert.js +162 -0
- package/rules/prefer-todo.js +98 -0
- package/rules/require-assertion.js +99 -0
- package/rules/require-await-concurrent-subtests.js +119 -0
- package/rules/require-context-assert-with-plan.js +127 -0
- package/rules/require-hook.js +108 -0
- package/rules/require-mock-timers-advance.js +313 -0
- package/rules/require-mock-timers-apis.js +383 -0
- package/rules/require-throws-expectation.js +65 -0
- package/rules/require-throws-validator-return-true.js +156 -0
- package/rules/require-top-level-describe.js +89 -0
- package/rules/rule/index.js +9 -0
- package/rules/rule/to-eslint-create.js +35 -0
- package/rules/rule/to-eslint-listener.js +52 -0
- package/rules/rule/to-eslint-problem.js +36 -0
- package/rules/rule/to-eslint-rule-fixer.js +47 -0
- package/rules/rule/to-eslint-rule.js +36 -0
- package/rules/rule/to-eslint-rules.js +8 -0
- package/rules/rule/unicorn-context.js +34 -0
- package/rules/rule/unicorn-listeners.js +56 -0
- package/rules/rule/utilities.js +26 -0
- package/rules/shared/test-modifier-rule.js +92 -0
- package/rules/test-title-format.js +86 -0
- package/rules/test-title.js +139 -0
- package/rules/utils/contains-suspension-point.js +35 -0
- package/rules/utils/get-comments.js +15 -0
- package/rules/utils/get-documentation-url.js +9 -0
- package/rules/utils/get-enclosing-function.js +18 -0
- package/rules/utils/index.js +15 -0
- package/rules/utils/is-conditional-branch.js +37 -0
- package/rules/utils/is-promise-type.js +26 -0
- package/rules/utils/is-same-reference.js +179 -0
- package/rules/utils/is-value-not-usable.js +5 -0
- package/rules/utils/node-test.js +883 -0
- package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +78 -0
- package/rules/utils/parentheses/iterate-surrounding-parentheses.js +80 -0
- package/rules/utils/parentheses/parentheses.js +69 -0
- package/rules/utils/types.js +5 -0
- package/rules/utils/unwrap-typescript-expression.js +16 -0
- package/rules/valid-describe-callback.js +63 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
resolveImports,
|
|
4
|
+
parseTestCall,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
getSubtestReceiver,
|
|
7
|
+
HOOK_FUNCTIONS,
|
|
8
|
+
isGlobalMock,
|
|
9
|
+
MODIFIERS,
|
|
10
|
+
} from './utils/node-test.js';
|
|
11
|
+
import {getEnclosingFunction} from './utils/index.js';
|
|
12
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
13
|
+
|
|
14
|
+
const MESSAGE_ID = 'require-mock-timers-advance';
|
|
15
|
+
const messages = {
|
|
16
|
+
[MESSAGE_ID]: '`mock.timers.enable()` must be followed by `tick()` or `runAll()` so mocked timer callbacks can run.',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ADVANCE_METHODS = new Set(['tick', 'runAll']);
|
|
20
|
+
|
|
21
|
+
function getStaticPropertyName(node) {
|
|
22
|
+
if (node.type === 'Identifier') {
|
|
23
|
+
return node.name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
27
|
+
return node.value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getPropertyName(property) {
|
|
32
|
+
if (property.type !== 'Property') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!property.computed) {
|
|
37
|
+
return getStaticPropertyName(property.key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (property.key.type === 'Literal' && typeof property.key.value === 'string') {
|
|
41
|
+
return property.key.value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDynamicProperty(property) {
|
|
46
|
+
return property.type === 'Property' && property.computed && property.key.type !== 'Literal';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mayEnableTimerApis(callExpression) {
|
|
50
|
+
const argument = unwrapTypeScriptExpression(callExpression.arguments[0]);
|
|
51
|
+
if (argument?.type !== 'ObjectExpression') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let apisValue;
|
|
56
|
+
for (const property of argument.properties) {
|
|
57
|
+
if (property.type === 'SpreadElement') {
|
|
58
|
+
if (apisValue) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (getPropertyName(property) === 'apis') {
|
|
66
|
+
apisValue = property.value;
|
|
67
|
+
} else if (apisValue && isDynamicProperty(property)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!apisValue) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const unwrappedApisValue = unwrapTypeScriptExpression(apisValue);
|
|
77
|
+
if (unwrappedApisValue.type !== 'ArrayExpression') {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const rawElement of unwrappedApisValue.elements) {
|
|
82
|
+
const element = rawElement && unwrapTypeScriptExpression(rawElement);
|
|
83
|
+
if (!element) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (element.value === 'Date') {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isImportedIdentifier(node, sourceCode) {
|
|
102
|
+
const variable = findVariable(sourceCode.getScope(node), node);
|
|
103
|
+
return variable?.defs.some(({type}) => type === 'ImportBinding') ?? false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMockTimersReceiverKey(node, imports, sourceCode, contextVariables) {
|
|
107
|
+
if (
|
|
108
|
+
node.type !== 'MemberExpression'
|
|
109
|
+
|| node.computed
|
|
110
|
+
|| node.optional
|
|
111
|
+
|| getStaticPropertyName(node.property) !== 'timers'
|
|
112
|
+
) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isGlobalMock(node.object, imports)) {
|
|
117
|
+
return 'global';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
node.object.type === 'MemberExpression'
|
|
122
|
+
&& !node.object.computed
|
|
123
|
+
&& !node.object.optional
|
|
124
|
+
&& getStaticPropertyName(node.object.property) === 'mock'
|
|
125
|
+
&& node.object.object.type === 'Identifier'
|
|
126
|
+
) {
|
|
127
|
+
const variable = findVariable(sourceCode.getScope(node.object.object), node.object.object);
|
|
128
|
+
if (!variable || !contextVariables.includes(variable)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `context:${node.object.object.name}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getMockTimersCall(callExpression, imports, sourceCode, contextVariables) {
|
|
137
|
+
const {callee} = callExpression;
|
|
138
|
+
if (
|
|
139
|
+
callee.type !== 'MemberExpression'
|
|
140
|
+
|| callee.computed
|
|
141
|
+
|| callee.optional
|
|
142
|
+
) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const method = getStaticPropertyName(callee.property);
|
|
147
|
+
const receiverKey = getMockTimersReceiverKey(callee.object, imports, sourceCode, contextVariables);
|
|
148
|
+
if (!method || !receiverKey) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {method, receiverKey};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function satisfyPending(scope, receiverKey) {
|
|
156
|
+
for (const pending of scope.pending) {
|
|
157
|
+
if (!pending.satisfied && pending.receiverKey === receiverKey) {
|
|
158
|
+
pending.satisfied = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getContextVariable(callback, sourceCode) {
|
|
164
|
+
const [parameter] = callback.params;
|
|
165
|
+
if (parameter?.type !== 'Identifier') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return findVariable(sourceCode.getScope(parameter), parameter);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getCalleeRootIdentifier(node) {
|
|
173
|
+
while (
|
|
174
|
+
node.type === 'MemberExpression'
|
|
175
|
+
&& !node.computed
|
|
176
|
+
&& !node.optional
|
|
177
|
+
) {
|
|
178
|
+
node = node.object;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return node.type === 'Identifier' ? node : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getContextVariables(scopeStack) {
|
|
185
|
+
return scopeStack
|
|
186
|
+
.map(scope => scope.contextVariable)
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getContextCallKind(node, sourceCode, contextVariables) {
|
|
191
|
+
const receiver = getSubtestReceiver(node);
|
|
192
|
+
if (receiver) {
|
|
193
|
+
const receiverVariable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
194
|
+
return receiverVariable && contextVariables.includes(receiverVariable) ? 'test' : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const {callee} = node;
|
|
198
|
+
if (
|
|
199
|
+
callee.type !== 'MemberExpression'
|
|
200
|
+
|| callee.computed
|
|
201
|
+
|| callee.optional
|
|
202
|
+
|| callee.object.type !== 'Identifier'
|
|
203
|
+
|| !HOOK_FUNCTIONS.has(getStaticPropertyName(callee.property))
|
|
204
|
+
) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const receiverVariable = findVariable(sourceCode.getScope(callee.object), callee.object);
|
|
209
|
+
return receiverVariable && contextVariables.includes(receiverVariable) ? 'hook' : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getScopeCallback(node, imports, sourceCode, contextVariables) {
|
|
213
|
+
const parsed = parseTestCall(node, imports);
|
|
214
|
+
const root = getCalleeRootIdentifier(node.callee);
|
|
215
|
+
if (
|
|
216
|
+
(parsed?.kind === 'test' || parsed?.kind === 'hook')
|
|
217
|
+
&& parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name))
|
|
218
|
+
&& root
|
|
219
|
+
&& isImportedIdentifier(root, sourceCode)
|
|
220
|
+
) {
|
|
221
|
+
return getTestCallback(node);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const contextCallKind = getContextCallKind(node, sourceCode, contextVariables);
|
|
225
|
+
return contextCallKind === 'test' || contextCallKind === 'hook' ? getTestCallback(node) : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
229
|
+
const create = context => {
|
|
230
|
+
const {sourceCode} = context;
|
|
231
|
+
const imports = resolveImports(context);
|
|
232
|
+
if (!imports.isTestFile) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const scopeStack = [];
|
|
237
|
+
|
|
238
|
+
context.on('CallExpression', node => {
|
|
239
|
+
const callback = getScopeCallback(node, imports, sourceCode, getContextVariables(scopeStack));
|
|
240
|
+
if (callback) {
|
|
241
|
+
const contextVariable = getContextVariable(callback, sourceCode);
|
|
242
|
+
scopeStack.push({
|
|
243
|
+
callNode: node,
|
|
244
|
+
callback,
|
|
245
|
+
contextVariable,
|
|
246
|
+
pending: [],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (scopeStack.length === 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const currentScope = scopeStack.at(-1);
|
|
255
|
+
if (getEnclosingFunction(node) !== currentScope.callback) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const timersCall = getMockTimersCall(node, imports, sourceCode, getContextVariables(scopeStack));
|
|
260
|
+
if (timersCall && ADVANCE_METHODS.has(timersCall.method)) {
|
|
261
|
+
satisfyPending(currentScope, timersCall.receiverKey);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
context.onExit('CallExpression', node => {
|
|
266
|
+
let problems;
|
|
267
|
+
const currentScope = scopeStack.at(-1);
|
|
268
|
+
if (currentScope?.callNode === node) {
|
|
269
|
+
scopeStack.pop();
|
|
270
|
+
problems = currentScope.pending
|
|
271
|
+
.filter(pending => !pending.satisfied)
|
|
272
|
+
.map(pending => ({
|
|
273
|
+
node: pending.node,
|
|
274
|
+
messageId: pending.messageId,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const newCurrentScope = scopeStack.at(-1);
|
|
279
|
+
if (
|
|
280
|
+
newCurrentScope
|
|
281
|
+
&& getEnclosingFunction(node) === newCurrentScope.callback
|
|
282
|
+
) {
|
|
283
|
+
const timersCall = getMockTimersCall(node, imports, sourceCode, getContextVariables(scopeStack));
|
|
284
|
+
if (timersCall?.method === 'enable' && mayEnableTimerApis(node)) {
|
|
285
|
+
newCurrentScope.pending.push({
|
|
286
|
+
node,
|
|
287
|
+
messageId: MESSAGE_ID,
|
|
288
|
+
receiverKey: timersCall.receiverKey,
|
|
289
|
+
satisfied: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return problems;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
299
|
+
const config = {
|
|
300
|
+
create,
|
|
301
|
+
meta: {
|
|
302
|
+
type: 'problem',
|
|
303
|
+
docs: {
|
|
304
|
+
description: 'Require mock timers to be advanced after enabling timer APIs.',
|
|
305
|
+
recommended: true,
|
|
306
|
+
},
|
|
307
|
+
schema: [],
|
|
308
|
+
messages,
|
|
309
|
+
languages: ['js/js'],
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export default config;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {isFunction} from './ast/index.js';
|
|
3
|
+
import {
|
|
4
|
+
getSubtestReceiver,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
MODIFIERS,
|
|
7
|
+
parseTestCall,
|
|
8
|
+
resolveImports,
|
|
9
|
+
} from './utils/node-test.js';
|
|
10
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_ID = 'require-mock-timers-apis';
|
|
13
|
+
const CONTEXT_HOOKS = new Set(['before', 'beforeEach', 'after', 'afterEach']);
|
|
14
|
+
const TEST_MODULES = new Set(['node:test', 'test']);
|
|
15
|
+
const STATIC_NON_OPTIONS_VALUE_TYPES = new Set(['ArrayExpression', 'Literal', 'TemplateLiteral']);
|
|
16
|
+
|
|
17
|
+
const messages = {
|
|
18
|
+
[MESSAGE_ID]: '`mock.timers.enable()` should explicitly specify the `apis` option to avoid unexpectedly mocking `Date`.',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isMissingApisValue(node) {
|
|
22
|
+
const expression = unwrapTypeScriptExpression(node);
|
|
23
|
+
if (!expression) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
(expression.type === 'Identifier' && expression.name === 'undefined')
|
|
29
|
+
|| (expression.type === 'UnaryExpression' && expression.operator === 'void')
|
|
30
|
+
|| (expression.type === 'Literal' && !expression.value)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isStaticNonOptionsValue(node) {
|
|
35
|
+
return STATIC_NON_OPTIONS_VALUE_TYPES.has(node.type);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isApisProperty(property) {
|
|
39
|
+
return property.type === 'Property'
|
|
40
|
+
&& !property.computed
|
|
41
|
+
&& (
|
|
42
|
+
(property.key.type === 'Identifier' && property.key.name === 'apis')
|
|
43
|
+
|| (property.key.type === 'Literal' && property.key.value === 'apis')
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLastVisibleApisProperty(optionsObject) {
|
|
48
|
+
for (let index = optionsObject.properties.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
const property = optionsObject.properties[index];
|
|
50
|
+
if (property.type === 'SpreadElement') {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isApisProperty(property)) {
|
|
55
|
+
return property;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isImportedReference(node, sourceCode) {
|
|
63
|
+
const variable = findVariable(sourceCode.getScope(node), node);
|
|
64
|
+
return variable?.defs.some(({type}) => type === 'ImportBinding') ?? false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isMissingApisOption(callExpression) {
|
|
68
|
+
const firstArgument = unwrapTypeScriptExpression(callExpression.arguments[0]);
|
|
69
|
+
if (!firstArgument) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isMissingApisValue(firstArgument)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (firstArgument.type !== 'ObjectExpression') {
|
|
78
|
+
return isStaticNonOptionsValue(firstArgument);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const apisProperty = getLastVisibleApisProperty(firstArgument);
|
|
82
|
+
return !apisProperty || isMissingApisValue(apisProperty.value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isContextProvidingCall(testCall) {
|
|
86
|
+
if (testCall.kind === 'hook') {
|
|
87
|
+
return testCall.modifiers.length === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return testCall.kind === 'test'
|
|
91
|
+
&& testCall.modifiers.every(modifier => MODIFIERS.has(modifier.name));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getParameterIdentifier(parameter) {
|
|
95
|
+
if (parameter?.type === 'Identifier') {
|
|
96
|
+
return parameter;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (parameter?.type === 'AssignmentPattern' && parameter.left.type === 'Identifier') {
|
|
100
|
+
return parameter.left;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getContextHookReceiver(callExpression) {
|
|
107
|
+
const {callee} = callExpression;
|
|
108
|
+
if (
|
|
109
|
+
callee.type !== 'MemberExpression'
|
|
110
|
+
|| callee.computed
|
|
111
|
+
|| callee.property.type !== 'Identifier'
|
|
112
|
+
|| !CONTEXT_HOOKS.has(callee.property.name)
|
|
113
|
+
) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const receiver = unwrapTypeScriptExpression(callee.object);
|
|
118
|
+
return receiver.type === 'Identifier' ? receiver : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getTestContextImportLocals(sourceCode) {
|
|
122
|
+
const locals = new Set();
|
|
123
|
+
|
|
124
|
+
for (const node of sourceCode.ast.body) {
|
|
125
|
+
if (node.type !== 'ImportDeclaration' || !TEST_MODULES.has(node.source.value)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const specifier of node.specifiers) {
|
|
130
|
+
if (
|
|
131
|
+
specifier.type === 'ImportSpecifier'
|
|
132
|
+
&& specifier.imported.type === 'Identifier'
|
|
133
|
+
&& specifier.imported.name === 'getTestContext'
|
|
134
|
+
) {
|
|
135
|
+
locals.add(specifier.local.name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return locals;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getHookCallback(callExpression) {
|
|
144
|
+
const firstArgument = unwrapTypeScriptExpression(callExpression.arguments[0]);
|
|
145
|
+
return isFunction(firstArgument) ? firstArgument : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getLocalTestHookCall(callExpression, imports, sourceCode) {
|
|
149
|
+
const callee = unwrapTypeScriptExpression(callExpression.callee);
|
|
150
|
+
if (
|
|
151
|
+
callee.type !== 'MemberExpression'
|
|
152
|
+
|| callee.computed
|
|
153
|
+
|| callee.property.type !== 'Identifier'
|
|
154
|
+
|| !CONTEXT_HOOKS.has(callee.property.name)
|
|
155
|
+
) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const object = unwrapTypeScriptExpression(callee.object);
|
|
160
|
+
if (
|
|
161
|
+
object.type !== 'Identifier'
|
|
162
|
+
|| !['test', 'it'].includes(imports.locals.get(object.name))
|
|
163
|
+
|| !isImportedReference(object, sourceCode)
|
|
164
|
+
) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
name: callee.property.name,
|
|
170
|
+
kind: 'hook',
|
|
171
|
+
modifiers: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
176
|
+
const create = context => {
|
|
177
|
+
const {sourceCode} = context;
|
|
178
|
+
const imports = resolveImports(context);
|
|
179
|
+
const testContextLocals = getTestContextImportLocals(sourceCode);
|
|
180
|
+
if (!imports.isTestFile && testContextLocals.size === 0) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const contextVariables = [];
|
|
185
|
+
const pushedCalls = new Set();
|
|
186
|
+
|
|
187
|
+
const getImportedContextCall = node => {
|
|
188
|
+
const testCall = parseTestCall(node, imports);
|
|
189
|
+
if (testCall !== undefined && isContextProvidingCall(testCall)) {
|
|
190
|
+
return testCall;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return getLocalTestHookCall(node, imports, sourceCode);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const isSubtestCall = node => {
|
|
197
|
+
const receiver = getSubtestReceiver(node);
|
|
198
|
+
if (!receiver) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const variable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
203
|
+
return variable !== undefined && contextVariables.includes(variable);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const isContextHookCall = node => {
|
|
207
|
+
const receiver = getContextHookReceiver(node);
|
|
208
|
+
if (!receiver) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const variable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
213
|
+
return variable !== undefined && contextVariables.includes(variable);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const updateContext = node => {
|
|
217
|
+
const importedContextCall = getImportedContextCall(node);
|
|
218
|
+
const isContextHook = isContextHookCall(node);
|
|
219
|
+
if (!importedContextCall && !isSubtestCall(node) && !isContextHook) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const callback = importedContextCall?.kind === 'hook' || isContextHook
|
|
224
|
+
? getHookCallback(node)
|
|
225
|
+
: getTestCallback(node);
|
|
226
|
+
if (!callback) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const parameter = getParameterIdentifier(callback.params[0]);
|
|
231
|
+
const variable = parameter
|
|
232
|
+
? findVariable(sourceCode.getScope(parameter), parameter)
|
|
233
|
+
: undefined;
|
|
234
|
+
|
|
235
|
+
contextVariables.push(variable);
|
|
236
|
+
pushedCalls.add(node);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const leaveContext = node => {
|
|
240
|
+
if (!pushedCalls.has(node)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pushedCalls.delete(node);
|
|
245
|
+
contextVariables.pop();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const isGlobalMock = node => {
|
|
249
|
+
const expression = unwrapTypeScriptExpression(node);
|
|
250
|
+
if (
|
|
251
|
+
expression.type === 'Identifier'
|
|
252
|
+
&& imports.mockLocals.has(expression.name)
|
|
253
|
+
&& isImportedReference(expression, sourceCode)
|
|
254
|
+
) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
expression.type !== 'MemberExpression'
|
|
260
|
+
|| expression.computed
|
|
261
|
+
|| expression.property.type !== 'Identifier'
|
|
262
|
+
|| expression.property.name !== 'mock'
|
|
263
|
+
) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const object = unwrapTypeScriptExpression(expression.object);
|
|
268
|
+
return object.type === 'Identifier'
|
|
269
|
+
&& (
|
|
270
|
+
object.name === imports.namespace
|
|
271
|
+
|| imports.locals.get(object.name) === 'test'
|
|
272
|
+
|| imports.locals.get(object.name) === 'it'
|
|
273
|
+
)
|
|
274
|
+
&& isImportedReference(object, sourceCode);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const isCurrentContextReference = node => {
|
|
278
|
+
const variable = findVariable(sourceCode.getScope(node), node);
|
|
279
|
+
return variable !== undefined && contextVariables.includes(variable);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const isGetTestContextCall = node => {
|
|
283
|
+
if (node.type !== 'CallExpression') {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const callee = unwrapTypeScriptExpression(node.callee);
|
|
288
|
+
if (
|
|
289
|
+
callee.type === 'Identifier'
|
|
290
|
+
&& testContextLocals.has(callee.name)
|
|
291
|
+
&& isImportedReference(callee, sourceCode)
|
|
292
|
+
) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
callee.type !== 'MemberExpression'
|
|
298
|
+
|| callee.computed
|
|
299
|
+
|| callee.property.type !== 'Identifier'
|
|
300
|
+
|| callee.property.name !== 'getTestContext'
|
|
301
|
+
) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const object = unwrapTypeScriptExpression(callee.object);
|
|
306
|
+
return object.type === 'Identifier'
|
|
307
|
+
&& (
|
|
308
|
+
object.name === imports.namespace
|
|
309
|
+
|| imports.locals.get(object.name) === 'test'
|
|
310
|
+
|| imports.locals.get(object.name) === 'it'
|
|
311
|
+
)
|
|
312
|
+
&& isImportedReference(object, sourceCode);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const isContextMock = node => {
|
|
316
|
+
const expression = unwrapTypeScriptExpression(node);
|
|
317
|
+
if (
|
|
318
|
+
expression.type !== 'MemberExpression'
|
|
319
|
+
|| expression.computed
|
|
320
|
+
|| expression.property.type !== 'Identifier'
|
|
321
|
+
|| expression.property.name !== 'mock'
|
|
322
|
+
) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const object = unwrapTypeScriptExpression(expression.object);
|
|
327
|
+
return (
|
|
328
|
+
(object.type === 'Identifier' && isCurrentContextReference(object))
|
|
329
|
+
|| isGetTestContextCall(object)
|
|
330
|
+
);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const isMockTimers = node => {
|
|
334
|
+
const expression = unwrapTypeScriptExpression(node);
|
|
335
|
+
return expression.type === 'MemberExpression'
|
|
336
|
+
&& !expression.computed
|
|
337
|
+
&& expression.property.type === 'Identifier'
|
|
338
|
+
&& expression.property.name === 'timers'
|
|
339
|
+
&& (isGlobalMock(expression.object) || isContextMock(expression.object));
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
context.on('CallExpression', node => {
|
|
343
|
+
updateContext(node);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
context.onExit('CallExpression', node => {
|
|
347
|
+
leaveContext(node);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
context.on('CallExpression', node => {
|
|
351
|
+
const callee = unwrapTypeScriptExpression(node.callee);
|
|
352
|
+
if (
|
|
353
|
+
callee.type === 'MemberExpression'
|
|
354
|
+
&& !callee.computed
|
|
355
|
+
&& callee.property.type === 'Identifier'
|
|
356
|
+
&& callee.property.name === 'enable'
|
|
357
|
+
&& isMockTimers(callee.object)
|
|
358
|
+
&& isMissingApisOption(node)
|
|
359
|
+
) {
|
|
360
|
+
return {
|
|
361
|
+
node,
|
|
362
|
+
messageId: MESSAGE_ID,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
369
|
+
const config = {
|
|
370
|
+
create,
|
|
371
|
+
meta: {
|
|
372
|
+
type: 'problem',
|
|
373
|
+
docs: {
|
|
374
|
+
description: 'Require an explicit `apis` option when enabling `mock.timers`.',
|
|
375
|
+
recommended: 'unopinionated',
|
|
376
|
+
},
|
|
377
|
+
schema: [],
|
|
378
|
+
messages,
|
|
379
|
+
languages: ['js/js'],
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export default config;
|