@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,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'no-deep-equal-with-primitive';
|
|
10
|
+
|
|
11
|
+
const DEEP_EQUAL_METHODS = new Map([
|
|
12
|
+
['deepEqual', 'equal'],
|
|
13
|
+
['deepStrictEqual', 'strictEqual'],
|
|
14
|
+
['notDeepEqual', 'notEqual'],
|
|
15
|
+
['notDeepStrictEqual', 'notStrictEqual'],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
Check if a node represents a primitive value.
|
|
20
|
+
Covers: literals, `undefined`/`NaN`/`Infinity` identifiers, template literals (always a string,
|
|
21
|
+
regardless of interpolation), `void` expressions, and negated numeric/Infinity/NaN literals.
|
|
22
|
+
*/
|
|
23
|
+
function isPrimitive(node) {
|
|
24
|
+
node = unwrapTypeScriptExpression(node);
|
|
25
|
+
return (
|
|
26
|
+
(node.type === 'Literal' && !node.regex)
|
|
27
|
+
|| (node.type === 'Identifier' && ['undefined', 'NaN', 'Infinity'].includes(node.name))
|
|
28
|
+
|| node.type === 'TemplateLiteral'
|
|
29
|
+
|| (node.type === 'UnaryExpression' && node.operator === 'void')
|
|
30
|
+
|| (
|
|
31
|
+
node.type === 'UnaryExpression'
|
|
32
|
+
&& node.operator === '-'
|
|
33
|
+
&& (
|
|
34
|
+
(node.argument.type === 'Literal' && !node.argument.regex)
|
|
35
|
+
|| (node.argument.type === 'Identifier' && (node.argument.name === 'Infinity' || node.argument.name === 'NaN'))
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
42
|
+
const create = context => {
|
|
43
|
+
const imports = resolveImports(context);
|
|
44
|
+
if (!imports.isAssertOrTestFile) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
49
|
+
|
|
50
|
+
context.on('CallExpression', node => {
|
|
51
|
+
tracker.update(node);
|
|
52
|
+
|
|
53
|
+
const assertion = parseAssertionCall(node, imports);
|
|
54
|
+
if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {method} = assertion;
|
|
59
|
+
const replacement = DEEP_EQUAL_METHODS.get(method);
|
|
60
|
+
if (!replacement) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [actual, expected] = node.arguments;
|
|
65
|
+
if (!actual || !expected) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isPrimitive(actual) && !isPrimitive(expected)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {callee} = node;
|
|
74
|
+
const problem = {
|
|
75
|
+
node,
|
|
76
|
+
messageId: MESSAGE_ID,
|
|
77
|
+
data: {method},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Autofix only the member forms (`assert.deepEqual`, `t.assert.deepEqual`). A bare named
|
|
81
|
+
// import (`deepEqual`) cannot be rewritten to `equal` without also importing it, so leave
|
|
82
|
+
// it reported but unfixed.
|
|
83
|
+
if (callee.type === 'MemberExpression') {
|
|
84
|
+
problem.fix = fixer => fixer.replaceText(callee.property, replacement);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return problem;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
context.onExit('CallExpression', node => {
|
|
91
|
+
tracker.leave(node);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
96
|
+
const config = {
|
|
97
|
+
create,
|
|
98
|
+
meta: {
|
|
99
|
+
type: 'suggestion',
|
|
100
|
+
docs: {
|
|
101
|
+
description: 'Disallow `deepEqual`/`deepStrictEqual` (and their `notDeep*` variants) when comparing with primitives.',
|
|
102
|
+
recommended: 'unopinionated',
|
|
103
|
+
},
|
|
104
|
+
fixable: 'code',
|
|
105
|
+
schema: [],
|
|
106
|
+
messages: {
|
|
107
|
+
[MESSAGE_ID]: 'Avoid using `{{method}}` with a primitive. Use the non-deep equality equivalent instead.',
|
|
108
|
+
},
|
|
109
|
+
languages: ['js/js'],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default config;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'no-incorrect-strict-equal';
|
|
10
|
+
|
|
11
|
+
// Strict/loose equality methods and their deep equivalents.
|
|
12
|
+
const STRICT_TO_DEEP = new Map([
|
|
13
|
+
['equal', 'deepEqual'],
|
|
14
|
+
['strictEqual', 'deepStrictEqual'],
|
|
15
|
+
['notEqual', 'notDeepEqual'],
|
|
16
|
+
['notStrictEqual', 'notDeepStrictEqual'],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Check if a node is an object or array literal. These are freshly allocated, so a strict/loose
|
|
21
|
+
equality comparison against them is decided purely by reference identity, never by structure.
|
|
22
|
+
*/
|
|
23
|
+
function isObjectOrArrayLiteral(node) {
|
|
24
|
+
const unwrapped = unwrapTypeScriptExpression(node);
|
|
25
|
+
return unwrapped.type === 'ObjectExpression' || unwrapped.type === 'ArrayExpression';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
29
|
+
const create = context => {
|
|
30
|
+
const imports = resolveImports(context);
|
|
31
|
+
if (!imports.isAssertOrTestFile) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
36
|
+
|
|
37
|
+
context.on('CallExpression', node => {
|
|
38
|
+
tracker.update(node);
|
|
39
|
+
|
|
40
|
+
const assertion = parseAssertionCall(node, imports);
|
|
41
|
+
if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const replacement = STRICT_TO_DEEP.get(assertion.method);
|
|
46
|
+
if (!replacement) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [actual, expected] = node.arguments;
|
|
51
|
+
if (!actual || !expected) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isObjectOrArrayLiteral(actual) && !isObjectOrArrayLiteral(expected)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const {callee} = node;
|
|
60
|
+
const problem = {
|
|
61
|
+
node,
|
|
62
|
+
messageId: MESSAGE_ID,
|
|
63
|
+
data: {method: assertion.method, replacement},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Autofix only the member forms (`assert.strictEqual`, `t.assert.strictEqual`). A bare named
|
|
67
|
+
// import (`strictEqual`) cannot be rewritten to `deepStrictEqual` without also importing it,
|
|
68
|
+
// so leave it reported but unfixed.
|
|
69
|
+
if (callee.type === 'MemberExpression') {
|
|
70
|
+
problem.fix = fixer => fixer.replaceText(callee.property, replacement);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return problem;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
context.onExit('CallExpression', node => {
|
|
77
|
+
tracker.leave(node);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
82
|
+
const config = {
|
|
83
|
+
create,
|
|
84
|
+
meta: {
|
|
85
|
+
type: 'suggestion',
|
|
86
|
+
docs: {
|
|
87
|
+
description: 'Disallow `strictEqual`/`equal` (and their `not*` variants) when comparing with an object or array literal.',
|
|
88
|
+
recommended: 'unopinionated',
|
|
89
|
+
},
|
|
90
|
+
fixable: 'code',
|
|
91
|
+
schema: [],
|
|
92
|
+
messages: {
|
|
93
|
+
[MESSAGE_ID]: 'Avoid using `{{method}}` with an object or array literal, which compares by reference rather than structure. Use `{{replacement}}` instead.',
|
|
94
|
+
},
|
|
95
|
+
languages: ['js/js'],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default config;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getStaticString} from './utils/node-test.js';
|
|
2
|
+
import {isLoop, isFunction} from './ast/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'no-loop-static-title';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: 'This title is static but generated in a loop, so every iteration registers the same title. Include the loop variable so each title is unique.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Array methods whose callback is commonly used to generate one test per element.
|
|
11
|
+
const ITERATION_METHODS = new Set(['map', 'forEach', 'flatMap']);
|
|
12
|
+
|
|
13
|
+
/** Whether `callExpression` is an iteration method call (`xs.map(fn)`) whose callback is `callback`. */
|
|
14
|
+
function isIterationCall(callExpression, callback) {
|
|
15
|
+
const {callee} = callExpression;
|
|
16
|
+
return (
|
|
17
|
+
callee.type === 'MemberExpression'
|
|
18
|
+
&& !callee.computed
|
|
19
|
+
&& callee.property.type === 'Identifier'
|
|
20
|
+
&& ITERATION_METHODS.has(callee.property.name)
|
|
21
|
+
&& callExpression.arguments.includes(callback)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
Whether the test/suite call repeats across a loop without an intervening test/suite scope.
|
|
27
|
+
Walking up from the call: a loop means it repeats; the first enclosing function that is an
|
|
28
|
+
iteration callback (`xs.map(…)`) also means it repeats; any other function is a scope boundary
|
|
29
|
+
(a `describe`/test callback or helper) under which the static title is no longer a duplicate.
|
|
30
|
+
*/
|
|
31
|
+
function isInRepeatingScope(node) {
|
|
32
|
+
let current = node.parent;
|
|
33
|
+
while (current) {
|
|
34
|
+
if (isLoop(current)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isFunction(current)) {
|
|
39
|
+
return current.parent?.type === 'CallExpression' && isIterationCall(current.parent, current);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current = current.parent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
49
|
+
const create = context => {
|
|
50
|
+
const imports = resolveImports(context);
|
|
51
|
+
if (!imports.isTestFile) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
context.on('CallExpression', node => {
|
|
56
|
+
const parsed = parseTestCall(node, imports);
|
|
57
|
+
if (parsed?.kind !== 'test' && parsed?.kind !== 'suite') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A static title resolves to a constant string; a title that interpolates the loop variable
|
|
62
|
+
// does not, so it is correctly left alone.
|
|
63
|
+
if (getStaticString(node.arguments[0], context) === undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isInRepeatingScope(node)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
node: node.arguments[0],
|
|
73
|
+
messageId: MESSAGE_ID,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
79
|
+
const config = {
|
|
80
|
+
create,
|
|
81
|
+
meta: {
|
|
82
|
+
type: 'problem',
|
|
83
|
+
docs: {
|
|
84
|
+
description: 'Disallow a static test or suite title inside a loop.',
|
|
85
|
+
recommended: 'unopinionated',
|
|
86
|
+
},
|
|
87
|
+
schema: [],
|
|
88
|
+
messages,
|
|
89
|
+
languages: ['js/js'],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default config;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
getSubtestReceiver,
|
|
6
|
+
getTestOptions,
|
|
7
|
+
findOptionsProperty,
|
|
8
|
+
} from './utils/node-test.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID = 'no-misused-concurrency';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID]: 'The `concurrency` option has no effect on a test without subtests. It only controls how a suite or a test\'s subtests run concurrently.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
17
|
+
const create = context => {
|
|
18
|
+
const imports = resolveImports(context);
|
|
19
|
+
if (!imports.isTestFile) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const tracker = createContextTracker(imports);
|
|
24
|
+
|
|
25
|
+
// One frame per test/subtest. A frame with a `concurrency` option but no subtests is misused.
|
|
26
|
+
const frames = [];
|
|
27
|
+
|
|
28
|
+
context.on('CallExpression', node => {
|
|
29
|
+
const isSubtest = tracker.isSubtestCall(node);
|
|
30
|
+
|
|
31
|
+
// Attribute this subtest to the frame that owns its receiver context, before it pushes its own.
|
|
32
|
+
if (isSubtest) {
|
|
33
|
+
const receiver = getSubtestReceiver(node).name;
|
|
34
|
+
const ownerFrame = frames.findLast(frame => frame.contextName === receiver);
|
|
35
|
+
if (ownerFrame) {
|
|
36
|
+
ownerFrame.hasSubtest = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test';
|
|
41
|
+
tracker.update(node);
|
|
42
|
+
|
|
43
|
+
if (isTest || isSubtest) {
|
|
44
|
+
frames.push({
|
|
45
|
+
node,
|
|
46
|
+
contextName: tracker.current(),
|
|
47
|
+
concurrencyProperty: findOptionsProperty(getTestOptions(node), 'concurrency'),
|
|
48
|
+
hasSubtest: false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
context.onExit('CallExpression', node => {
|
|
54
|
+
tracker.leave(node);
|
|
55
|
+
|
|
56
|
+
if (frames.at(-1)?.node !== node) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const frame = frames.pop();
|
|
61
|
+
if (frame.concurrencyProperty && !frame.hasSubtest) {
|
|
62
|
+
return {
|
|
63
|
+
node: frame.concurrencyProperty,
|
|
64
|
+
messageId: MESSAGE_ID,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
71
|
+
const config = {
|
|
72
|
+
create,
|
|
73
|
+
meta: {
|
|
74
|
+
type: 'problem',
|
|
75
|
+
docs: {
|
|
76
|
+
description: 'Disallow the `concurrency` option on a test without subtests.',
|
|
77
|
+
recommended: 'unopinionated',
|
|
78
|
+
},
|
|
79
|
+
schema: [],
|
|
80
|
+
messages,
|
|
81
|
+
languages: ['js/js'],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default config;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {resolveImports} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-mock-timers-destructured-import';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`{{name}}` is imported directly, so `mock.timers` cannot intercept it. Call the global `{{name}}` instead.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const TIMER_MODULES = new Set(['node:timers', 'timers']);
|
|
10
|
+
|
|
11
|
+
// Imported timer function -> the `mock.timers` API name that mocks it.
|
|
12
|
+
const FUNCTION_TO_API = new Map([
|
|
13
|
+
['setTimeout', 'setTimeout'],
|
|
14
|
+
['clearTimeout', 'setTimeout'],
|
|
15
|
+
['setInterval', 'setInterval'],
|
|
16
|
+
['clearInterval', 'setInterval'],
|
|
17
|
+
['setImmediate', 'setImmediate'],
|
|
18
|
+
['clearImmediate', 'setImmediate'],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
The enabled timer APIs, or `null` when `enable()` was called without an `apis` list (all APIs).
|
|
23
|
+
*/
|
|
24
|
+
function getEnabledApis(callExpression) {
|
|
25
|
+
const [argument] = callExpression.arguments;
|
|
26
|
+
if (argument?.type !== 'ObjectExpression') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apisProperty = argument.properties.find(property =>
|
|
31
|
+
property.type === 'Property'
|
|
32
|
+
&& !property.computed
|
|
33
|
+
&& (
|
|
34
|
+
(property.key.type === 'Identifier' && property.key.name === 'apis')
|
|
35
|
+
|| (property.key.type === 'Literal' && property.key.value === 'apis')
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
if (apisProperty?.value.type !== 'ArrayExpression') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return apisProperty.value.elements
|
|
43
|
+
.filter(element => element?.type === 'Literal' && typeof element.value === 'string')
|
|
44
|
+
.map(element => element.value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
48
|
+
const create = context => {
|
|
49
|
+
const {sourceCode} = context;
|
|
50
|
+
const imports = resolveImports(context);
|
|
51
|
+
if (!imports.isTestFile) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Named timer-function imports from `node:timers`.
|
|
56
|
+
const timerImports = [];
|
|
57
|
+
for (const node of sourceCode.ast.body) {
|
|
58
|
+
if (node.type !== 'ImportDeclaration' || !TIMER_MODULES.has(node.source.value)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const specifier of node.specifiers) {
|
|
63
|
+
if (
|
|
64
|
+
specifier.type === 'ImportSpecifier'
|
|
65
|
+
&& specifier.imported.type === 'Identifier'
|
|
66
|
+
&& FUNCTION_TO_API.has(specifier.imported.name)
|
|
67
|
+
) {
|
|
68
|
+
timerImports.push(specifier);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (timerImports.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const {mockLocals} = imports;
|
|
78
|
+
|
|
79
|
+
// `mock.timers` (global) or `t.mock.timers` (context).
|
|
80
|
+
const isMockTimers = node =>
|
|
81
|
+
node.type === 'MemberExpression'
|
|
82
|
+
&& !node.computed
|
|
83
|
+
&& node.property.type === 'Identifier'
|
|
84
|
+
&& node.property.name === 'timers'
|
|
85
|
+
&& (
|
|
86
|
+
// `mock.timers` (global, named/renamed import).
|
|
87
|
+
(node.object.type === 'Identifier' && mockLocals.has(node.object.name))
|
|
88
|
+
// `t.mock.timers` (context) or `namespace.mock.timers`.
|
|
89
|
+
|| (
|
|
90
|
+
node.object.type === 'MemberExpression'
|
|
91
|
+
&& !node.object.computed
|
|
92
|
+
&& node.object.property.type === 'Identifier'
|
|
93
|
+
&& node.object.property.name === 'mock'
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const enabledApis = new Set();
|
|
98
|
+
let isAllEnabled = false;
|
|
99
|
+
|
|
100
|
+
context.on('CallExpression', node => {
|
|
101
|
+
const {callee} = node;
|
|
102
|
+
if (
|
|
103
|
+
callee.type === 'MemberExpression'
|
|
104
|
+
&& !callee.computed
|
|
105
|
+
&& callee.property.type === 'Identifier'
|
|
106
|
+
&& callee.property.name === 'enable'
|
|
107
|
+
&& isMockTimers(callee.object)
|
|
108
|
+
) {
|
|
109
|
+
const apis = getEnabledApis(node);
|
|
110
|
+
if (apis === null) {
|
|
111
|
+
isAllEnabled = true;
|
|
112
|
+
} else {
|
|
113
|
+
for (const api of apis) {
|
|
114
|
+
enabledApis.add(api);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
context.onExit('Program', () => {
|
|
121
|
+
if (!isAllEnabled && enabledApis.size === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return timerImports
|
|
126
|
+
.filter(specifier => isAllEnabled || enabledApis.has(FUNCTION_TO_API.get(specifier.imported.name)))
|
|
127
|
+
.map(specifier => ({
|
|
128
|
+
node: specifier,
|
|
129
|
+
messageId: MESSAGE_ID,
|
|
130
|
+
data: {name: specifier.imported.name},
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
136
|
+
const config = {
|
|
137
|
+
create,
|
|
138
|
+
meta: {
|
|
139
|
+
type: 'problem',
|
|
140
|
+
docs: {
|
|
141
|
+
description: 'Disallow destructured timer imports when using `mock.timers`.',
|
|
142
|
+
recommended: 'unopinionated',
|
|
143
|
+
},
|
|
144
|
+
schema: [],
|
|
145
|
+
messages,
|
|
146
|
+
languages: ['js/js'],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default config;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-nested-tests/error';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Do not define a test or suite inside a test body. Use `t.test()` for subtests instead.',
|
|
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 callback function nodes of the `test`/`it` calls we are currently inside.
|
|
17
|
+
// Suites (`describe`/`suite`) legitimately contain tests and nested suites, so they
|
|
18
|
+
// do not open a scope here — only a test/it body does.
|
|
19
|
+
const testCallbackStack = [];
|
|
20
|
+
|
|
21
|
+
context.on('CallExpression', node => {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A test or suite defined inside a test body should be a subtest (`t.test()`).
|
|
28
|
+
if ((parsed.kind === 'test' || parsed.kind === 'suite') && testCallbackStack.length > 0) {
|
|
29
|
+
return {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (parsed.kind === 'test') {
|
|
36
|
+
const callback = getTestCallback(node);
|
|
37
|
+
if (callback) {
|
|
38
|
+
testCallbackStack.push(callback);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
context.onExit('CallExpression', node => {
|
|
44
|
+
const parsed = parseTestCall(node, imports);
|
|
45
|
+
if (!parsed || parsed.kind !== 'test') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const callback = getTestCallback(node);
|
|
50
|
+
if (callback && testCallbackStack.at(-1) === callback) {
|
|
51
|
+
testCallbackStack.pop();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
57
|
+
const config = {
|
|
58
|
+
create,
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'problem',
|
|
61
|
+
docs: {
|
|
62
|
+
description: 'Disallow tests and suites nested inside a test body.',
|
|
63
|
+
recommended: 'unopinionated',
|
|
64
|
+
},
|
|
65
|
+
schema: [],
|
|
66
|
+
messages,
|
|
67
|
+
languages: ['js/js'],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default config;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import createTestModifierRule from './shared/test-modifier-rule.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
4
|
+
const config = createTestModifierRule({
|
|
5
|
+
modifier: 'only',
|
|
6
|
+
description: 'Disallow the `.only` test modifier.',
|
|
7
|
+
errorMessage: 'Do not use the `.only` test modifier as it prevents the other tests from running.',
|
|
8
|
+
recommended: 'unopinionated',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default config;
|