eslint-node-test 0.0.1
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/configs/core-rule-replacements.js +9 -0
- package/configs/flat-config-base.js +9 -0
- package/index.d.ts +11 -0
- package/index.js +51 -0
- package/license +9 -0
- package/package.json +106 -0
- package/readme.md +143 -0
- package/rules/assertion-arguments.js +134 -0
- package/rules/ast/call-or-new-expression.js +100 -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 +98 -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-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 +86 -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 +132 -0
- package/rules/index.js +66 -0
- package/rules/max-assertions.js +87 -0
- package/rules/max-nested-describe.js +70 -0
- package/rules/no-assert-in-describe.js +51 -0
- package/rules/no-assert-in-hook.js +51 -0
- package/rules/no-assert-throws-async.js +114 -0
- package/rules/no-assert-throws-string.js +65 -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 +59 -0
- package/rules/no-conditional-assertion.js +101 -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-done-callback.js +58 -0
- package/rules/no-duplicate-hooks.js +75 -0
- package/rules/no-export.js +79 -0
- package/rules/no-identical-assertion-arguments.js +71 -0
- package/rules/no-identical-title.js +101 -0
- package/rules/no-incorrect-deep-equal.js +100 -0
- package/rules/no-incorrect-strict-equal.js +86 -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-skip-test.js +11 -0
- package/rules/no-skip-without-reason.js +88 -0
- package/rules/no-skip-without-return.js +127 -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 +114 -0
- package/rules/no-todo-test.js +11 -0
- package/rules/no-unawaited-rejects.js +74 -0
- package/rules/no-unawaited-subtest.js +66 -0
- package/rules/no-unknown-test-options.js +77 -0
- package/rules/no-useless-assertion.js +47 -0
- package/rules/prefer-assert-match.js +245 -0
- package/rules/prefer-assert-throws.js +90 -0
- package/rules/prefer-async-await.js +203 -0
- package/rules/prefer-context-mock.js +59 -0
- package/rules/prefer-diagnostic.js +94 -0
- package/rules/prefer-equality-assertion.js +101 -0
- package/rules/prefer-hooks-on-top.js +73 -0
- package/rules/prefer-lowercase-title.js +119 -0
- package/rules/prefer-mock-method.js +115 -0
- package/rules/prefer-strict-assert.js +69 -0
- package/rules/prefer-test-context-assert.js +125 -0
- package/rules/prefer-todo.js +98 -0
- package/rules/require-assertion.js +92 -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-throws-expectation.js +52 -0
- package/rules/require-top-level-describe.js +89 -0
- package/rules/rule/index.js +9 -0
- package/rules/rule/to-eslint-create.js +37 -0
- package/rules/rule/to-eslint-listener.js +40 -0
- package/rules/rule/to-eslint-problem.js +38 -0
- package/rules/rule/to-eslint-rule-fixer.js +49 -0
- package/rules/rule/to-eslint-rule.js +38 -0
- package/rules/rule/to-eslint-rules.js +10 -0
- package/rules/rule/unicorn-context.js +36 -0
- package/rules/rule/unicorn-listeners.js +65 -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/escape-string.js +24 -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 +16 -0
- package/rules/utils/is-conditional-branch.js +37 -0
- package/rules/utils/is-promise-type.js +28 -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 +713 -0
- package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +80 -0
- package/rules/utils/parentheses/iterate-surrounding-parentheses.js +82 -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,74 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import {getEnclosingFunction} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'no-unawaited-rejects/error';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: '`assert.{{method}}()` must be awaited or returned.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const REJECTS_METHODS = new Set(['rejects', 'doesNotReject']);
|
|
11
|
+
|
|
12
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
13
|
+
const create = context => {
|
|
14
|
+
const imports = resolveImports(context);
|
|
15
|
+
|
|
16
|
+
if (!imports.isAssertOrTestFile) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
const parsed = parseAssertionCall(node, imports);
|
|
22
|
+
if (!parsed) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!REJECTS_METHODS.has(parsed.method)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The call must be an ExpressionStatement (i.e. bare call, not awaited/returned/assigned).
|
|
31
|
+
const {parent} = node;
|
|
32
|
+
if (!parent || parent.type !== 'ExpressionStatement') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
37
|
+
const isInAsyncFunction = enclosingFunction?.async === true;
|
|
38
|
+
|
|
39
|
+
if (isInAsyncFunction) {
|
|
40
|
+
// Can autofix with `await`.
|
|
41
|
+
return {
|
|
42
|
+
node,
|
|
43
|
+
messageId: MESSAGE_ID,
|
|
44
|
+
data: {method: parsed.method},
|
|
45
|
+
fix: fixer => fixer.insertTextBefore(node, 'await '),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Not in an async function — report without a fix (await would be a syntax error here).
|
|
50
|
+
return {
|
|
51
|
+
node,
|
|
52
|
+
messageId: MESSAGE_ID,
|
|
53
|
+
data: {method: parsed.method},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
59
|
+
const config = {
|
|
60
|
+
create,
|
|
61
|
+
meta: {
|
|
62
|
+
type: 'problem',
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Require `assert.rejects()`/`assert.doesNotReject()` to be awaited or returned.',
|
|
65
|
+
recommended: 'unopinionated',
|
|
66
|
+
},
|
|
67
|
+
fixable: 'code',
|
|
68
|
+
schema: [],
|
|
69
|
+
messages,
|
|
70
|
+
languages: ['js/js'],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default config;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {resolveImports, createContextTracker, getSubtestReceiver} from './utils/node-test.js';
|
|
2
|
+
import {getEnclosingFunction} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'no-unawaited-subtest';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: 'Subtest `{{name}}.test()` must be awaited or returned, otherwise it is cancelled when the parent test finishes.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
11
|
+
const create = context => {
|
|
12
|
+
const imports = resolveImports(context);
|
|
13
|
+
if (!imports.isTestFile) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tracker = createContextTracker(imports);
|
|
18
|
+
|
|
19
|
+
context.on('CallExpression', node => {
|
|
20
|
+
// Whether this is a floating subtest must be decided against the current stack,
|
|
21
|
+
// before this call pushes its own context.
|
|
22
|
+
const subtest = tracker.isSubtestCall(node);
|
|
23
|
+
|
|
24
|
+
let problem;
|
|
25
|
+
if (subtest && node.parent.type === 'ExpressionStatement') {
|
|
26
|
+
const {name} = getSubtestReceiver(node);
|
|
27
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
28
|
+
|
|
29
|
+
problem = {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
data: {name},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// `await` is only valid (and a behavior-preserving fix) inside an async function.
|
|
36
|
+
if (enclosingFunction?.async) {
|
|
37
|
+
problem.fix = fixer => fixer.insertTextBefore(node, 'await ');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tracker.update(node);
|
|
42
|
+
return problem;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
context.onExit('CallExpression', node => {
|
|
46
|
+
tracker.leave(node);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
51
|
+
const config = {
|
|
52
|
+
create,
|
|
53
|
+
meta: {
|
|
54
|
+
type: 'problem',
|
|
55
|
+
docs: {
|
|
56
|
+
description: 'Require subtests created with the test context to be awaited or returned.',
|
|
57
|
+
recommended: true,
|
|
58
|
+
},
|
|
59
|
+
fixable: 'code',
|
|
60
|
+
schema: [],
|
|
61
|
+
messages,
|
|
62
|
+
languages: ['js/js'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default config;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestOptions} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-unknown-test-options';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`{{name}}` is not a recognized {{kind}} option.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
The option keys `node:test` recognizes. An unknown key is silently ignored, so a typo like
|
|
11
|
+
`{skp: true}` quietly runs the test as normal. This list tracks the runner and may need
|
|
12
|
+
updating as `node:test` gains options.
|
|
13
|
+
*/
|
|
14
|
+
const TEST_OPTIONS = new Set(['concurrency', 'expectFailure', 'only', 'plan', 'signal', 'skip', 'tags', 'timeout', 'todo']);
|
|
15
|
+
const HOOK_OPTIONS = new Set(['signal', 'timeout']);
|
|
16
|
+
|
|
17
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
18
|
+
const create = context => {
|
|
19
|
+
const imports = resolveImports(context);
|
|
20
|
+
if (!imports.isTestFile) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
context.on('CallExpression', function * (node) {
|
|
25
|
+
const parsed = parseTestCall(node, imports);
|
|
26
|
+
if (!parsed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options = getTestOptions(node);
|
|
31
|
+
if (!options) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const known = parsed.kind === 'hook' ? HOOK_OPTIONS : TEST_OPTIONS;
|
|
36
|
+
|
|
37
|
+
for (const property of options.properties) {
|
|
38
|
+
if (property.type !== 'Property' || property.computed) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let name;
|
|
43
|
+
if (property.key.type === 'Identifier') {
|
|
44
|
+
name = property.key.name;
|
|
45
|
+
} else if (property.key.type === 'Literal' && typeof property.key.value === 'string') {
|
|
46
|
+
name = property.key.value;
|
|
47
|
+
} else {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!known.has(name)) {
|
|
52
|
+
yield {
|
|
53
|
+
node: property.key,
|
|
54
|
+
messageId: MESSAGE_ID,
|
|
55
|
+
data: {name, kind: parsed.kind},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
63
|
+
const config = {
|
|
64
|
+
create,
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'problem',
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Disallow unknown options in test and hook option objects.',
|
|
69
|
+
recommended: true,
|
|
70
|
+
},
|
|
71
|
+
schema: [],
|
|
72
|
+
messages,
|
|
73
|
+
languages: ['js/js'],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default config;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-useless-assertion';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`{{method}}()` is not useful. It catches an error only to rethrow it, so call the code directly instead.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const USELESS_METHODS = new Set(['doesNotThrow', 'doesNotReject']);
|
|
10
|
+
|
|
11
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
12
|
+
const create = context => {
|
|
13
|
+
const imports = resolveImports(context);
|
|
14
|
+
if (!imports.isAssertOrTestFile) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
context.on('CallExpression', node => {
|
|
19
|
+
const parsed = parseAssertionCall(node, imports);
|
|
20
|
+
if (!parsed || !USELESS_METHODS.has(parsed.method)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
node,
|
|
26
|
+
messageId: MESSAGE_ID,
|
|
27
|
+
data: {method: parsed.method},
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
33
|
+
const config = {
|
|
34
|
+
create,
|
|
35
|
+
meta: {
|
|
36
|
+
type: 'suggestion',
|
|
37
|
+
docs: {
|
|
38
|
+
description: 'Disallow `assert.doesNotThrow()` and `assert.doesNotReject()`.',
|
|
39
|
+
recommended: true,
|
|
40
|
+
},
|
|
41
|
+
schema: [],
|
|
42
|
+
messages,
|
|
43
|
+
languages: ['js/js'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default config;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import {isRegexLiteral, isBooleanLiteral} from './ast/index.js';
|
|
3
|
+
import {isParenthesized} from './utils/index.js';
|
|
4
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
5
|
+
|
|
6
|
+
const MESSAGE_ID = 'prefer-assert-match/error';
|
|
7
|
+
|
|
8
|
+
const messages = {
|
|
9
|
+
[MESSAGE_ID]: 'Prefer `assert.{{method}}()` over asserting `{{pattern}}` results.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Determine whether a node is a RegExp (regex literal or `new RegExp()` / `RegExp()` call).
|
|
14
|
+
We deliberately keep this simple: only regex literals and direct constructor calls.
|
|
15
|
+
We do not follow variable references to avoid false positives.
|
|
16
|
+
*/
|
|
17
|
+
function isRegExp(node) {
|
|
18
|
+
if (!node) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (isRegexLiteral(node)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `new RegExp(...)` or `RegExp(...)`
|
|
27
|
+
if (
|
|
28
|
+
(node.type === 'NewExpression' || node.type === 'CallExpression')
|
|
29
|
+
&& node.callee.type === 'Identifier'
|
|
30
|
+
&& node.callee.name === 'RegExp'
|
|
31
|
+
) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
Parse a `re.test(str)` or `str.match(re)` call.
|
|
40
|
+
Returns `{regex, string, methodName}` or `undefined`.
|
|
41
|
+
|
|
42
|
+
`String#search` is intentionally not handled: it returns the match index (`-1` for no match),
|
|
43
|
+
so `assert.ok(str.search(re))` is truthy for *no* match and falsy for a match at index `0` —
|
|
44
|
+
the opposite polarity of `re.test()` / `str.match()`, so it cannot be rewritten to `assert.match`.
|
|
45
|
+
*/
|
|
46
|
+
function parseRegexCall(node) {
|
|
47
|
+
if (
|
|
48
|
+
node.type !== 'CallExpression'
|
|
49
|
+
|| node.callee.type !== 'MemberExpression'
|
|
50
|
+
|| node.callee.computed
|
|
51
|
+
|| node.callee.property.type !== 'Identifier'
|
|
52
|
+
) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const {name} = node.callee.property;
|
|
57
|
+
const {object} = node.callee;
|
|
58
|
+
|
|
59
|
+
if (name === 'test' && isRegExp(object)) {
|
|
60
|
+
// `re.test(str)` — first arg is the string
|
|
61
|
+
const stringNode = node.arguments[0];
|
|
62
|
+
if (!stringNode || stringNode.type === 'SpreadElement') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {regex: object, string: stringNode, methodName: 'test'};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (name === 'match' && node.arguments.length > 0) {
|
|
70
|
+
// `str.match(re)` — first arg should be the regex
|
|
71
|
+
const regexArgument = node.arguments[0];
|
|
72
|
+
if (!regexArgument || regexArgument.type === 'SpreadElement') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!isRegExp(regexArgument)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {regex: regexArgument, string: object, methodName: name};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
Build a fixer that replaces `assert.ok(re.test(str))` → `assert.match(str, re)`.
|
|
86
|
+
Handles the callee method rename and the argument rewrite.
|
|
87
|
+
*/
|
|
88
|
+
function buildFix({node, method, regexNode, stringNode, extraArgsToRemove, sourceCode}) {
|
|
89
|
+
return function * fix(fixer) {
|
|
90
|
+
const assertCallee = node.callee;
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
assertCallee.type === 'MemberExpression'
|
|
94
|
+
&& !assertCallee.computed
|
|
95
|
+
&& assertCallee.property.type === 'Identifier'
|
|
96
|
+
) {
|
|
97
|
+
yield fixer.replaceText(assertCallee.property, method);
|
|
98
|
+
} else {
|
|
99
|
+
// Named import: the callee is an Identifier — we can't rename it without
|
|
100
|
+
// knowing the local name. Just bail on the fix.
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const regexText = sourceCode.getText(regexNode);
|
|
105
|
+
const stringText = sourceCode.getText(stringNode);
|
|
106
|
+
yield fixer.replaceText(node.arguments[0], `${stringText}, ${regexText}`);
|
|
107
|
+
|
|
108
|
+
// Remove any extra arguments (e.g. the boolean literal in strictEqual).
|
|
109
|
+
for (const argument of extraArgsToRemove) {
|
|
110
|
+
const tokenBefore = sourceCode.getTokenBefore(argument, {includeComments: false});
|
|
111
|
+
yield fixer.removeRange([sourceCode.getRange(tokenBefore)[0], sourceCode.getRange(argument)[1]]);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
Whether the call can be safely rewritten: a member-expression callee (a named import can't be
|
|
118
|
+
renamed without knowing the local name), no comments inside the call that the argument
|
|
119
|
+
rewrite/removal could drop, and a non-parenthesized first argument (the rewrite replaces the inner
|
|
120
|
+
node, so surrounding parentheses would be left wrapping the `str, re` pair as a comma expression).
|
|
121
|
+
*/
|
|
122
|
+
function canAutofix(node, context) {
|
|
123
|
+
return node.callee.type === 'MemberExpression'
|
|
124
|
+
&& context.sourceCode.getCommentsInside(node).length === 0
|
|
125
|
+
&& !isParenthesized(node.arguments[0], 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 negated = 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 matches = (booleanLiteral.value === true) !== negated;
|
|
171
|
+
const assertMethod = matches ? '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
|
+
context.on('CallExpression', node => {
|
|
188
|
+
const parsed = parseAssertionCall(node, imports);
|
|
189
|
+
if (!parsed) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const {method} = parsed;
|
|
194
|
+
|
|
195
|
+
// `assert.ok(re.test(str))` / `assert.ok(str.match(re))`
|
|
196
|
+
// `assert.ok(!re.test(str))` / `assert.ok(!str.match(re))`
|
|
197
|
+
if (method === 'ok') {
|
|
198
|
+
const firstArgument = node.arguments[0];
|
|
199
|
+
if (!firstArgument) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let target = unwrapTypeScriptExpression(firstArgument);
|
|
204
|
+
let assertMethod = 'match';
|
|
205
|
+
|
|
206
|
+
// Negated: `assert.ok(!re.test(str))` asserts no match.
|
|
207
|
+
if (target.type === 'UnaryExpression' && target.operator === '!') {
|
|
208
|
+
target = unwrapTypeScriptExpression(target.argument);
|
|
209
|
+
assertMethod = 'doesNotMatch';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const regexCall = parseRegexCall(target);
|
|
213
|
+
if (!regexCall) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return makeProblem({
|
|
218
|
+
node, assertMethod, regexCall, extraArgsToRemove: [], context,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// `assert.strictEqual`/`equal`/`notStrictEqual`/`notEqual(re.test(str), true/false)`
|
|
223
|
+
if (method === 'strictEqual' || method === 'equal' || method === 'notStrictEqual' || method === 'notEqual') {
|
|
224
|
+
return getEqualityProblem(node, method, context);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
230
|
+
const config = {
|
|
231
|
+
create,
|
|
232
|
+
meta: {
|
|
233
|
+
type: 'suggestion',
|
|
234
|
+
docs: {
|
|
235
|
+
description: 'Prefer `assert.match()`/`assert.doesNotMatch()` over asserting `RegExp#test()` / `String#match()` results.',
|
|
236
|
+
recommended: 'unopinionated',
|
|
237
|
+
},
|
|
238
|
+
fixable: 'code',
|
|
239
|
+
schema: [],
|
|
240
|
+
messages,
|
|
241
|
+
languages: ['js/js'],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export default config;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import containsSuspensionPoint from './utils/contains-suspension-point.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_SYNC = 'prefer-assert-throws/sync';
|
|
5
|
+
const MESSAGE_ID_ASYNC = 'prefer-assert-throws/async';
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID_SYNC]: 'Prefer `assert.throws()` over try/catch with an assertion.',
|
|
9
|
+
[MESSAGE_ID_ASYNC]: 'Prefer `assert.rejects()` over try/catch with an assertion.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Return true if the catch block contains at least one assertion call anywhere inside it. `assert.fail()`
|
|
14
|
+
is excluded: a bare `fail()` in a catch asserts that the try body should *not* throw, which is the
|
|
15
|
+
opposite of the `assert.throws()` pattern this rule suggests.
|
|
16
|
+
*/
|
|
17
|
+
function catchHasAssertion(catchClause, imports, visitorKeys) {
|
|
18
|
+
function walk(node) {
|
|
19
|
+
const assertion = node.type === 'CallExpression' ? parseAssertionCall(node, imports) : undefined;
|
|
20
|
+
if (assertion && assertion.method !== 'fail') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
25
|
+
const child = node[key];
|
|
26
|
+
for (const childNode of Array.isArray(child) ? child : [child]) {
|
|
27
|
+
if (childNode?.type && walk(childNode)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return walk(catchClause.body);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
40
|
+
const create = context => {
|
|
41
|
+
const imports = resolveImports(context);
|
|
42
|
+
|
|
43
|
+
if (!imports.isAssertOrTestFile) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const {visitorKeys} = context.sourceCode;
|
|
48
|
+
|
|
49
|
+
context.on('TryStatement', node => {
|
|
50
|
+
// Must have a catch clause — otherwise there is no assertion to move.
|
|
51
|
+
if (!node.handler) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The try body must have at least one statement (the throwing code).
|
|
56
|
+
if (node.block.body.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The catch clause must contain an assertion.
|
|
61
|
+
if (!catchHasAssertion(node.handler, imports, visitorKeys)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A suspension point (`await`, `for await`) in the try body makes it async.
|
|
66
|
+
const isAsync = node.block.body.some(statement => containsSuspensionPoint(statement, visitorKeys));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
node,
|
|
70
|
+
messageId: isAsync ? MESSAGE_ID_ASYNC : MESSAGE_ID_SYNC,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
76
|
+
const config = {
|
|
77
|
+
create,
|
|
78
|
+
meta: {
|
|
79
|
+
type: 'suggestion',
|
|
80
|
+
docs: {
|
|
81
|
+
description: 'Prefer `assert.throws()`/`assert.rejects()` over try/catch with an assertion.',
|
|
82
|
+
recommended: true,
|
|
83
|
+
},
|
|
84
|
+
schema: [],
|
|
85
|
+
messages,
|
|
86
|
+
languages: ['js/js'],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default config;
|