@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,194 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
resolveImports,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
parseTestCall,
|
|
6
|
+
getTestCallback,
|
|
7
|
+
getSubtestReceiver,
|
|
8
|
+
} from './utils/node-test.js';
|
|
9
|
+
import isFunction from './ast/is-function.js';
|
|
10
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_ID = 'no-assert-throws-multiple-statements';
|
|
13
|
+
|
|
14
|
+
const messages = {
|
|
15
|
+
[MESSAGE_ID]: 'Keep the `{{method}}()` callback to one statement so unrelated setup errors cannot satisfy the assertion.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const TARGET_METHODS = new Set(['throws', 'rejects']);
|
|
19
|
+
|
|
20
|
+
function getCalleeRootIdentifier(node) {
|
|
21
|
+
let current = node;
|
|
22
|
+
while (current.type === 'MemberExpression' && !current.computed) {
|
|
23
|
+
current = current.object;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (current.type === 'Identifier') {
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isImportBinding(identifier, sourceCode) {
|
|
32
|
+
const variable = findVariable(sourceCode.getScope(identifier), identifier);
|
|
33
|
+
|
|
34
|
+
return variable?.defs.some(({type}) => type === 'ImportBinding') === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getImportedTestReceiver(callExpression, imports) {
|
|
38
|
+
const receiver = getCalleeRootIdentifier(callExpression.callee);
|
|
39
|
+
if (
|
|
40
|
+
receiver
|
|
41
|
+
&& (
|
|
42
|
+
receiver.name === imports.namespace
|
|
43
|
+
|| imports.locals.has(receiver.name)
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
46
|
+
return receiver;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isImportedTestCall(callExpression, imports, sourceCode) {
|
|
51
|
+
const receiver = getImportedTestReceiver(callExpression, imports);
|
|
52
|
+
|
|
53
|
+
return receiver !== undefined && isImportBinding(receiver, sourceCode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getImportedAssertReceiver(callExpression, imports) {
|
|
57
|
+
const {callee} = callExpression;
|
|
58
|
+
if (
|
|
59
|
+
callee.type === 'Identifier'
|
|
60
|
+
&& (
|
|
61
|
+
imports.assertNamed.has(callee.name)
|
|
62
|
+
|| imports.assertNamespace.has(callee.name)
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
return callee;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
callee.type === 'MemberExpression'
|
|
70
|
+
&& !callee.computed
|
|
71
|
+
&& callee.object.type === 'Identifier'
|
|
72
|
+
&& (
|
|
73
|
+
imports.assertNamespace.has(callee.object.name)
|
|
74
|
+
|| imports.assertNamed.get(callee.object.name) === 'strict'
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
return callee.object;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
callee.type === 'MemberExpression'
|
|
82
|
+
&& !callee.computed
|
|
83
|
+
&& callee.object.type === 'MemberExpression'
|
|
84
|
+
&& !callee.object.computed
|
|
85
|
+
&& callee.object.object.type === 'Identifier'
|
|
86
|
+
&& callee.object.property.type === 'Identifier'
|
|
87
|
+
&& callee.object.property.name === 'strict'
|
|
88
|
+
&& imports.assertNamespace.has(callee.object.object.name)
|
|
89
|
+
) {
|
|
90
|
+
return callee.object.object;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getContextAssertReceiver(callee) {
|
|
95
|
+
if (
|
|
96
|
+
callee.type === 'MemberExpression'
|
|
97
|
+
&& !callee.computed
|
|
98
|
+
&& callee.object.type === 'MemberExpression'
|
|
99
|
+
&& !callee.object.computed
|
|
100
|
+
&& callee.object.object.type === 'Identifier'
|
|
101
|
+
&& callee.object.property.type === 'Identifier'
|
|
102
|
+
&& callee.object.property.name === 'assert'
|
|
103
|
+
) {
|
|
104
|
+
return callee.object.object;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isKnownContextReceiver(receiver, contextParameters, sourceCode) {
|
|
109
|
+
const variable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
110
|
+
|
|
111
|
+
return variable?.defs.some(({name}) => contextParameters.has(name)) === true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
115
|
+
const create = context => {
|
|
116
|
+
const {sourceCode} = context;
|
|
117
|
+
const imports = resolveImports(context);
|
|
118
|
+
if (!imports.isAssertOrTestFile) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const contextParameters = new WeakSet();
|
|
123
|
+
|
|
124
|
+
function rememberContextParameter(node) {
|
|
125
|
+
const parsed = parseTestCall(node, imports);
|
|
126
|
+
const subtestReceiver = getSubtestReceiver(node);
|
|
127
|
+
const isTest = parsed?.kind === 'test' && isImportedTestCall(node, imports, sourceCode);
|
|
128
|
+
const isSubtest = subtestReceiver && isKnownContextReceiver(subtestReceiver, contextParameters, sourceCode);
|
|
129
|
+
if (
|
|
130
|
+
!isTest
|
|
131
|
+
&& !isSubtest
|
|
132
|
+
) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const callback = getTestCallback(node);
|
|
137
|
+
const parameter = callback?.params[0];
|
|
138
|
+
if (parameter?.type === 'Identifier') {
|
|
139
|
+
contextParameters.add(parameter);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
context.on('CallExpression', node => {
|
|
144
|
+
rememberContextParameter(node);
|
|
145
|
+
|
|
146
|
+
const parsed = parseAssertionCall(node, imports);
|
|
147
|
+
if (!parsed || !TARGET_METHODS.has(parsed.method)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const importedAssertReceiver = getImportedAssertReceiver(node, imports);
|
|
152
|
+
if (importedAssertReceiver && !isImportBinding(importedAssertReceiver, sourceCode)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const contextAssertReceiver = getContextAssertReceiver(node.callee);
|
|
157
|
+
if (contextAssertReceiver && !isKnownContextReceiver(contextAssertReceiver, contextParameters, sourceCode)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const [firstArgument] = node.arguments;
|
|
162
|
+
if (!firstArgument || firstArgument.type === 'SpreadElement') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const callback = unwrapTypeScriptExpression(firstArgument);
|
|
167
|
+
if (!isFunction(callback) || callback.body.type !== 'BlockStatement' || callback.body.body.length <= 1) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
node: callback.body,
|
|
173
|
+
messageId: MESSAGE_ID,
|
|
174
|
+
data: {method: parsed.method},
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
180
|
+
const config = {
|
|
181
|
+
create,
|
|
182
|
+
meta: {
|
|
183
|
+
type: 'problem',
|
|
184
|
+
docs: {
|
|
185
|
+
description: 'Disallow multiple statements in `assert.throws()`/`assert.rejects()` callbacks.',
|
|
186
|
+
recommended: true,
|
|
187
|
+
},
|
|
188
|
+
schema: [],
|
|
189
|
+
messages,
|
|
190
|
+
languages: ['js/js'],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export default config;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
8
|
+
import {isStringExpression} from './ast/index.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID_ERROR = 'no-assert-throws-string/error';
|
|
11
|
+
const MESSAGE_ID_SUGGESTION = 'no-assert-throws-string/suggestion';
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
[MESSAGE_ID_ERROR]: 'The second argument to `{{method}}()` is the failure message, not an error matcher, so the thrown error is not validated.',
|
|
15
|
+
[MESSAGE_ID_SUGGESTION]: 'Match the error message with `{message: …}`.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const THROWS_METHODS = new Set(['throws', 'rejects']);
|
|
19
|
+
|
|
20
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
21
|
+
const create = context => {
|
|
22
|
+
const {sourceCode} = context;
|
|
23
|
+
const imports = resolveImports(context);
|
|
24
|
+
if (!imports.isAssertOrTestFile) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
29
|
+
|
|
30
|
+
context.on('CallExpression', node => {
|
|
31
|
+
tracker.update(node);
|
|
32
|
+
|
|
33
|
+
const parsed = parseAssertionCall(node, imports);
|
|
34
|
+
if (!parsed || !THROWS_METHODS.has(parsed.method) || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const errorArgument = node.arguments[1];
|
|
39
|
+
if (!errorArgument || !isStringExpression(unwrapTypeScriptExpression(errorArgument))) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
node: errorArgument,
|
|
45
|
+
messageId: MESSAGE_ID_ERROR,
|
|
46
|
+
data: {method: parsed.method},
|
|
47
|
+
suggest: [
|
|
48
|
+
{
|
|
49
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
50
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
51
|
+
fix: fixer => fixer.replaceText(errorArgument, `{message: ${sourceCode.getText(errorArgument)}}`),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
context.onExit('CallExpression', node => {
|
|
58
|
+
tracker.leave(node);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
63
|
+
const config = {
|
|
64
|
+
create,
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'problem',
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Disallow a string as the error matcher of `assert.throws()`/`assert.rejects()`.',
|
|
69
|
+
recommended: 'unopinionated',
|
|
70
|
+
},
|
|
71
|
+
hasSuggestions: true,
|
|
72
|
+
schema: [],
|
|
73
|
+
messages,
|
|
74
|
+
languages: ['js/js'],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default config;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-async-describe';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`node:test` does not await a `{{name}}` callback, so any test registered after an `await` is silently dropped. Make the callback synchronous.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
10
|
+
const create = context => {
|
|
11
|
+
const imports = resolveImports(context);
|
|
12
|
+
if (!imports.isTestFile) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
context.on('CallExpression', node => {
|
|
17
|
+
const parsed = parseTestCall(node, imports);
|
|
18
|
+
if (parsed?.kind !== 'suite') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const callback = getTestCallback(node);
|
|
23
|
+
if (!callback?.async) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
node: callback,
|
|
29
|
+
messageId: MESSAGE_ID,
|
|
30
|
+
data: {name: parsed.name},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
36
|
+
const config = {
|
|
37
|
+
create,
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
docs: {
|
|
41
|
+
description: 'Disallow `async` `describe` callbacks.',
|
|
42
|
+
recommended: 'unopinionated',
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
messages,
|
|
46
|
+
languages: ['js/js'],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default config;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
import containsSuspensionPoint from './utils/contains-suspension-point.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'no-async-fn-without-await/error';
|
|
5
|
+
const MESSAGE_ID_SUGGESTION = 'no-async-fn-without-await/suggestion';
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID]: 'Async test/hook function has no `await` expression.',
|
|
9
|
+
[MESSAGE_ID_SUGGESTION]: 'Remove the `async` keyword.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
13
|
+
const create = context => {
|
|
14
|
+
const {sourceCode} = context;
|
|
15
|
+
const imports = resolveImports(context);
|
|
16
|
+
if (!imports.isTestFile) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
const parsed = parseTestCall(node, imports);
|
|
22
|
+
// Suites are handled by `no-async-describe`, which forbids an async `describe` callback
|
|
23
|
+
// outright (the runner never awaits it), so skip them here to avoid a duplicate report.
|
|
24
|
+
if (!parsed || parsed.kind === 'suite') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const callback = getTestCallback(node);
|
|
29
|
+
if (!callback?.async) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if the async function body contains any suspension point at its own level.
|
|
34
|
+
// containsSuspensionPoint does not descend into nested functions.
|
|
35
|
+
if (containsSuspensionPoint(callback.body, sourceCode.visitorKeys)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const asyncToken = sourceCode.getFirstToken(callback, token => token.value === 'async');
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
node: asyncToken,
|
|
43
|
+
messageId: MESSAGE_ID,
|
|
44
|
+
suggest: [
|
|
45
|
+
{
|
|
46
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
47
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
48
|
+
fix(fixer) {
|
|
49
|
+
const nextToken = sourceCode.getTokenAfter(asyncToken);
|
|
50
|
+
return fixer.removeRange([sourceCode.getRange(asyncToken)[0], sourceCode.getRange(nextToken)[0]]);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
59
|
+
const config = {
|
|
60
|
+
create,
|
|
61
|
+
meta: {
|
|
62
|
+
type: 'suggestion',
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Disallow async test/hook functions that have no `await` expression.',
|
|
65
|
+
recommended: 'unopinionated',
|
|
66
|
+
},
|
|
67
|
+
hasSuggestions: true,
|
|
68
|
+
schema: [],
|
|
69
|
+
messages,
|
|
70
|
+
languages: ['js/js'],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default config;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
getEffectiveArity,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'no-callback-and-promise';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'A {{kind}} cannot use both a callback parameter and a Promise; this `async` function also declares a callback.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
15
|
+
const create = context => {
|
|
16
|
+
const imports = resolveImports(context);
|
|
17
|
+
if (!imports.isTestFile) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
context.on('CallExpression', node => {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
// Suite (`describe`/`suite`) callbacks receive a `SuiteContext`, never a `done` callback.
|
|
24
|
+
if (parsed?.kind !== 'test' && parsed?.kind !== 'hook') {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const callback = getTestCallback(node);
|
|
29
|
+
if (!callback?.async || getEffectiveArity(callback.params) < 2) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
node: callback.params[1],
|
|
35
|
+
messageId: MESSAGE_ID,
|
|
36
|
+
data: {kind: parsed.kind === 'hook' ? 'hook' : 'test'},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
42
|
+
const config = {
|
|
43
|
+
create,
|
|
44
|
+
meta: {
|
|
45
|
+
type: 'problem',
|
|
46
|
+
docs: {
|
|
47
|
+
description: 'Disallow a test or hook from using both a callback and a Promise.',
|
|
48
|
+
recommended: 'unopinionated',
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
messages,
|
|
52
|
+
languages: ['js/js'],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default config;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import getComments from './utils/get-comments.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-commented-tests/error';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Use `.skip()` or remove the commented-out test instead of commenting it out.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Matches lines that look like commented-out test/hook calls from node:test.
|
|
10
|
+
// Anchored at start-of-line (with optional leading whitespace and block comment asterisk).
|
|
11
|
+
// Matches: test(, it(, describe(, suite(, before(, after(, beforeEach(, afterEach(
|
|
12
|
+
// and dotted modifier variants like test.only(, it.skip(, describe.todo(, etc.
|
|
13
|
+
// Only the real node:test modifiers are allowed in the chain, so unrelated method calls like
|
|
14
|
+
// `it.each(` or `test.config(` are not misidentified as commented-out tests.
|
|
15
|
+
const COMMENTED_TEST_PATTERN = /^\s*\*?\s*(?:test|it|describe|suite|before|after|beforeEach|afterEach)\s*(?:\.\s*(?:only|skip|todo)\s*)*\(/v;
|
|
16
|
+
|
|
17
|
+
// Reports the first line of the comment that looks like a commented-out test.
|
|
18
|
+
function reportFirstMatch(context, comment) {
|
|
19
|
+
const lines = comment.value.split('\n');
|
|
20
|
+
const commentStartLine = context.sourceCode.getLoc(comment).start.line;
|
|
21
|
+
for (const [index, line] of lines.entries()) {
|
|
22
|
+
if (COMMENTED_TEST_PATTERN.test(line)) {
|
|
23
|
+
context.report({
|
|
24
|
+
loc: {
|
|
25
|
+
line: commentStartLine + index,
|
|
26
|
+
column: 0,
|
|
27
|
+
},
|
|
28
|
+
messageId: MESSAGE_ID,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
36
|
+
const create = context => {
|
|
37
|
+
context.on('Program:exit', () => {
|
|
38
|
+
for (const comment of getComments(context)) {
|
|
39
|
+
// Skip JSDoc-style block comments (/** ... */).
|
|
40
|
+
if (comment.type === 'Block' && context.sourceCode.getText(comment).startsWith('/**')) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reportFirstMatch(context, comment);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
50
|
+
const config = {
|
|
51
|
+
create,
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'suggestion',
|
|
54
|
+
docs: {
|
|
55
|
+
description: 'Disallow commented-out tests.',
|
|
56
|
+
recommended: false,
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
messages,
|
|
60
|
+
languages: ['js/js'],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default config;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
} from './utils/node-test.js';
|
|
6
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'no-compound-assertion';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'Split this compound assertion into separate assertions so the failing operand is clear.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getConjunctionOperands(node) {
|
|
15
|
+
const unwrappedNode = unwrapTypeScriptExpression(node);
|
|
16
|
+
|
|
17
|
+
if (unwrappedNode.type !== 'LogicalExpression' || unwrappedNode.operator !== '&&') {
|
|
18
|
+
return [node];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
...getConjunctionOperands(unwrappedNode.left),
|
|
23
|
+
...getConjunctionOperands(unwrappedNode.right),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getIndent(sourceCode, node) {
|
|
28
|
+
const prefix = sourceCode.lines[sourceCode.getLoc(node).start.line - 1].slice(0, sourceCode.getLoc(node).start.column);
|
|
29
|
+
return /^\s*$/.test(prefix) ? prefix : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasOnlyWhitespaceAfterStatement(sourceCode, node) {
|
|
33
|
+
const location = sourceCode.getLoc(node);
|
|
34
|
+
const suffix = sourceCode.lines[location.end.line - 1].slice(location.end.column);
|
|
35
|
+
return /^\s*$/.test(suffix);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getOperandText(sourceCode, operand) {
|
|
39
|
+
const text = sourceCode.getText(operand);
|
|
40
|
+
return operand.type === 'SequenceExpression' ? `(${text})` : text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildFix({node, operands, sourceCode}) {
|
|
44
|
+
return fixer => {
|
|
45
|
+
if (
|
|
46
|
+
node.arguments.length !== 1
|
|
47
|
+
|| node.parent.type !== 'ExpressionStatement'
|
|
48
|
+
|| !['Program', 'BlockStatement'].includes(node.parent.parent.type)
|
|
49
|
+
|| sourceCode.getCommentsInside(node.parent).length > 0
|
|
50
|
+
|| !hasOnlyWhitespaceAfterStatement(sourceCode, node.parent)
|
|
51
|
+
) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const callee = sourceCode.getText(node.callee);
|
|
56
|
+
const indent = getIndent(sourceCode, node.parent);
|
|
57
|
+
if (indent === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const replacement = operands
|
|
62
|
+
.map(operand => `${callee}(${getOperandText(sourceCode, operand)});`)
|
|
63
|
+
.join(`\n${indent}`);
|
|
64
|
+
|
|
65
|
+
return fixer.replaceText(node.parent, replacement);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
70
|
+
const create = context => {
|
|
71
|
+
const {sourceCode} = context;
|
|
72
|
+
const imports = resolveImports(context);
|
|
73
|
+
if (!imports.isAssertOrTestFile) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const contextTracker = createContextTracker(imports, {trackHooks: true});
|
|
78
|
+
|
|
79
|
+
context.on('CallExpression', node => {
|
|
80
|
+
contextTracker.update(node);
|
|
81
|
+
|
|
82
|
+
const assertion = parseAssertionCall(node, imports);
|
|
83
|
+
if (assertion?.method !== 'ok') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (assertion.contextReceiver && !contextTracker.isContextIdentifier(assertion.contextReceiver)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [firstArgument] = node.arguments;
|
|
92
|
+
if (!firstArgument || firstArgument.type === 'SpreadElement') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const argument = unwrapTypeScriptExpression(firstArgument);
|
|
97
|
+
if (argument.type !== 'LogicalExpression' || argument.operator !== '&&') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const operands = getConjunctionOperands(argument);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
node,
|
|
105
|
+
messageId: MESSAGE_ID,
|
|
106
|
+
fix: buildFix({node, operands, sourceCode}),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
context.onExit('CallExpression', node => {
|
|
111
|
+
contextTracker.leave(node);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
116
|
+
const config = {
|
|
117
|
+
create,
|
|
118
|
+
meta: {
|
|
119
|
+
type: 'suggestion',
|
|
120
|
+
docs: {
|
|
121
|
+
description: 'Disallow compound truthiness assertions.',
|
|
122
|
+
recommended: 'unopinionated',
|
|
123
|
+
},
|
|
124
|
+
fixable: 'code',
|
|
125
|
+
schema: [],
|
|
126
|
+
messages,
|
|
127
|
+
languages: ['js/js'],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default config;
|