@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,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
getSubtestReceiver,
|
|
6
|
+
parseAssertionCall,
|
|
7
|
+
createContextTracker,
|
|
8
|
+
isAssertionCallWithSupportedContext,
|
|
9
|
+
} from './utils/node-test.js';
|
|
10
|
+
import isConditionalBranch from './utils/is-conditional-branch.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_ID = 'no-conditional-assertion/error';
|
|
13
|
+
|
|
14
|
+
const messages = {
|
|
15
|
+
[MESSAGE_ID]: 'Assertions should not be placed inside conditionals, as they may never execute.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
The callback that bounds an assertion's scope: a test/hook call, or a subtest (`t.test(…)`, a method
|
|
20
|
+
call rather than an imported binding). Returns `undefined` for suites (their bodies only register
|
|
21
|
+
tests, so conditional assertions there are not this rule's concern) and non-test calls.
|
|
22
|
+
*/
|
|
23
|
+
function getScopeBoundaryCallback(node, imports) {
|
|
24
|
+
const parsed = parseTestCall(node, imports);
|
|
25
|
+
if (parsed) {
|
|
26
|
+
return parsed.kind === 'test' || parsed.kind === 'hook' ? getTestCallback(node) : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return getSubtestReceiver(node) === undefined ? undefined : getTestCallback(node);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
33
|
+
const create = context => {
|
|
34
|
+
const imports = resolveImports(context);
|
|
35
|
+
if (!imports.isTestFile) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Stack of callback function nodes for the currently open test/hook/subtest scopes. An assertion
|
|
40
|
+
// is checked against the conditionals up to its nearest enclosing scope, so a subtest's body is
|
|
41
|
+
// scoped to the subtest, not to a conditional wrapping the subtest call in the outer test.
|
|
42
|
+
const testCallbackStack = [];
|
|
43
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
44
|
+
|
|
45
|
+
context.on('CallExpression', node => {
|
|
46
|
+
tracker.update(node);
|
|
47
|
+
|
|
48
|
+
const boundaryCallback = getScopeBoundaryCallback(node, imports);
|
|
49
|
+
if (boundaryCallback) {
|
|
50
|
+
testCallbackStack.push(boundaryCallback);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Not a test/subtest call — check if it's an assertion inside a test.
|
|
55
|
+
if (testCallbackStack.length === 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!parseAssertionCall(node, imports) || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const testCallback = testCallbackStack.at(-1);
|
|
64
|
+
|
|
65
|
+
// Walk ancestors from this assertion up to (but not including) the test callback.
|
|
66
|
+
let child = node;
|
|
67
|
+
let current = node.parent;
|
|
68
|
+
|
|
69
|
+
while (current && current !== testCallback) {
|
|
70
|
+
// Loops count here: an assertion in a loop body may run zero or many times.
|
|
71
|
+
if (isConditionalBranch(current, child, {includeLoops: true})) {
|
|
72
|
+
return {
|
|
73
|
+
node,
|
|
74
|
+
messageId: MESSAGE_ID,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
child = current;
|
|
79
|
+
current = current.parent;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
context.onExit('CallExpression', node => {
|
|
84
|
+
tracker.leave(node);
|
|
85
|
+
|
|
86
|
+
const boundaryCallback = getScopeBoundaryCallback(node, imports);
|
|
87
|
+
if (boundaryCallback && testCallbackStack.at(-1) === boundaryCallback) {
|
|
88
|
+
testCallbackStack.pop();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
94
|
+
const config = {
|
|
95
|
+
create,
|
|
96
|
+
meta: {
|
|
97
|
+
type: 'problem',
|
|
98
|
+
docs: {
|
|
99
|
+
description: 'Disallow assertions inside conditional code within a test.',
|
|
100
|
+
recommended: 'unopinionated',
|
|
101
|
+
},
|
|
102
|
+
schema: [],
|
|
103
|
+
messages,
|
|
104
|
+
languages: ['js/js'],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default config;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
import isFunction from './ast/is-function.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'no-conditional-in-test';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: 'Avoid conditional logic in a test; a test should run the same way every time.',
|
|
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
|
+
// Callbacks of test/hook calls. Conditionals inside a `describe` body are about test
|
|
18
|
+
// registration (see `no-conditional-tests`), so suites are excluded here.
|
|
19
|
+
const testCallbacks = new Set();
|
|
20
|
+
|
|
21
|
+
context.on('CallExpression', node => {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
if (parsed?.kind !== 'test' && parsed?.kind !== 'hook') {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const callback = getTestCallback(node);
|
|
28
|
+
if (callback) {
|
|
29
|
+
testCallbacks.add(callback);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const report = node => {
|
|
34
|
+
// The conditional must sit directly in the test body, not in a sibling argument like the
|
|
35
|
+
// options object (`{skip: a ? … : …}`) or inside a nested helper function.
|
|
36
|
+
let enclosing = node.parent;
|
|
37
|
+
while (enclosing && !isFunction(enclosing)) {
|
|
38
|
+
enclosing = enclosing.parent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (enclosing && testCallbacks.has(enclosing)) {
|
|
42
|
+
return {node, messageId: MESSAGE_ID};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
context.on('IfStatement', report);
|
|
47
|
+
context.on('SwitchStatement', report);
|
|
48
|
+
context.on('ConditionalExpression', report);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
52
|
+
const config = {
|
|
53
|
+
create,
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'suggestion',
|
|
56
|
+
docs: {
|
|
57
|
+
description: 'Disallow conditional logic inside tests.',
|
|
58
|
+
recommended: false,
|
|
59
|
+
},
|
|
60
|
+
schema: [],
|
|
61
|
+
messages,
|
|
62
|
+
languages: ['js/js'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default config;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall} from './utils/node-test.js';
|
|
2
|
+
import isConditionalBranch from './utils/is-conditional-branch.js';
|
|
3
|
+
import isFunction from './ast/is-function.js';
|
|
4
|
+
|
|
5
|
+
const MESSAGE_ID = 'no-conditional-tests';
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID]: 'Do not register a {{kind}} conditionally; it makes the suite structure non-deterministic.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
Whether a conditional construct guards `node`, searching up to the enclosing function. Loops are
|
|
13
|
+
intentionally ignored: iterating to register tests is the idiomatic way to write table-driven tests
|
|
14
|
+
in `node:test`.
|
|
15
|
+
*/
|
|
16
|
+
function isConditionallyRegistered(node) {
|
|
17
|
+
let current = node;
|
|
18
|
+
let {parent} = current;
|
|
19
|
+
while (parent) {
|
|
20
|
+
if (isFunction(parent)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isConditionalBranch(parent, current)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
current = parent;
|
|
29
|
+
({parent} = current);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
36
|
+
const create = context => {
|
|
37
|
+
const imports = resolveImports(context);
|
|
38
|
+
if (!imports.isTestFile) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
context.on('CallExpression', node => {
|
|
43
|
+
const parsed = parseTestCall(node, imports);
|
|
44
|
+
if (!parsed) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!isConditionallyRegistered(node)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
node,
|
|
54
|
+
messageId: MESSAGE_ID,
|
|
55
|
+
data: {kind: parsed.kind},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
61
|
+
const config = {
|
|
62
|
+
create,
|
|
63
|
+
meta: {
|
|
64
|
+
type: 'problem',
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Disallow conditionally registering tests, suites, and hooks.',
|
|
67
|
+
recommended: true,
|
|
68
|
+
},
|
|
69
|
+
schema: [],
|
|
70
|
+
messages,
|
|
71
|
+
languages: ['js/js'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default config;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestOptions,
|
|
5
|
+
findEnabledOptionsProperty,
|
|
6
|
+
MODIFIERS,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'no-conflicting-modifiers';
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
[MESSAGE_ID]: 'Conflicting modifiers {{modifiers}}; `node:test` applies only one.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
16
|
+
const create = context => {
|
|
17
|
+
const imports = resolveImports(context);
|
|
18
|
+
if (!imports.isTestFile) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
context.on('CallExpression', node => {
|
|
23
|
+
const parsed = parseTestCall(node, imports);
|
|
24
|
+
if (!parsed) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const active = new Set();
|
|
29
|
+
|
|
30
|
+
// Chained form: `test.skip.only(…)`.
|
|
31
|
+
for (const modifier of parsed.modifiers) {
|
|
32
|
+
if (MODIFIERS.has(modifier.name)) {
|
|
33
|
+
active.add(modifier.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Options form: `test('t', {skip: true, only: true}, …)`.
|
|
38
|
+
const options = getTestOptions(node);
|
|
39
|
+
for (const name of MODIFIERS) {
|
|
40
|
+
if (findEnabledOptionsProperty(options, name)) {
|
|
41
|
+
active.add(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (active.size < 2) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const modifiers = [...active].toSorted((a, b) => a.localeCompare(b)).map(name => `\`${name}\``).join(', ');
|
|
50
|
+
return {
|
|
51
|
+
node,
|
|
52
|
+
messageId: MESSAGE_ID,
|
|
53
|
+
data: {modifiers},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
59
|
+
const config = {
|
|
60
|
+
create,
|
|
61
|
+
meta: {
|
|
62
|
+
type: 'problem',
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Disallow conflicting `only`/`skip`/`todo` modifiers.',
|
|
65
|
+
recommended: 'unopinionated',
|
|
66
|
+
},
|
|
67
|
+
schema: [],
|
|
68
|
+
messages,
|
|
69
|
+
languages: ['js/js'],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default config;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {findVariable, getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
MODIFIERS,
|
|
4
|
+
resolveImports,
|
|
5
|
+
parseAssertionCall,
|
|
6
|
+
parseTestCall,
|
|
7
|
+
getSubtestReceiver,
|
|
8
|
+
getTestCallback,
|
|
9
|
+
} from './utils/node-test.js';
|
|
10
|
+
import {isRegexLiteral} from './ast/index.js';
|
|
11
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
12
|
+
|
|
13
|
+
const MESSAGE_ID = 'no-constant-assertion';
|
|
14
|
+
|
|
15
|
+
const messages = {
|
|
16
|
+
[MESSAGE_ID]: 'This assertion has a constant outcome, so it does not test code behavior.',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const COMPARISON_METHODS = new Set([
|
|
20
|
+
'equal',
|
|
21
|
+
'notEqual',
|
|
22
|
+
'strictEqual',
|
|
23
|
+
'notStrictEqual',
|
|
24
|
+
'deepEqual',
|
|
25
|
+
'notDeepEqual',
|
|
26
|
+
'deepStrictEqual',
|
|
27
|
+
'notDeepStrictEqual',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const MATCH_METHODS = new Set([
|
|
31
|
+
'match',
|
|
32
|
+
'doesNotMatch',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function isStaticArgument(node, sourceCode) {
|
|
36
|
+
if (!node || node.type === 'SpreadElement') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
node = unwrapTypeScriptExpression(node);
|
|
41
|
+
|
|
42
|
+
return getStaticValue(node, sourceCode.getScope(node)) !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function areStaticArguments(nodes, sourceCode) {
|
|
46
|
+
return nodes.every(node => isStaticArgument(node, sourceCode));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isStaticRegexLiteral(node) {
|
|
50
|
+
if (!node || node.type === 'SpreadElement') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return isRegexLiteral(unwrapTypeScriptExpression(node));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `parseTestCall` already guarantees the callee root is an imported `node:test` binding, so no
|
|
58
|
+
// separate binding check is needed here (and doing one on the raw callee would miss TypeScript
|
|
59
|
+
// wrappers like `(test as any)(…)`).
|
|
60
|
+
function isNodeTestCall(node, imports) {
|
|
61
|
+
const parsed = parseTestCall(node, imports);
|
|
62
|
+
return Boolean(parsed)
|
|
63
|
+
&& (parsed.kind === 'test' || parsed.kind === 'hook')
|
|
64
|
+
&& !(parsed.kind === 'hook' && parsed.modifiers.length > 0)
|
|
65
|
+
&& parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getContextAssertReceiver(node) {
|
|
69
|
+
const {callee} = node;
|
|
70
|
+
if (
|
|
71
|
+
callee.type !== 'MemberExpression'
|
|
72
|
+
|| callee.computed
|
|
73
|
+
|| callee.object.type !== 'MemberExpression'
|
|
74
|
+
|| callee.object.computed
|
|
75
|
+
|| callee.object.object.type !== 'Identifier'
|
|
76
|
+
|| callee.object.property.type !== 'Identifier'
|
|
77
|
+
|| callee.object.property.name !== 'assert'
|
|
78
|
+
) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return callee.object.object;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isInsideNode(node, ancestor) {
|
|
86
|
+
let current = node;
|
|
87
|
+
while (current) {
|
|
88
|
+
if (current === ancestor) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
current = current.parent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getContextParameterVariable(callback, sourceCode) {
|
|
99
|
+
const parameter = callback.params[0];
|
|
100
|
+
if (parameter?.type !== 'Identifier') {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sourceCode
|
|
105
|
+
.getDeclaredVariables(callback)
|
|
106
|
+
.find(variable => variable.identifiers.includes(parameter));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isSubtestCall(node, activeContexts, sourceCode) {
|
|
110
|
+
const receiver = getSubtestReceiver(node);
|
|
111
|
+
if (!receiver) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const receiverVariable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
116
|
+
return activeContexts.some(({variable}) => receiverVariable === variable);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getTestContextCallback(node, imports, activeContexts, sourceCode) {
|
|
120
|
+
if (isNodeTestCall(node, imports) || isSubtestCall(node, activeContexts, sourceCode)) {
|
|
121
|
+
return getTestCallback(node);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isUntrackedContextAssertCall(node, activeContexts, sourceCode) {
|
|
126
|
+
const receiver = getContextAssertReceiver(node);
|
|
127
|
+
if (!receiver) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const receiverVariable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
132
|
+
return activeContexts.every(({callback, variable}) =>
|
|
133
|
+
!isInsideNode(receiver, callback)
|
|
134
|
+
|| receiverVariable !== variable);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
138
|
+
const create = context => {
|
|
139
|
+
const imports = resolveImports(context);
|
|
140
|
+
if (!imports.isAssertOrTestFile) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const {sourceCode} = context;
|
|
145
|
+
const activeContexts = [];
|
|
146
|
+
const trackedTestCalls = new Set();
|
|
147
|
+
|
|
148
|
+
context.on('CallExpression', node => {
|
|
149
|
+
const callback = getTestContextCallback(node, imports, activeContexts, sourceCode);
|
|
150
|
+
if (callback) {
|
|
151
|
+
const variable = getContextParameterVariable(callback, sourceCode);
|
|
152
|
+
if (variable) {
|
|
153
|
+
activeContexts.push({callback, variable});
|
|
154
|
+
trackedTestCalls.add(node);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const assertion = parseAssertionCall(node, imports);
|
|
159
|
+
if (!assertion) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (getContextAssertReceiver(node) && isUntrackedContextAssertCall(node, activeContexts, sourceCode)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (node.arguments.some(argument => argument.type === 'SpreadElement')) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const {method} = assertion;
|
|
172
|
+
const [firstArgument, secondArgument] = node.arguments;
|
|
173
|
+
let isConstant = false;
|
|
174
|
+
|
|
175
|
+
if (method === 'ok' || method === 'ifError') {
|
|
176
|
+
isConstant = isStaticArgument(firstArgument, sourceCode);
|
|
177
|
+
} else if (COMPARISON_METHODS.has(method)) {
|
|
178
|
+
isConstant = areStaticArguments([firstArgument, secondArgument], sourceCode);
|
|
179
|
+
} else if (MATCH_METHODS.has(method)) {
|
|
180
|
+
isConstant = isStaticArgument(firstArgument, sourceCode) && isStaticRegexLiteral(secondArgument);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!isConstant) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
node,
|
|
189
|
+
messageId: MESSAGE_ID,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
context.onExit('CallExpression', node => {
|
|
194
|
+
if (!trackedTestCalls.has(node)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
trackedTestCalls.delete(node);
|
|
199
|
+
activeContexts.pop();
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
204
|
+
const config = {
|
|
205
|
+
create,
|
|
206
|
+
meta: {
|
|
207
|
+
type: 'problem',
|
|
208
|
+
docs: {
|
|
209
|
+
description: 'Disallow assertions with constant outcomes.',
|
|
210
|
+
recommended: 'unopinionated',
|
|
211
|
+
},
|
|
212
|
+
schema: [],
|
|
213
|
+
messages,
|
|
214
|
+
languages: ['js/js'],
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export default config;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
getEffectiveArity,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'no-done-callback';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'Use `async`/`await` or return a Promise instead of the `{{name}}` callback parameter.',
|
|
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
|
+
// A declared second parameter is the `done` callback `node:test` passes based on arity.
|
|
30
|
+
if (!callback || getEffectiveArity(callback.params) < 2) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parameter = callback.params[1];
|
|
35
|
+
return {
|
|
36
|
+
node: parameter,
|
|
37
|
+
messageId: MESSAGE_ID,
|
|
38
|
+
data: {name: parameter.type === 'Identifier' ? parameter.name : 'done'},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
44
|
+
const config = {
|
|
45
|
+
create,
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'suggestion',
|
|
48
|
+
docs: {
|
|
49
|
+
description: 'Disallow callback (`done`) parameters in tests and hooks.',
|
|
50
|
+
recommended: false,
|
|
51
|
+
},
|
|
52
|
+
schema: [],
|
|
53
|
+
messages,
|
|
54
|
+
languages: ['js/js'],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default config;
|