@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,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import {isRegexLiteral, isBooleanLiteral} from './ast/index.js';
|
|
8
|
+
import {isParenthesized} from './utils/index.js';
|
|
9
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
10
|
+
|
|
11
|
+
const MESSAGE_ID = 'prefer-assert-match/error';
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
[MESSAGE_ID]: 'Prefer `assert.{{method}}()` over asserting `{{pattern}}` results.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
Determine whether a node is a RegExp (regex literal or `new RegExp()` / `RegExp()` call).
|
|
19
|
+
We deliberately keep this simple: only regex literals and direct constructor calls.
|
|
20
|
+
We do not follow variable references to avoid false positives.
|
|
21
|
+
*/
|
|
22
|
+
function isRegExp(node) {
|
|
23
|
+
if (!node) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isRegexLiteral(node)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `new RegExp(...)` or `RegExp(...)`
|
|
32
|
+
return (node.type === 'NewExpression' || node.type === 'CallExpression')
|
|
33
|
+
&& node.callee.type === 'Identifier'
|
|
34
|
+
&& node.callee.name === 'RegExp';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
Parse a `re.test(str)` or `str.match(re)` call.
|
|
39
|
+
Returns `{regex, string, methodName}` or `undefined`.
|
|
40
|
+
|
|
41
|
+
`String#search` is intentionally not handled: it returns the match index (`-1` for no match),
|
|
42
|
+
so `assert.ok(str.search(re))` is truthy for *no* match and falsy for a match at index `0` —
|
|
43
|
+
the opposite polarity of `re.test()` / `str.match()`, so it cannot be rewritten to `assert.match`.
|
|
44
|
+
*/
|
|
45
|
+
function parseRegexCall(node) {
|
|
46
|
+
if (
|
|
47
|
+
node.type !== 'CallExpression'
|
|
48
|
+
|| node.callee.type !== 'MemberExpression'
|
|
49
|
+
|| node.callee.computed
|
|
50
|
+
|| node.callee.property.type !== 'Identifier'
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const {name} = node.callee.property;
|
|
56
|
+
const {object} = node.callee;
|
|
57
|
+
|
|
58
|
+
if (name === 'test' && isRegExp(object)) {
|
|
59
|
+
// `re.test(str)` — first arg is the string
|
|
60
|
+
const stringNode = node.arguments[0];
|
|
61
|
+
if (!stringNode || stringNode.type === 'SpreadElement') {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {regex: object, string: stringNode, methodName: 'test'};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (name === 'match' && node.arguments.length > 0) {
|
|
69
|
+
// `str.match(re)` — first arg should be the regex
|
|
70
|
+
const regexArgument = node.arguments[0];
|
|
71
|
+
if (!regexArgument || regexArgument.type === 'SpreadElement') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!isRegExp(regexArgument)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {regex: regexArgument, string: object, methodName: name};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
Build a fixer that replaces `assert.ok(re.test(str))` → `assert.match(str, re)`.
|
|
85
|
+
Handles the callee method rename and the argument rewrite.
|
|
86
|
+
*/
|
|
87
|
+
function buildFix({node, method, regexNode, stringNode, extraArgsToRemove, sourceCode}) {
|
|
88
|
+
return function * fix(fixer) {
|
|
89
|
+
const assertCallee = node.callee;
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
assertCallee.type === 'MemberExpression'
|
|
93
|
+
&& !assertCallee.computed
|
|
94
|
+
&& assertCallee.property.type === 'Identifier'
|
|
95
|
+
) {
|
|
96
|
+
yield fixer.replaceText(assertCallee.property, method);
|
|
97
|
+
} else {
|
|
98
|
+
// Named import: the callee is an Identifier — we can't rename it without
|
|
99
|
+
// knowing the local name. Just bail on the fix.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const regexText = sourceCode.getText(regexNode);
|
|
104
|
+
const stringText = sourceCode.getText(stringNode);
|
|
105
|
+
yield fixer.replaceText(node.arguments[0], `${stringText}, ${regexText}`);
|
|
106
|
+
|
|
107
|
+
// Remove any extra arguments (e.g. the boolean literal in strictEqual).
|
|
108
|
+
for (const argument of extraArgsToRemove) {
|
|
109
|
+
const tokenBefore = sourceCode.getTokenBefore(argument, {includeComments: false});
|
|
110
|
+
yield fixer.removeRange([sourceCode.getRange(tokenBefore)[0], sourceCode.getRange(argument)[1]]);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
Whether the call can be safely rewritten: a member-expression callee (a named import can't be
|
|
117
|
+
renamed without knowing the local name), no comments inside the call that the argument
|
|
118
|
+
rewrite/removal could drop, and no parenthesized arguments. The rewrite replaces the first
|
|
119
|
+
argument's inner node and removes the boolean argument up to its own inner node, so surrounding
|
|
120
|
+
parentheses on either would be left behind as stray tokens.
|
|
121
|
+
*/
|
|
122
|
+
function canAutofix(node, context) {
|
|
123
|
+
return node.callee.type === 'MemberExpression'
|
|
124
|
+
&& context.sourceCode.getCommentsInside(node).length === 0
|
|
125
|
+
&& node.arguments.every(argument => !isParenthesized(argument, context));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Build the problem object for a detected regex-result assertion. */
|
|
129
|
+
function makeProblem({node, assertMethod, regexCall, extraArgsToRemove, context}) {
|
|
130
|
+
const fix = canAutofix(node, context)
|
|
131
|
+
? buildFix({
|
|
132
|
+
node, method: assertMethod, regexNode: regexCall.regex, stringNode: regexCall.string, extraArgsToRemove, sourceCode: context.sourceCode,
|
|
133
|
+
})
|
|
134
|
+
: undefined;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
node,
|
|
138
|
+
messageId: MESSAGE_ID,
|
|
139
|
+
data: {method: assertMethod, pattern: `${regexCall.methodName}()`},
|
|
140
|
+
fix,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
Handle the equality forms `assert.strictEqual`/`equal`/`notStrictEqual`/`notEqual`, where one
|
|
146
|
+
argument is `re.test(str)` and the other a boolean literal, in either order.
|
|
147
|
+
*/
|
|
148
|
+
function getEqualityProblem(node, method, context) {
|
|
149
|
+
if (node.arguments.length < 2) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const [firstArgument, secondArgument] = node.arguments;
|
|
154
|
+
const unwrappedFirst = unwrapTypeScriptExpression(firstArgument);
|
|
155
|
+
const unwrappedSecond = unwrapTypeScriptExpression(secondArgument);
|
|
156
|
+
|
|
157
|
+
let regexCall = parseRegexCall(unwrappedFirst);
|
|
158
|
+
let booleanLiteral = unwrappedSecond;
|
|
159
|
+
if (!(regexCall && isBooleanLiteral(booleanLiteral))) {
|
|
160
|
+
regexCall = parseRegexCall(unwrappedSecond);
|
|
161
|
+
booleanLiteral = unwrappedFirst;
|
|
162
|
+
if (!(regexCall && isBooleanLiteral(booleanLiteral))) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isNegated = method === 'notStrictEqual' || method === 'notEqual';
|
|
168
|
+
// `strictEqual(re.test(str), true)` asserts a match; negating the method or comparing to
|
|
169
|
+
// `false` each flip the meaning.
|
|
170
|
+
const isMatches = (booleanLiteral.value === true) !== isNegated;
|
|
171
|
+
const assertMethod = isMatches ? 'match' : 'doesNotMatch';
|
|
172
|
+
|
|
173
|
+
// `buildFix` collapses the two arguments into `str, re` regardless of their original order.
|
|
174
|
+
return makeProblem({
|
|
175
|
+
node, assertMethod, regexCall, extraArgsToRemove: [secondArgument], context,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
180
|
+
const create = context => {
|
|
181
|
+
const imports = resolveImports(context);
|
|
182
|
+
|
|
183
|
+
if (!imports.isAssertOrTestFile) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
188
|
+
|
|
189
|
+
context.on('CallExpression', node => {
|
|
190
|
+
tracker.update(node);
|
|
191
|
+
|
|
192
|
+
const parsed = parseAssertionCall(node, imports);
|
|
193
|
+
if (!parsed || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const {method} = parsed;
|
|
198
|
+
|
|
199
|
+
// `assert.ok(re.test(str))` / `assert.ok(str.match(re))`
|
|
200
|
+
// `assert.ok(!re.test(str))` / `assert.ok(!str.match(re))`
|
|
201
|
+
if (method === 'ok') {
|
|
202
|
+
const firstArgument = node.arguments[0];
|
|
203
|
+
if (!firstArgument) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let target = unwrapTypeScriptExpression(firstArgument);
|
|
208
|
+
let assertMethod = 'match';
|
|
209
|
+
|
|
210
|
+
// Negated: `assert.ok(!re.test(str))` asserts no match.
|
|
211
|
+
if (target.type === 'UnaryExpression' && target.operator === '!') {
|
|
212
|
+
target = unwrapTypeScriptExpression(target.argument);
|
|
213
|
+
assertMethod = 'doesNotMatch';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const regexCall = parseRegexCall(target);
|
|
217
|
+
if (!regexCall) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return makeProblem({
|
|
222
|
+
node, assertMethod, regexCall, extraArgsToRemove: [], context,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// `assert.strictEqual`/`equal`/`notStrictEqual`/`notEqual(re.test(str), true/false)`
|
|
227
|
+
if (['strictEqual', 'equal', 'notStrictEqual', 'notEqual'].includes(method)) {
|
|
228
|
+
return getEqualityProblem(node, method, context);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
context.onExit('CallExpression', node => {
|
|
233
|
+
tracker.leave(node);
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
238
|
+
const config = {
|
|
239
|
+
create,
|
|
240
|
+
meta: {
|
|
241
|
+
type: 'suggestion',
|
|
242
|
+
docs: {
|
|
243
|
+
description: 'Prefer `assert.match()`/`assert.doesNotMatch()` over asserting `RegExp#test()` / `String#match()` results.',
|
|
244
|
+
recommended: 'unopinionated',
|
|
245
|
+
},
|
|
246
|
+
fixable: 'code',
|
|
247
|
+
schema: [],
|
|
248
|
+
messages,
|
|
249
|
+
languages: ['js/js'],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export default config;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import containsSuspensionPoint from './utils/contains-suspension-point.js';
|
|
3
|
+
import isFunction from './ast/is-function.js';
|
|
4
|
+
|
|
5
|
+
const MESSAGE_ID_SYNC = 'prefer-assert-throws/sync';
|
|
6
|
+
const MESSAGE_ID_ASYNC = 'prefer-assert-throws/async';
|
|
7
|
+
|
|
8
|
+
const messages = {
|
|
9
|
+
[MESSAGE_ID_SYNC]: 'Prefer `assert.throws()` over try/catch with an assertion.',
|
|
10
|
+
[MESSAGE_ID_ASYNC]: 'Prefer `assert.rejects()` over try/catch with an assertion.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Return true if the catch block contains at least one assertion call anywhere inside it. `assert.fail()`
|
|
15
|
+
is excluded: a bare `fail()` in a catch asserts that the try body should *not* throw, which is the
|
|
16
|
+
opposite of the `assert.throws()` pattern this rule suggests.
|
|
17
|
+
*/
|
|
18
|
+
function catchHasAssertion(catchClause, imports, visitorKeys) {
|
|
19
|
+
function walk(node) {
|
|
20
|
+
// Do not descend into nested functions — an assertion defined there is not executed by the
|
|
21
|
+
// catch itself, so it does not make this try/catch the `assert.throws()` pattern.
|
|
22
|
+
if (isFunction(node)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const assertion = node.type === 'CallExpression' ? parseAssertionCall(node, imports) : undefined;
|
|
27
|
+
if (assertion && assertion.method !== 'fail') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
32
|
+
const child = node[key];
|
|
33
|
+
for (const childNode of Array.isArray(child) ? child : [child]) {
|
|
34
|
+
if (childNode?.type && walk(childNode)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return walk(catchClause.body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
47
|
+
const create = context => {
|
|
48
|
+
const imports = resolveImports(context);
|
|
49
|
+
|
|
50
|
+
if (!imports.isAssertOrTestFile) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const {visitorKeys} = context.sourceCode;
|
|
55
|
+
|
|
56
|
+
context.on('TryStatement', node => {
|
|
57
|
+
// Must have a catch clause — otherwise there is no assertion to move.
|
|
58
|
+
if (!node.handler) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The try body must have at least one statement (the throwing code).
|
|
63
|
+
if (node.block.body.length === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The catch clause must contain an assertion.
|
|
68
|
+
if (!catchHasAssertion(node.handler, imports, visitorKeys)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A suspension point (`await`, `for await`) in the try body makes it async.
|
|
73
|
+
const isAsync = node.block.body.some(statement => containsSuspensionPoint(statement, visitorKeys));
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
node,
|
|
77
|
+
messageId: isAsync ? MESSAGE_ID_ASYNC : MESSAGE_ID_SYNC,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
83
|
+
const config = {
|
|
84
|
+
create,
|
|
85
|
+
meta: {
|
|
86
|
+
type: 'suggestion',
|
|
87
|
+
docs: {
|
|
88
|
+
description: 'Prefer `assert.throws()`/`assert.rejects()` over try/catch with an assertion.',
|
|
89
|
+
recommended: true,
|
|
90
|
+
},
|
|
91
|
+
schema: [],
|
|
92
|
+
messages,
|
|
93
|
+
languages: ['js/js'],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default config;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'prefer-async-await/error';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: 'Prefer async/await instead of returning a Promise.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
Collect all `return` statements that are directly inside `block`, descending
|
|
12
|
+
into control-flow nodes but not into nested functions.
|
|
13
|
+
|
|
14
|
+
@param {import('estree').BlockStatement} block
|
|
15
|
+
@returns {import('estree').ReturnStatement[]}
|
|
16
|
+
*/
|
|
17
|
+
function findReturnStatements(block) {
|
|
18
|
+
const results = [];
|
|
19
|
+
|
|
20
|
+
function walk(node) {
|
|
21
|
+
if (!node) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (node.type) {
|
|
26
|
+
case 'ReturnStatement': {
|
|
27
|
+
results.push(node);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'BlockStatement': {
|
|
32
|
+
for (const statement of node.body) {
|
|
33
|
+
walk(statement);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case 'IfStatement': {
|
|
40
|
+
walk(node.consequent);
|
|
41
|
+
walk(node.alternate);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'SwitchStatement': {
|
|
46
|
+
for (const switchCase of node.cases) {
|
|
47
|
+
for (const statement of switchCase.consequent) {
|
|
48
|
+
walk(statement);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case 'TryStatement': {
|
|
56
|
+
walk(node.block);
|
|
57
|
+
if (node.handler) {
|
|
58
|
+
walk(node.handler.body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
walk(node.finalizer);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'ForStatement':
|
|
66
|
+
case 'ForInStatement':
|
|
67
|
+
case 'ForOfStatement':
|
|
68
|
+
case 'WhileStatement':
|
|
69
|
+
case 'DoWhileStatement':
|
|
70
|
+
case 'LabeledStatement':
|
|
71
|
+
case 'WithStatement': {
|
|
72
|
+
walk(node.body);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Do not descend into nested functions
|
|
77
|
+
default: {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
walk(block);
|
|
84
|
+
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
Check whether a node contains a `.then(...)` call anywhere in its `.then`/`.catch`/`.finally`
|
|
90
|
+
member chain.
|
|
91
|
+
|
|
92
|
+
@param {import('estree').Node | null | undefined} node
|
|
93
|
+
@returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
function containsThen(node) {
|
|
96
|
+
while (node) {
|
|
97
|
+
if (node.type === 'ChainExpression') {
|
|
98
|
+
node = node.expression;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
node.type !== 'CallExpression'
|
|
104
|
+
|| node.callee.type !== 'MemberExpression'
|
|
105
|
+
) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const {callee} = node;
|
|
110
|
+
if (
|
|
111
|
+
callee.property.type === 'Identifier'
|
|
112
|
+
&& callee.property.name === 'then'
|
|
113
|
+
) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
node = callee.object;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
124
|
+
const create = context => {
|
|
125
|
+
const {sourceCode} = context;
|
|
126
|
+
const imports = resolveImports(context);
|
|
127
|
+
if (!imports.isTestFile) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
context.on('CallExpression', node => {
|
|
132
|
+
const parsed = parseTestCall(node, imports);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// `describe`/`suite` callbacks run synchronously and are never awaited, so returning a
|
|
138
|
+
// Promise from them is meaningless and converting to async/await would not help.
|
|
139
|
+
if (parsed.kind === 'suite') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const callback = getTestCallback(node);
|
|
144
|
+
// Only flag non-async functions with a block body (arrow shorthand already returns)
|
|
145
|
+
if (!callback || callback.async || callback.body.type !== 'BlockStatement') {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const returnStatements = findReturnStatements(callback.body);
|
|
150
|
+
if (returnStatements.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Flag if any return statement returns a .then() call chain
|
|
155
|
+
for (const returnStatement of returnStatements) {
|
|
156
|
+
if (containsThen(returnStatement.argument)) {
|
|
157
|
+
return {
|
|
158
|
+
node: callback,
|
|
159
|
+
messageId: MESSAGE_ID,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Flag if any return statement returns a variable that was assigned from a .then() call
|
|
165
|
+
for (const returnStatement of returnStatements) {
|
|
166
|
+
if (returnStatement.argument?.type !== 'Identifier') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const variable = findVariable(sourceCode.getScope(returnStatement), returnStatement.argument);
|
|
171
|
+
if (!variable) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const assignedFromThen = variable.defs.some(
|
|
176
|
+
definition => definition.type === 'Variable' && containsThen(definition.node.init),
|
|
177
|
+
);
|
|
178
|
+
if (assignedFromThen) {
|
|
179
|
+
return {
|
|
180
|
+
node: callback,
|
|
181
|
+
messageId: MESSAGE_ID,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
189
|
+
const config = {
|
|
190
|
+
create,
|
|
191
|
+
meta: {
|
|
192
|
+
type: 'suggestion',
|
|
193
|
+
docs: {
|
|
194
|
+
description: 'Prefer async/await over returning a Promise.',
|
|
195
|
+
recommended: true,
|
|
196
|
+
},
|
|
197
|
+
schema: [],
|
|
198
|
+
messages,
|
|
199
|
+
languages: ['js/js'],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default config;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {resolveImports, isGlobalMock} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'prefer-context-mock';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Prefer `t.mock.{{accessor}}` over the global `mock.{{accessor}}`, which is not automatically restored between tests.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Accessors that create state which the global `mock` does not auto-restore.
|
|
10
|
+
const STATEFUL_ACCESSORS = new Set(['fn', 'method', 'getter', 'setter', 'property', 'module', 'timers']);
|
|
11
|
+
|
|
12
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
13
|
+
const create = context => {
|
|
14
|
+
const imports = resolveImports(context);
|
|
15
|
+
if (!imports.isTestFile) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
context.on('CallExpression', node => {
|
|
20
|
+
let member = node.callee;
|
|
21
|
+
while (member.type === 'MemberExpression') {
|
|
22
|
+
if (isGlobalMock(member.object, imports) && !member.computed && member.property.type === 'Identifier') {
|
|
23
|
+
const accessor = member.property.name;
|
|
24
|
+
if (STATEFUL_ACCESSORS.has(accessor)) {
|
|
25
|
+
return {
|
|
26
|
+
node,
|
|
27
|
+
messageId: MESSAGE_ID,
|
|
28
|
+
data: {accessor},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
member = member.object;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
41
|
+
const config = {
|
|
42
|
+
create,
|
|
43
|
+
meta: {
|
|
44
|
+
type: 'suggestion',
|
|
45
|
+
docs: {
|
|
46
|
+
description: 'Prefer the test context `t.mock` over the global `mock`.',
|
|
47
|
+
recommended: true,
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
messages,
|
|
51
|
+
languages: ['js/js'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default config;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {resolveImports, createContextTracker} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID_ERROR = 'prefer-diagnostic/error';
|
|
4
|
+
const MESSAGE_ID_SUGGESTION = 'prefer-diagnostic/suggestion';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID_ERROR]: 'Prefer `{{context}}.diagnostic()` over `console.{{method}}()` inside a test, so the message is attached to the test as a TAP diagnostic.',
|
|
8
|
+
[MESSAGE_ID_SUGGESTION]: 'Replace with `{{context}}.diagnostic()`.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const CONSOLE_METHODS = new Set(['log', 'info', 'debug']);
|
|
12
|
+
|
|
13
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
14
|
+
const create = context => {
|
|
15
|
+
const imports = resolveImports(context);
|
|
16
|
+
if (!imports.isTestFile) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tracker = createContextTracker(imports);
|
|
21
|
+
|
|
22
|
+
context.on('CallExpression', node => {
|
|
23
|
+
tracker.update(node);
|
|
24
|
+
|
|
25
|
+
const {callee} = node;
|
|
26
|
+
if (
|
|
27
|
+
callee.type !== 'MemberExpression'
|
|
28
|
+
|| callee.computed
|
|
29
|
+
|| callee.object.type !== 'Identifier'
|
|
30
|
+
|| callee.object.name !== 'console'
|
|
31
|
+
|| callee.property.type !== 'Identifier'
|
|
32
|
+
|| !CONSOLE_METHODS.has(callee.property.name)
|
|
33
|
+
) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const contextName = tracker.current();
|
|
38
|
+
if (!contextName) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The context parameter is only in scope inside the test callback. Skip console calls in the
|
|
43
|
+
// title/options arguments (visited before the callback), where the context does not exist.
|
|
44
|
+
const callback = tracker.currentCallback();
|
|
45
|
+
const [callStart, callEnd] = context.sourceCode.getRange(callback);
|
|
46
|
+
const [consoleStart] = context.sourceCode.getRange(node);
|
|
47
|
+
if (consoleStart < callStart || consoleStart >= callEnd) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const method = callee.property.name;
|
|
52
|
+
const data = {context: contextName, method};
|
|
53
|
+
const problem = {
|
|
54
|
+
node: callee,
|
|
55
|
+
messageId: MESSAGE_ID_ERROR,
|
|
56
|
+
data,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// `diagnostic()` takes a single message, so only suggest a rewrite for a single argument.
|
|
60
|
+
if (node.arguments.length === 1) {
|
|
61
|
+
problem.suggest = [
|
|
62
|
+
{
|
|
63
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
64
|
+
data,
|
|
65
|
+
fix: fixer => fixer.replaceText(callee, `${contextName}.diagnostic`),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return problem;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
context.onExit('CallExpression', node => {
|
|
74
|
+
tracker.leave(node);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
79
|
+
const config = {
|
|
80
|
+
create,
|
|
81
|
+
meta: {
|
|
82
|
+
type: 'suggestion',
|
|
83
|
+
docs: {
|
|
84
|
+
description: 'Prefer the test context `diagnostic()` over `console` inside tests.',
|
|
85
|
+
recommended: false,
|
|
86
|
+
},
|
|
87
|
+
hasSuggestions: true,
|
|
88
|
+
schema: [],
|
|
89
|
+
messages,
|
|
90
|
+
languages: ['js/js'],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default config;
|