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, 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,59 @@
|
|
|
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
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
18
|
+
const create = context => {
|
|
19
|
+
context.on('Program:exit', () => {
|
|
20
|
+
for (const comment of getComments(context)) {
|
|
21
|
+
// Skip JSDoc-style block comments (/** ... */).
|
|
22
|
+
if (comment.type === 'Block' && context.sourceCode.getText(comment).startsWith('/**')) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = comment.value.split('\n');
|
|
27
|
+
const commentStartLine = context.sourceCode.getLoc(comment).start.line;
|
|
28
|
+
for (const [index, line] of lines.entries()) {
|
|
29
|
+
if (COMMENTED_TEST_PATTERN.test(line)) {
|
|
30
|
+
context.report({
|
|
31
|
+
loc: {
|
|
32
|
+
line: commentStartLine + index,
|
|
33
|
+
column: 0,
|
|
34
|
+
},
|
|
35
|
+
messageId: MESSAGE_ID,
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
45
|
+
const config = {
|
|
46
|
+
create,
|
|
47
|
+
meta: {
|
|
48
|
+
type: 'suggestion',
|
|
49
|
+
docs: {
|
|
50
|
+
description: 'Disallow commented-out tests.',
|
|
51
|
+
recommended: false,
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
messages,
|
|
55
|
+
languages: ['js/js'],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default config;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
getSubtestReceiver,
|
|
6
|
+
parseAssertionCall,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
import isConditionalBranch from './utils/is-conditional-branch.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID = 'no-conditional-assertion/error';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID]: 'Assertions should not be placed inside conditionals, as they may never execute.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
The callback that bounds an assertion's scope: a test/hook call, or a subtest (`t.test(…)`, a method
|
|
18
|
+
call rather than an imported binding). Returns `undefined` for suites (their bodies only register
|
|
19
|
+
tests, so conditional assertions there are not this rule's concern) and non-test calls.
|
|
20
|
+
*/
|
|
21
|
+
function getScopeBoundaryCallback(node, imports) {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
if (parsed) {
|
|
24
|
+
return parsed.kind === 'test' || parsed.kind === 'hook' ? getTestCallback(node) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return getSubtestReceiver(node) === undefined ? undefined : getTestCallback(node);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
31
|
+
const create = context => {
|
|
32
|
+
const imports = resolveImports(context);
|
|
33
|
+
if (!imports.isTestFile) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Stack of callback function nodes for the currently open test/hook/subtest scopes. An assertion
|
|
38
|
+
// is checked against the conditionals up to its nearest enclosing scope, so a subtest's body is
|
|
39
|
+
// scoped to the subtest, not to a conditional wrapping the subtest call in the outer test.
|
|
40
|
+
const testCallbackStack = [];
|
|
41
|
+
|
|
42
|
+
context.on('CallExpression', node => {
|
|
43
|
+
const boundaryCallback = getScopeBoundaryCallback(node, imports);
|
|
44
|
+
if (boundaryCallback) {
|
|
45
|
+
testCallbackStack.push(boundaryCallback);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Not a test/subtest call — check if it's an assertion inside a test.
|
|
50
|
+
if (testCallbackStack.length === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!parseAssertionCall(node, imports)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const testCallback = testCallbackStack.at(-1);
|
|
59
|
+
|
|
60
|
+
// Walk ancestors from this assertion up to (but not including) the test callback.
|
|
61
|
+
let child = node;
|
|
62
|
+
let current = node.parent;
|
|
63
|
+
|
|
64
|
+
while (current && current !== testCallback) {
|
|
65
|
+
// Loops count here: an assertion in a loop body may run zero or many times.
|
|
66
|
+
if (isConditionalBranch(current, child, {includeLoops: true})) {
|
|
67
|
+
return {
|
|
68
|
+
node,
|
|
69
|
+
messageId: MESSAGE_ID,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
child = current;
|
|
74
|
+
current = current.parent;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
context.onExit('CallExpression', node => {
|
|
79
|
+
const boundaryCallback = getScopeBoundaryCallback(node, imports);
|
|
80
|
+
if (boundaryCallback && testCallbackStack.at(-1) === boundaryCallback) {
|
|
81
|
+
testCallbackStack.pop();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
87
|
+
const config = {
|
|
88
|
+
create,
|
|
89
|
+
meta: {
|
|
90
|
+
type: 'problem',
|
|
91
|
+
docs: {
|
|
92
|
+
description: 'Disallow assertions inside conditional code within a test.',
|
|
93
|
+
recommended: 'unopinionated',
|
|
94
|
+
},
|
|
95
|
+
schema: [],
|
|
96
|
+
messages,
|
|
97
|
+
languages: ['js/js'],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
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?.kind !== 'test' && parsed?.kind !== 'suite') {
|
|
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 and suites.',
|
|
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().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,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;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-duplicate-hooks';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Duplicate `{{name}}` hook in the same scope.',
|
|
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
|
+
// Stack of scopes; each scope tracks the hook names already declared in it.
|
|
17
|
+
const scopeStack = [new Set()];
|
|
18
|
+
// Calls whose callback opened a scope, so we can pop on exit.
|
|
19
|
+
const pushedCalls = new Set();
|
|
20
|
+
|
|
21
|
+
context.on('CallExpression', node => {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let problem;
|
|
28
|
+
if (parsed.kind === 'hook') {
|
|
29
|
+
const scope = scopeStack.at(-1);
|
|
30
|
+
if (scope.has(parsed.name)) {
|
|
31
|
+
problem = {
|
|
32
|
+
node,
|
|
33
|
+
messageId: MESSAGE_ID,
|
|
34
|
+
data: {name: parsed.name},
|
|
35
|
+
};
|
|
36
|
+
} else {
|
|
37
|
+
scope.add(parsed.name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parsed.kind === 'test' || parsed.kind === 'suite') {
|
|
42
|
+
const callback = getTestCallback(node);
|
|
43
|
+
if (callback) {
|
|
44
|
+
scopeStack.push(new Set());
|
|
45
|
+
pushedCalls.add(node);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return problem;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
context.onExit('CallExpression', node => {
|
|
53
|
+
if (pushedCalls.has(node)) {
|
|
54
|
+
pushedCalls.delete(node);
|
|
55
|
+
scopeStack.pop();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
61
|
+
const config = {
|
|
62
|
+
create,
|
|
63
|
+
meta: {
|
|
64
|
+
type: 'suggestion',
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Disallow duplicate hooks within the same scope.',
|
|
67
|
+
recommended: true,
|
|
68
|
+
},
|
|
69
|
+
schema: [],
|
|
70
|
+
messages,
|
|
71
|
+
languages: ['js/js'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default config;
|