@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,123 @@
|
|
|
1
|
+
import {resolveImports, createContextTracker} from './utils/node-test.js';
|
|
2
|
+
import isFunction from './ast/is-function.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_ERROR = 'no-skip-without-return/error';
|
|
5
|
+
const MESSAGE_ID_SUGGESTION = 'no-skip-without-return/suggestion';
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID_ERROR]: '`{{name}}.{{method}}()` does not stop the test; code after it still runs. Return afterwards.',
|
|
9
|
+
[MESSAGE_ID_SUGGESTION]: 'Add `return` after `{{name}}.{{method}}()`.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const SKIP_METHODS = new Set(['skip', 'todo']);
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
Whether reachable code follows the skip statement before the enclosing test function ends.
|
|
16
|
+
Walks outward: a following sibling statement means code runs after the skip, unless the skip
|
|
17
|
+
is directly followed by a `return`/`throw`. Only block and program bodies are inspected; a skip
|
|
18
|
+
inside a `switch` case is best-effort and not flagged, since detecting it correctly would require
|
|
19
|
+
modeling break/return/fall-through control flow.
|
|
20
|
+
*/
|
|
21
|
+
function hasCodeAfter(skipStatement) {
|
|
22
|
+
let node = skipStatement;
|
|
23
|
+
while (node) {
|
|
24
|
+
const {parent} = node;
|
|
25
|
+
if (!parent) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (parent.type === 'BlockStatement' || parent.type === 'Program') {
|
|
30
|
+
const next = parent.body[parent.body.indexOf(node) + 1];
|
|
31
|
+
if (next) {
|
|
32
|
+
// A `return`/`throw` immediately after the skip itself is the correct pattern.
|
|
33
|
+
return !(node === skipStatement && (next.type === 'ReturnStatement' || next.type === 'ThrowStatement'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isFunction(parent)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
node = parent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
48
|
+
const create = context => {
|
|
49
|
+
const {sourceCode} = context;
|
|
50
|
+
const imports = resolveImports(context);
|
|
51
|
+
if (!imports.isTestFile) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tracker = createContextTracker(imports);
|
|
56
|
+
|
|
57
|
+
context.on('CallExpression', node => {
|
|
58
|
+
let problem;
|
|
59
|
+
|
|
60
|
+
const {callee, parent} = node;
|
|
61
|
+
if (
|
|
62
|
+
parent.type === 'ExpressionStatement'
|
|
63
|
+
&& callee.type === 'MemberExpression'
|
|
64
|
+
&& !callee.computed
|
|
65
|
+
&& callee.property.type === 'Identifier'
|
|
66
|
+
&& SKIP_METHODS.has(callee.property.name)
|
|
67
|
+
&& callee.object.type === 'Identifier'
|
|
68
|
+
&& tracker.isContextIdentifier(callee.object)
|
|
69
|
+
&& hasCodeAfter(parent)
|
|
70
|
+
) {
|
|
71
|
+
const {name} = callee.object;
|
|
72
|
+
const method = callee.property.name;
|
|
73
|
+
problem = {
|
|
74
|
+
node,
|
|
75
|
+
messageId: MESSAGE_ID_ERROR,
|
|
76
|
+
data: {name, method},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Only suggest inserting `return` when the skip is in a block; in a braceless
|
|
80
|
+
// `if (x) t.skip()` the inserted `return` would escape the condition.
|
|
81
|
+
if (parent.parent.type === 'BlockStatement') {
|
|
82
|
+
problem.suggest = [
|
|
83
|
+
{
|
|
84
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
85
|
+
data: {name, method},
|
|
86
|
+
fix(fixer) {
|
|
87
|
+
// Insert `return;` on its own line, matching the skip statement's indentation.
|
|
88
|
+
const [start] = sourceCode.getRange(parent);
|
|
89
|
+
const lineStart = sourceCode.text.lastIndexOf('\n', start - 1) + 1;
|
|
90
|
+
const [indentation] = /^\s*/.exec(sourceCode.text.slice(lineStart, start));
|
|
91
|
+
return fixer.insertTextAfter(parent, `\n${indentation}return;`);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tracker.update(node);
|
|
99
|
+
return problem;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
context.onExit('CallExpression', node => {
|
|
103
|
+
tracker.leave(node);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
108
|
+
const config = {
|
|
109
|
+
create,
|
|
110
|
+
meta: {
|
|
111
|
+
type: 'problem',
|
|
112
|
+
docs: {
|
|
113
|
+
description: 'Disallow `t.skip()`/`t.todo()` without returning afterwards.',
|
|
114
|
+
recommended: true,
|
|
115
|
+
},
|
|
116
|
+
hasSuggestions: true,
|
|
117
|
+
schema: [],
|
|
118
|
+
messages,
|
|
119
|
+
languages: ['js/js'],
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default config;
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
resolveImports,
|
|
4
|
+
parseTestCall,
|
|
5
|
+
getCalleeChain,
|
|
6
|
+
getHookCallback,
|
|
7
|
+
getTestCallback,
|
|
8
|
+
getTestOptions,
|
|
9
|
+
findEnabledOptionsProperty,
|
|
10
|
+
isHookMemberTestCall,
|
|
11
|
+
} from './utils/node-test.js';
|
|
12
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
13
|
+
import {getEnclosingFunction} from './utils/index.js';
|
|
14
|
+
import {isFunction} from './ast/index.js';
|
|
15
|
+
|
|
16
|
+
const MESSAGE_ID = 'no-sleep-in-test';
|
|
17
|
+
|
|
18
|
+
const messages = {
|
|
19
|
+
[MESSAGE_ID]: 'Do not sleep in tests with `setTimeout`. Await the real signal or use mock timers instead.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CALLBACK_TIMER_MODULES = new Set(['node:timers', 'timers']);
|
|
23
|
+
const PROMISE_TIMER_MODULES = new Set(['node:timers/promises', 'timers/promises']);
|
|
24
|
+
const TEST_MODIFIERS = new Set(['expectFailure', 'only', 'skip', 'todo']);
|
|
25
|
+
const ACTIVE_TEST_MODIFIERS = new Set(['expectFailure', 'only', 'todo']);
|
|
26
|
+
const CONTEXT_HOOKS = new Set(['before', 'beforeEach', 'after', 'afterEach']);
|
|
27
|
+
|
|
28
|
+
const unwrapExpression = node => {
|
|
29
|
+
let unwrapped = node && unwrapTypeScriptExpression(node);
|
|
30
|
+
while (unwrapped?.type === 'ChainExpression') {
|
|
31
|
+
unwrapped = unwrapTypeScriptExpression(unwrapped.expression);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return unwrapped;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function isIdentifierReference(node, name) {
|
|
38
|
+
return node?.type === 'Identifier' && node.name === name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isGlobalReference(sourceCode, node, name) {
|
|
42
|
+
node = unwrapExpression(node);
|
|
43
|
+
if (!isIdentifierReference(node, name)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const variable = findVariable(sourceCode.getScope(node), node);
|
|
48
|
+
return !variable || variable.defs.length === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isSameReference(sourceCode, node, variable) {
|
|
52
|
+
node = unwrapExpression(node);
|
|
53
|
+
return node?.type === 'Identifier'
|
|
54
|
+
&& findVariable(sourceCode.getScope(node), node) === variable;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isGlobalObjectSetTimeout(sourceCode, node) {
|
|
58
|
+
node = unwrapExpression(node);
|
|
59
|
+
return node?.type === 'MemberExpression'
|
|
60
|
+
&& !node.computed
|
|
61
|
+
&& (
|
|
62
|
+
isGlobalReference(sourceCode, node.object, 'globalThis')
|
|
63
|
+
|| isGlobalReference(sourceCode, node.object, 'global')
|
|
64
|
+
)
|
|
65
|
+
&& isIdentifierReference(node.property, 'setTimeout');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function areActiveModifiers(modifiers) {
|
|
69
|
+
return modifiers.every(modifier => ACTIVE_TEST_MODIFIERS.has(modifier.name));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasInactiveTestOptions(node) {
|
|
73
|
+
const options = getTestOptions(node);
|
|
74
|
+
return Boolean(findEnabledOptionsProperty(options, 'skip'));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getSubtestModifiers(node) {
|
|
78
|
+
const {members = []} = getCalleeChain(node.callee) ?? {};
|
|
79
|
+
return members?.[0]?.name === 'test' ? members.slice(1) : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getSupportedSubtestReceiver(node) {
|
|
83
|
+
const chain = getCalleeChain(node.callee);
|
|
84
|
+
if (
|
|
85
|
+
chain
|
|
86
|
+
&& chain.members[0]?.name === 'test'
|
|
87
|
+
&& chain.members.slice(1).every(member => TEST_MODIFIERS.has(member.name))
|
|
88
|
+
) {
|
|
89
|
+
return chain.root;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getContextHookReceiver(node) {
|
|
94
|
+
const callee = unwrapExpression(node.callee);
|
|
95
|
+
if (
|
|
96
|
+
callee?.type !== 'MemberExpression'
|
|
97
|
+
|| callee.computed
|
|
98
|
+
|| callee.property.type !== 'Identifier'
|
|
99
|
+
|| !CONTEXT_HOOKS.has(callee.property.name)
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const receiver = unwrapExpression(callee.object);
|
|
105
|
+
return receiver.type === 'Identifier' ? receiver : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getParsedCallback(node, parsed) {
|
|
109
|
+
if (getParsedKind(parsed) === 'hook') {
|
|
110
|
+
return getHookCallback(node);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return getTestCallback(node);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getParsedKind(parsed) {
|
|
117
|
+
return isHookMemberTestCall(parsed) ? 'hook' : parsed.kind;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getParsedModifiers(parsed) {
|
|
121
|
+
return isHookMemberTestCall(parsed) ? [] : parsed.modifiers;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasInactiveParsedOptions(node, parsed) {
|
|
125
|
+
return getParsedKind(parsed) !== 'hook' && hasInactiveTestOptions(node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getTimerImportBindings(sourceCode) {
|
|
129
|
+
const named = new Set();
|
|
130
|
+
const namespace = new Set();
|
|
131
|
+
const promiseNamed = new Set();
|
|
132
|
+
const promiseNamespace = new Set();
|
|
133
|
+
|
|
134
|
+
for (const node of sourceCode.ast.body) {
|
|
135
|
+
if (node.type !== 'ImportDeclaration') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isCallbackTimerModule = CALLBACK_TIMER_MODULES.has(node.source.value);
|
|
140
|
+
const isPromiseTimerModule = PROMISE_TIMER_MODULES.has(node.source.value);
|
|
141
|
+
if (!isCallbackTimerModule && !isPromiseTimerModule) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const specifier of node.specifiers) {
|
|
146
|
+
if (
|
|
147
|
+
specifier.type === 'ImportSpecifier'
|
|
148
|
+
&& specifier.imported.type === 'Identifier'
|
|
149
|
+
&& specifier.imported.name === 'setTimeout'
|
|
150
|
+
) {
|
|
151
|
+
const variable = findVariable(sourceCode.getScope(specifier.local), specifier.local);
|
|
152
|
+
if (variable) {
|
|
153
|
+
(isPromiseTimerModule ? promiseNamed : named).add(variable);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (specifier.type === 'ImportNamespaceSpecifier' || specifier.type === 'ImportDefaultSpecifier') {
|
|
158
|
+
const variable = findVariable(sourceCode.getScope(specifier.local), specifier.local);
|
|
159
|
+
if (variable) {
|
|
160
|
+
(isPromiseTimerModule ? promiseNamespace : namespace).add(variable);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
named, namespace, promiseNamed, promiseNamespace, sourceCode,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isImportedSetTimeout(node, sourceCode, named, namespace) {
|
|
172
|
+
node = unwrapExpression(node);
|
|
173
|
+
|
|
174
|
+
if (node?.type === 'Identifier') {
|
|
175
|
+
return named.has(findVariable(sourceCode.getScope(node), node));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return node?.type === 'MemberExpression'
|
|
179
|
+
&& !node.computed
|
|
180
|
+
&& node.object.type === 'Identifier'
|
|
181
|
+
&& namespace.has(findVariable(sourceCode.getScope(node.object), node.object))
|
|
182
|
+
&& isIdentifierReference(node.property, 'setTimeout');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isImportedTimerSetTimeout(node, timerImports) {
|
|
186
|
+
return isImportedSetTimeout(node, timerImports.sourceCode, timerImports.named, timerImports.namespace);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isImportedPromiseTimerSetTimeout(node, timerImports) {
|
|
190
|
+
return isImportedSetTimeout(node, timerImports.sourceCode, timerImports.promiseNamed, timerImports.promiseNamespace);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isSetTimeoutCallee(node, timerImports) {
|
|
194
|
+
node = unwrapExpression(node);
|
|
195
|
+
return isGlobalReference(timerImports.sourceCode, node, 'setTimeout')
|
|
196
|
+
|| isGlobalObjectSetTimeout(timerImports.sourceCode, node)
|
|
197
|
+
|| isImportedTimerSetTimeout(node, timerImports);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function containsExpression(node, predicate, visitorKeys) {
|
|
201
|
+
const stack = [node];
|
|
202
|
+
|
|
203
|
+
while (stack.length > 0) {
|
|
204
|
+
const current = unwrapExpression(stack.pop());
|
|
205
|
+
if (!current?.type) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (predicate(current)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isFunction(current)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const key of visitorKeys[current.type] ?? []) {
|
|
218
|
+
const child = current[key];
|
|
219
|
+
for (const childNode of Array.isArray(child) ? child : [child]) {
|
|
220
|
+
if (childNode?.type) {
|
|
221
|
+
stack.push(childNode);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function expressionCallsResolver(node, resolverVariable, sourceCode) {
|
|
231
|
+
node = unwrapExpression(node);
|
|
232
|
+
return node?.type === 'CallExpression'
|
|
233
|
+
&& isSameReference(sourceCode, node.callee, resolverVariable);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function functionCallsResolver(node, resolverVariable, sourceCode) {
|
|
237
|
+
node = unwrapExpression(node);
|
|
238
|
+
if (!isFunction(node)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return containsExpression(node.body, expression => expressionCallsResolver(expression, resolverVariable, sourceCode), sourceCode.visitorKeys);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isResolverArgument(node, resolverVariable, sourceCode) {
|
|
246
|
+
node = unwrapExpression(node);
|
|
247
|
+
return isSameReference(sourceCode, node, resolverVariable) || functionCallsResolver(node, resolverVariable, sourceCode);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isSleepSetTimeoutCall(node, resolverVariable, timerImports) {
|
|
251
|
+
node = unwrapExpression(node);
|
|
252
|
+
return node?.type === 'CallExpression'
|
|
253
|
+
&& isSetTimeoutCallee(node.callee, timerImports)
|
|
254
|
+
&& isResolverArgument(node.arguments[0], resolverVariable, timerImports.sourceCode);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function isSleepPromise(node, timerImports) {
|
|
258
|
+
node = unwrapExpression(node);
|
|
259
|
+
if (
|
|
260
|
+
node?.type !== 'NewExpression'
|
|
261
|
+
|| !isGlobalReference(timerImports.sourceCode, node.callee, 'Promise')
|
|
262
|
+
) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const [executor] = node.arguments;
|
|
267
|
+
const executorFunction = unwrapExpression(executor);
|
|
268
|
+
const resolveOrRejectVariables = isFunction(executorFunction)
|
|
269
|
+
? executorFunction.params.slice(0, 2)
|
|
270
|
+
.filter(parameter => parameter.type === 'Identifier')
|
|
271
|
+
.map(parameter => findVariable(timerImports.sourceCode.getScope(parameter), parameter))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
: [];
|
|
274
|
+
if (resolveOrRejectVariables.length === 0) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const isSleepCall = expression => resolveOrRejectVariables.some(resolveOrRejectVariable =>
|
|
279
|
+
isSleepSetTimeoutCall(expression, resolveOrRejectVariable, timerImports));
|
|
280
|
+
return containsExpression(executorFunction.body, isSleepCall, timerImports.sourceCode.visitorKeys);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isPromiseTimerSleep(node, timerImports) {
|
|
284
|
+
node = unwrapExpression(node);
|
|
285
|
+
return node?.type === 'CallExpression'
|
|
286
|
+
&& isImportedPromiseTimerSetTimeout(node.callee, timerImports);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
290
|
+
const create = context => {
|
|
291
|
+
const imports = resolveImports(context);
|
|
292
|
+
if (!imports.isTestFile) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const timerImports = getTimerImportBindings(context.sourceCode);
|
|
297
|
+
const testStack = [];
|
|
298
|
+
const inactiveCallbackStack = [];
|
|
299
|
+
const trackedCalls = new Set();
|
|
300
|
+
const {sourceCode} = context;
|
|
301
|
+
|
|
302
|
+
const getContextVariable = callback => {
|
|
303
|
+
const [parameter] = callback.params;
|
|
304
|
+
if (parameter?.type !== 'Identifier') {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return findVariable(sourceCode.getScope(parameter), parameter);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const hasInactiveCallbackAncestor = node => {
|
|
312
|
+
for (const {callback} of inactiveCallbackStack) {
|
|
313
|
+
let current = node;
|
|
314
|
+
while (current) {
|
|
315
|
+
if (current === callback) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
current = current.parent;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const isCurrentTestContextReceiver = (node, receiver) => {
|
|
327
|
+
const currentTest = testStack.at(-1);
|
|
328
|
+
if (!currentTest?.contextVariable) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const receiverVariable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
333
|
+
return getEnclosingFunction(node) === currentTest.callback
|
|
334
|
+
&& currentTest.contextVariable === receiverVariable;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const isCurrentTestContextSubtestCall = node => {
|
|
338
|
+
const receiver = getSupportedSubtestReceiver(node);
|
|
339
|
+
if (receiver?.type !== 'Identifier') {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return isCurrentTestContextReceiver(node, receiver);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const isCurrentTestContextHookCall = node => {
|
|
347
|
+
const receiver = getContextHookReceiver(node);
|
|
348
|
+
if (!receiver) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return isCurrentTestContextReceiver(node, receiver);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const isActiveSubtestCall = node => isCurrentTestContextSubtestCall(node)
|
|
356
|
+
&& areActiveModifiers(getSubtestModifiers(node))
|
|
357
|
+
&& !hasInactiveTestOptions(node);
|
|
358
|
+
|
|
359
|
+
const isInactiveSubtestCall = node => isCurrentTestContextSubtestCall(node)
|
|
360
|
+
&& (
|
|
361
|
+
!areActiveModifiers(getSubtestModifiers(node))
|
|
362
|
+
|| hasInactiveTestOptions(node)
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const getScopeBoundaryCallback = node => {
|
|
366
|
+
if (hasInactiveCallbackAncestor(node)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const parsed = parseTestCall(node, imports);
|
|
371
|
+
if (parsed) {
|
|
372
|
+
const kind = getParsedKind(parsed);
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
(kind === 'test' || kind === 'hook')
|
|
376
|
+
&& areActiveModifiers(getParsedModifiers(parsed))
|
|
377
|
+
&& !hasInactiveParsedOptions(node, parsed)
|
|
378
|
+
)
|
|
379
|
+
? getParsedCallback(node, parsed)
|
|
380
|
+
: undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (isCurrentTestContextHookCall(node)) {
|
|
384
|
+
return getHookCallback(node);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return isActiveSubtestCall(node) ? getTestCallback(node) : undefined;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const getInactiveScopeCallback = node => {
|
|
391
|
+
const parsed = parseTestCall(node, imports);
|
|
392
|
+
if (parsed) {
|
|
393
|
+
return (
|
|
394
|
+
!areActiveModifiers(getParsedModifiers(parsed))
|
|
395
|
+
|| hasInactiveParsedOptions(node, parsed)
|
|
396
|
+
)
|
|
397
|
+
? getParsedCallback(node, parsed)
|
|
398
|
+
: undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!isInactiveSubtestCall(node)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return getTestCallback(node);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const isInsideTestCallback = node => {
|
|
409
|
+
const testCallback = testStack.at(-1)?.callback;
|
|
410
|
+
return testCallback ? getEnclosingFunction(node) === testCallback : false;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
context.on('CallExpression', node => {
|
|
414
|
+
const inactiveScopeCallback = getInactiveScopeCallback(node);
|
|
415
|
+
if (inactiveScopeCallback) {
|
|
416
|
+
inactiveCallbackStack.push({node, callback: inactiveScopeCallback});
|
|
417
|
+
trackedCalls.add(node);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const boundaryCallback = getScopeBoundaryCallback(node);
|
|
422
|
+
if (boundaryCallback) {
|
|
423
|
+
testStack.push({
|
|
424
|
+
callback: boundaryCallback,
|
|
425
|
+
contextVariable: getContextVariable(boundaryCallback),
|
|
426
|
+
});
|
|
427
|
+
trackedCalls.add(node);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
context.onExit('CallExpression', node => {
|
|
432
|
+
if (!trackedCalls.has(node)) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
trackedCalls.delete(node);
|
|
437
|
+
if (inactiveCallbackStack.at(-1)?.node === node) {
|
|
438
|
+
inactiveCallbackStack.pop();
|
|
439
|
+
} else {
|
|
440
|
+
testStack.pop();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
context.on('NewExpression', node => {
|
|
445
|
+
if (!isInsideTestCallback(node) || !isSleepPromise(node, timerImports)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
node,
|
|
451
|
+
messageId: MESSAGE_ID,
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
context.on('CallExpression', node => {
|
|
456
|
+
if (!isInsideTestCallback(node) || !isPromiseTimerSleep(node, timerImports)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
node,
|
|
462
|
+
messageId: MESSAGE_ID,
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
468
|
+
const config = {
|
|
469
|
+
create,
|
|
470
|
+
meta: {
|
|
471
|
+
type: 'problem',
|
|
472
|
+
docs: {
|
|
473
|
+
description: 'Disallow sleeping in tests with `setTimeout`.',
|
|
474
|
+
recommended: false,
|
|
475
|
+
},
|
|
476
|
+
schema: [],
|
|
477
|
+
messages,
|
|
478
|
+
languages: ['js/js'],
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export default config;
|