@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,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
getTestOptions,
|
|
6
|
+
getTestTitle,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
import {removeArgument} from './fix/index.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID_ERROR = 'prefer-todo/error';
|
|
11
|
+
const MESSAGE_ID_SUGGESTION = 'prefer-todo/suggestion';
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
[MESSAGE_ID_ERROR]: 'Empty placeholder test. Use `.todo` to mark it as unfinished.',
|
|
15
|
+
[MESSAGE_ID_SUGGESTION]: 'Mark as `.todo`.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
19
|
+
const create = context => {
|
|
20
|
+
const {sourceCode} = context;
|
|
21
|
+
const imports = resolveImports(context);
|
|
22
|
+
if (!imports.isTestFile) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
context.on('CallExpression', node => {
|
|
27
|
+
const parsed = parseTestCall(node, imports);
|
|
28
|
+
// Only plain tests (a placeholder suite is a different concept); an existing modifier is intentional.
|
|
29
|
+
if (parsed?.kind !== 'test' || parsed.modifiers.length > 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A `.todo` needs a title to be meaningful.
|
|
34
|
+
if (!getTestTitle(node, context)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// An options object (`test('title', {skip: true}, …)`) marks intent, so leave it alone.
|
|
39
|
+
if (getTestOptions(node)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const callback = getTestCallback(node);
|
|
44
|
+
|
|
45
|
+
// `test('title')` — only a title, no implementation.
|
|
46
|
+
const isTitleOnly = !callback && node.arguments.length === 1;
|
|
47
|
+
|
|
48
|
+
// `test('title', () => {})` — an empty implementation body.
|
|
49
|
+
const hasEmptyBody = callback?.body.type === 'BlockStatement' && callback.body.body.length === 0;
|
|
50
|
+
|
|
51
|
+
if (!isTitleOnly && !hasEmptyBody) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const {callee} = node;
|
|
56
|
+
// Dropping the function would drop any comments inside its body, so skip the fix then.
|
|
57
|
+
const canFix = !callback || sourceCode.getCommentsInside(callback).length === 0;
|
|
58
|
+
|
|
59
|
+
const problem = {
|
|
60
|
+
node,
|
|
61
|
+
messageId: MESSAGE_ID_ERROR,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (canFix) {
|
|
65
|
+
problem.suggest = [
|
|
66
|
+
{
|
|
67
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
68
|
+
* fix(fixer) {
|
|
69
|
+
yield fixer.insertTextAfter(callee, '.todo');
|
|
70
|
+
if (callback) {
|
|
71
|
+
yield removeArgument(fixer, callback, context);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return problem;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
83
|
+
const config = {
|
|
84
|
+
create,
|
|
85
|
+
meta: {
|
|
86
|
+
type: 'suggestion',
|
|
87
|
+
docs: {
|
|
88
|
+
description: 'Prefer `.todo` for empty placeholder tests.',
|
|
89
|
+
recommended: true,
|
|
90
|
+
},
|
|
91
|
+
hasSuggestions: true,
|
|
92
|
+
schema: [],
|
|
93
|
+
messages,
|
|
94
|
+
languages: ['js/js'],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default config;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestCallback,
|
|
5
|
+
parseAssertionCall,
|
|
6
|
+
createContextTracker,
|
|
7
|
+
isAssertionCallWithSupportedContext,
|
|
8
|
+
} from './utils/node-test.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID = 'require-assertion/error';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID]: 'Test is missing an assertion. Tests without assertions will always pass.',
|
|
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
|
+
/*
|
|
24
|
+
Stack of {callNode, callback, hasAssertion} for each open test call.
|
|
25
|
+
We push when we enter a test call with an inline callback, and pop (and possibly report) on exit.
|
|
26
|
+
*/
|
|
27
|
+
const testStack = [];
|
|
28
|
+
const tracker = createContextTracker(imports);
|
|
29
|
+
|
|
30
|
+
context.on('CallExpression', node => {
|
|
31
|
+
tracker.update(node);
|
|
32
|
+
|
|
33
|
+
const parsed = parseTestCall(node, imports);
|
|
34
|
+
|
|
35
|
+
// Track nested test calls as their own scope (don't let their assertions count for parent).
|
|
36
|
+
if (parsed && parsed.kind === 'test') {
|
|
37
|
+
const callback = getTestCallback(node);
|
|
38
|
+
// Only push if there's an inline function body to inspect.
|
|
39
|
+
if (callback) {
|
|
40
|
+
testStack.push({callNode: node, callback, hasAssertion: false});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// No inline callback: skip/todo or external implementation — don't report.
|
|
45
|
+
if (testStack.length > 0) {
|
|
46
|
+
// Mark parent as having an assertion-like (external impl may assert).
|
|
47
|
+
testStack.at(-1).hasAssertion = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if this call is an assertion.
|
|
54
|
+
if (testStack.length > 0 && parseAssertionCall(node, imports) && isAssertionCallWithSupportedContext(node, tracker)) {
|
|
55
|
+
testStack.at(-1).hasAssertion = true;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
context.onExit('CallExpression', node => {
|
|
60
|
+
tracker.leave(node);
|
|
61
|
+
|
|
62
|
+
if (testStack.length === 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const top = testStack.at(-1);
|
|
67
|
+
if (top.callNode !== node) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
testStack.pop();
|
|
72
|
+
|
|
73
|
+
if (!top.hasAssertion) {
|
|
74
|
+
return {
|
|
75
|
+
node,
|
|
76
|
+
messageId: MESSAGE_ID,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Propagate to parent: a nested test call itself doesn't count as an assertion in the parent.
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
85
|
+
const config = {
|
|
86
|
+
create,
|
|
87
|
+
meta: {
|
|
88
|
+
type: 'problem',
|
|
89
|
+
docs: {
|
|
90
|
+
description: 'Require that each test contains at least one assertion.',
|
|
91
|
+
recommended: true,
|
|
92
|
+
},
|
|
93
|
+
schema: [],
|
|
94
|
+
messages,
|
|
95
|
+
languages: ['js/js'],
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default config;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {resolveImports, createContextTracker} from './utils/node-test.js';
|
|
2
|
+
import isFunction from './ast/is-function.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'require-await-concurrent-subtests';
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
[MESSAGE_ID]: 'Subtests created in a `{{method}}()` callback are not awaited, so they are cancelled when the parent test finishes. Use `await Promise.all(items.map(item => t.test(…)))`.',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Array methods commonly used to create one subtest per element.
|
|
11
|
+
const ITERATION_METHODS = new Set(['map', 'forEach', 'flatMap']);
|
|
12
|
+
|
|
13
|
+
/** Find the iteration call (`xs.map(cb)`) whose callback directly encloses `node`, or `undefined`. */
|
|
14
|
+
function findEnclosingIterationCall(node) {
|
|
15
|
+
let current = node.parent;
|
|
16
|
+
while (current) {
|
|
17
|
+
if (isFunction(current)) {
|
|
18
|
+
const {parent} = current;
|
|
19
|
+
if (
|
|
20
|
+
parent?.type === 'CallExpression'
|
|
21
|
+
&& parent.callee.type === 'MemberExpression'
|
|
22
|
+
&& !parent.callee.computed
|
|
23
|
+
&& parent.callee.property.type === 'Identifier'
|
|
24
|
+
&& ITERATION_METHODS.has(parent.callee.property.name)
|
|
25
|
+
&& parent.arguments.includes(current)
|
|
26
|
+
) {
|
|
27
|
+
return parent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Any other function is a scope boundary (the test callback or a helper).
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
current = current.parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether the iteration call is an argument to a consumed `Promise.all(…)` / `Promise.allSettled(…)`. */
|
|
39
|
+
function isAwaitedViaPromiseAll(iterationCall) {
|
|
40
|
+
const {parent} = iterationCall;
|
|
41
|
+
if (
|
|
42
|
+
parent?.type === 'CallExpression'
|
|
43
|
+
&& parent.callee.type === 'MemberExpression'
|
|
44
|
+
&& !parent.callee.computed
|
|
45
|
+
&& parent.callee.property.type === 'Identifier'
|
|
46
|
+
&& (parent.callee.property.name === 'all' || parent.callee.property.name === 'allSettled')
|
|
47
|
+
&& parent.callee.object.type === 'Identifier'
|
|
48
|
+
&& parent.callee.object.name === 'Promise'
|
|
49
|
+
&& parent.arguments.includes(iterationCall)
|
|
50
|
+
) {
|
|
51
|
+
// The `Promise.all(…)` itself must be consumed (awaited, returned, or assigned), not discarded —
|
|
52
|
+
// otherwise the parent test still finishes before the subtests settle. It is discarded when left
|
|
53
|
+
// as a floating bare statement or explicitly thrown away with `void`.
|
|
54
|
+
const {parent: grandparent} = parent;
|
|
55
|
+
const isDiscarded = grandparent?.type === 'ExpressionStatement'
|
|
56
|
+
|| (grandparent?.type === 'UnaryExpression' && grandparent.operator === 'void');
|
|
57
|
+
return !isDiscarded;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
64
|
+
const create = context => {
|
|
65
|
+
const imports = resolveImports(context);
|
|
66
|
+
if (!imports.isTestFile) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tracker = createContextTracker(imports);
|
|
71
|
+
|
|
72
|
+
context.on('CallExpression', node => {
|
|
73
|
+
const isSubtest = tracker.isSubtestCall(node);
|
|
74
|
+
|
|
75
|
+
let problem;
|
|
76
|
+
// A bare-statement subtest is already covered by `no-unawaited-subtest`; this rule covers the
|
|
77
|
+
// expression-body and `return` forms inside an iteration callback that it misses.
|
|
78
|
+
if (isSubtest && node.parent?.type !== 'ExpressionStatement') {
|
|
79
|
+
const iterationCall = findEnclosingIterationCall(node);
|
|
80
|
+
if (iterationCall) {
|
|
81
|
+
const method = iterationCall.callee.property.name;
|
|
82
|
+
// `forEach` discards its callbacks' results entirely; `map`/`flatMap` are fine only when
|
|
83
|
+
// the resulting array is awaited via `Promise.all`.
|
|
84
|
+
const handled = method !== 'forEach' && isAwaitedViaPromiseAll(iterationCall);
|
|
85
|
+
if (!handled) {
|
|
86
|
+
problem = {
|
|
87
|
+
node,
|
|
88
|
+
messageId: MESSAGE_ID,
|
|
89
|
+
data: {method},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
tracker.update(node);
|
|
96
|
+
return problem;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
context.onExit('CallExpression', node => {
|
|
100
|
+
tracker.leave(node);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
105
|
+
const config = {
|
|
106
|
+
create,
|
|
107
|
+
meta: {
|
|
108
|
+
type: 'problem',
|
|
109
|
+
docs: {
|
|
110
|
+
description: 'Require subtests created in a loop callback to be awaited.',
|
|
111
|
+
recommended: 'unopinionated',
|
|
112
|
+
},
|
|
113
|
+
schema: [],
|
|
114
|
+
messages,
|
|
115
|
+
languages: ['js/js'],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default config;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
createContextTracker,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'require-context-assert-with-plan';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'This assertion is not counted by `{{context}}.plan()`. Use `{{context}}.assert` so the runner counts it toward the plan.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Get the context name of a `<context>.plan(…)` call, or `undefined`. */
|
|
15
|
+
function getPlanContextName(node) {
|
|
16
|
+
const {callee} = node;
|
|
17
|
+
if (
|
|
18
|
+
callee.type === 'MemberExpression'
|
|
19
|
+
&& !callee.computed
|
|
20
|
+
&& callee.property.type === 'Identifier'
|
|
21
|
+
&& callee.property.name === 'plan'
|
|
22
|
+
&& callee.object.type === 'Identifier'
|
|
23
|
+
) {
|
|
24
|
+
return callee.object.name;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a call is the `<context>.assert.method(…)` form, which the plan does count. */
|
|
31
|
+
function isContextAssertCall(node, tracker) {
|
|
32
|
+
const {callee} = node;
|
|
33
|
+
return (
|
|
34
|
+
callee.type === 'MemberExpression'
|
|
35
|
+
&& callee.object.type === 'MemberExpression'
|
|
36
|
+
&& !callee.object.computed
|
|
37
|
+
&& callee.object.property.type === 'Identifier'
|
|
38
|
+
&& callee.object.property.name === 'assert'
|
|
39
|
+
&& callee.object.object.type === 'Identifier'
|
|
40
|
+
&& tracker.isContextName(callee.object.object.name)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
45
|
+
const create = context => {
|
|
46
|
+
const imports = resolveImports(context);
|
|
47
|
+
// Without a `node:assert` import the only assertions are `t.assert.*` (which count toward the
|
|
48
|
+
// plan and are excluded below), so there is nothing to report.
|
|
49
|
+
if (!imports.isTestFile || !imports.hasAssert) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tracker = createContextTracker(imports);
|
|
54
|
+
|
|
55
|
+
// One frame per enclosing test/subtest. Assertions attach to the innermost; the frame is
|
|
56
|
+
// reported only if its test called `plan()`.
|
|
57
|
+
const frames = [];
|
|
58
|
+
|
|
59
|
+
context.on('CallExpression', node => {
|
|
60
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test' || tracker.isSubtestCall(node);
|
|
61
|
+
tracker.update(node);
|
|
62
|
+
|
|
63
|
+
if (isTest) {
|
|
64
|
+
frames.push({
|
|
65
|
+
node, contextName: tracker.current(), hasPlan: false, assertions: [],
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (frames.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const planContextName = getPlanContextName(node);
|
|
75
|
+
if (planContextName !== undefined) {
|
|
76
|
+
// Mark the innermost frame whose test owns this context.
|
|
77
|
+
for (let index = frames.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
if (frames[index].contextName === planContextName) {
|
|
79
|
+
frames[index].hasPlan = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (parseAssertionCall(node, imports) && !isContextAssertCall(node, tracker)) {
|
|
88
|
+
frames.at(-1).assertions.push(node);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
context.onExit('CallExpression', node => {
|
|
93
|
+
tracker.leave(node);
|
|
94
|
+
|
|
95
|
+
if (frames.at(-1)?.node !== node) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const frame = frames.pop();
|
|
100
|
+
if (!frame.hasPlan || frame.contextName === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return frame.assertions.map(assertion => ({
|
|
105
|
+
node: assertion,
|
|
106
|
+
messageId: MESSAGE_ID,
|
|
107
|
+
data: {context: frame.contextName},
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
113
|
+
const config = {
|
|
114
|
+
create,
|
|
115
|
+
meta: {
|
|
116
|
+
type: 'problem',
|
|
117
|
+
docs: {
|
|
118
|
+
description: 'Require assertions to use the test context when the test sets a plan.',
|
|
119
|
+
recommended: 'unopinionated',
|
|
120
|
+
},
|
|
121
|
+
schema: [],
|
|
122
|
+
messages,
|
|
123
|
+
languages: ['js/js'],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default config;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import isFunction from './ast/is-function.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'require-hook';
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
[MESSAGE_ID]: 'This runs when the file is loaded, not as part of a test. Move it into a `before`, `beforeEach`, `after`, or `afterEach` hook.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
Whether the statement sits directly in a registration-time scope: the module top level or a
|
|
17
|
+
`describe`/`suite` body. Statements inside a test/hook callback or a helper function are fine.
|
|
18
|
+
*/
|
|
19
|
+
function isInRegistrationScope(statement, imports) {
|
|
20
|
+
const {parent} = statement;
|
|
21
|
+
if (parent.type === 'Program') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (parent.type !== 'BlockStatement') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const callback = parent.parent;
|
|
30
|
+
if (
|
|
31
|
+
!isFunction(callback)
|
|
32
|
+
|| callback.parent?.type !== 'CallExpression'
|
|
33
|
+
|| getTestCallback(callback.parent) !== callback
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return parseTestCall(callback.parent, imports)?.kind === 'suite';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
42
|
+
const create = context => {
|
|
43
|
+
const {sourceCode} = context;
|
|
44
|
+
const imports = resolveImports(context);
|
|
45
|
+
if (!imports.isTestFile) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const allow = new Set(context.options[0].allow);
|
|
50
|
+
|
|
51
|
+
context.on('ExpressionStatement', node => {
|
|
52
|
+
const call = node.expression;
|
|
53
|
+
if (call.type !== 'CallExpression') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The test/suite/hook registration calls themselves belong here.
|
|
58
|
+
if (parseTestCall(call, imports)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Misplaced assertions are reported by `no-assert-in-describe`.
|
|
63
|
+
if (parseAssertionCall(call, imports)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (allow.has(sourceCode.getText(call.callee))) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isInRegistrationScope(node, imports)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {node, messageId: MESSAGE_ID};
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
80
|
+
const config = {
|
|
81
|
+
create,
|
|
82
|
+
meta: {
|
|
83
|
+
type: 'suggestion',
|
|
84
|
+
docs: {
|
|
85
|
+
description: 'Require setup and teardown code to be inside a hook.',
|
|
86
|
+
recommended: false,
|
|
87
|
+
},
|
|
88
|
+
schema: [
|
|
89
|
+
{
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
allow: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: {type: 'string'},
|
|
95
|
+
uniqueItems: true,
|
|
96
|
+
description: 'Callee expressions allowed at the top level (for example, `["console.log"]`).',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
defaultOptions: [{allow: []}],
|
|
103
|
+
messages,
|
|
104
|
+
languages: ['js/js'],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default config;
|