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,132 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'hooks-order/error';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Hook `{{current}}` must come before `{{invalid}}`.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Canonical order for node:test hooks.
|
|
10
|
+
const HOOK_ORDER = ['before', 'beforeEach', 'afterEach', 'after'];
|
|
11
|
+
const HOOK_ORDER_INDEX = Object.fromEntries(HOOK_ORDER.map((name, index) => [name, index]));
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Build the fix that reorders a block's hooks into canonical order in a single pass. Returns
|
|
15
|
+
`undefined` (no fix) when the hooks are not a contiguous run of statements, or a comment sits
|
|
16
|
+
between them — reordering would otherwise drop or misattribute code.
|
|
17
|
+
*/
|
|
18
|
+
function getReorderFix(block, hooks, sourceCode) {
|
|
19
|
+
const positions = hooks.map(hook => block.body.indexOf(hook.statement));
|
|
20
|
+
const min = Math.min(...positions);
|
|
21
|
+
const max = Math.max(...positions);
|
|
22
|
+
|
|
23
|
+
// Non-hook statements interleaved with the hooks.
|
|
24
|
+
if (max - min + 1 !== hooks.length) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A comment anywhere between consecutive hooks must not be moved.
|
|
29
|
+
for (let index = min; index < max; index += 1) {
|
|
30
|
+
if (sourceCode.getTokensBetween(block.body[index], block.body[index + 1], {includeComments: true}).length > 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Stable sort preserves the original order of same-named hooks.
|
|
36
|
+
const sorted = hooks.toSorted((a, b) => HOOK_ORDER_INDEX[a.name] - HOOK_ORDER_INDEX[b.name]);
|
|
37
|
+
|
|
38
|
+
return function * (fixer) {
|
|
39
|
+
for (const [index, hook] of hooks.entries()) {
|
|
40
|
+
if (sorted[index].statement !== hook.statement) {
|
|
41
|
+
yield fixer.replaceText(hook.statement, sourceCode.getText(sorted[index].statement));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
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
|
+
// Group bare-statement hooks by their containing block, in source order. Hooks used as a
|
|
56
|
+
// sub-expression (`const x = before(…)`, `await before(…)`) cannot be moved safely and are
|
|
57
|
+
// skipped. The containing block (a `describe` body or the program) is the ordering scope.
|
|
58
|
+
const hooksByBlock = new Map();
|
|
59
|
+
|
|
60
|
+
context.on('CallExpression', node => {
|
|
61
|
+
const parsed = parseTestCall(node, imports);
|
|
62
|
+
if (parsed?.kind !== 'hook' || node.parent.type !== 'ExpressionStatement') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const block = node.parent.parent;
|
|
67
|
+
if (block?.type !== 'BlockStatement' && block?.type !== 'Program') {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let hooks = hooksByBlock.get(block);
|
|
72
|
+
if (!hooks) {
|
|
73
|
+
hooks = [];
|
|
74
|
+
hooksByBlock.set(block, hooks);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
hooks.push({name: parsed.name, statement: node.parent});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
context.onExit('Program', () => {
|
|
81
|
+
const problems = [];
|
|
82
|
+
|
|
83
|
+
for (const [block, hooks] of hooksByBlock) {
|
|
84
|
+
let fix;
|
|
85
|
+
let fixComputed = false;
|
|
86
|
+
|
|
87
|
+
for (const [position, hook] of hooks.entries()) {
|
|
88
|
+
const index = HOOK_ORDER_INDEX[hook.name];
|
|
89
|
+
// A hook that appears before the current one but belongs later means the current hook
|
|
90
|
+
// is out of order and should have come first.
|
|
91
|
+
const earlierConflict = hooks.slice(0, position).find(other => HOOK_ORDER_INDEX[other.name] > index);
|
|
92
|
+
if (!earlierConflict) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Compute the single block-wide reorder fix once, lazily, and share it across the
|
|
97
|
+
// block's problems; ESLint applies it once and the re-lint finds the block sorted.
|
|
98
|
+
if (!fixComputed) {
|
|
99
|
+
fix = getReorderFix(block, hooks, sourceCode);
|
|
100
|
+
fixComputed = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
problems.push({
|
|
104
|
+
node: hook.statement.expression,
|
|
105
|
+
messageId: MESSAGE_ID,
|
|
106
|
+
data: {current: hook.name, invalid: earlierConflict.name},
|
|
107
|
+
fix,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return problems;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
117
|
+
const config = {
|
|
118
|
+
create,
|
|
119
|
+
meta: {
|
|
120
|
+
type: 'suggestion',
|
|
121
|
+
docs: {
|
|
122
|
+
description: 'Enforce a consistent order of hook declarations.',
|
|
123
|
+
recommended: 'unopinionated',
|
|
124
|
+
},
|
|
125
|
+
fixable: 'code',
|
|
126
|
+
schema: [],
|
|
127
|
+
messages,
|
|
128
|
+
languages: ['js/js'],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export default config;
|
package/rules/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Generated file, DO NOT edit
|
|
2
|
+
|
|
3
|
+
export {default as 'assertion-arguments'} from './assertion-arguments.js';
|
|
4
|
+
export {default as 'consistent-modifier-style'} from './consistent-modifier-style.js';
|
|
5
|
+
export {default as 'consistent-test-context-name'} from './consistent-test-context-name.js';
|
|
6
|
+
export {default as 'consistent-test-filename'} from './consistent-test-filename.js';
|
|
7
|
+
export {default as 'consistent-test-it'} from './consistent-test-it.js';
|
|
8
|
+
export {default as 'hooks-order'} from './hooks-order.js';
|
|
9
|
+
export {default as 'max-assertions'} from './max-assertions.js';
|
|
10
|
+
export {default as 'max-nested-describe'} from './max-nested-describe.js';
|
|
11
|
+
export {default as 'no-assert-in-describe'} from './no-assert-in-describe.js';
|
|
12
|
+
export {default as 'no-assert-in-hook'} from './no-assert-in-hook.js';
|
|
13
|
+
export {default as 'no-assert-throws-async'} from './no-assert-throws-async.js';
|
|
14
|
+
export {default as 'no-assert-throws-string'} from './no-assert-throws-string.js';
|
|
15
|
+
export {default as 'no-async-describe'} from './no-async-describe.js';
|
|
16
|
+
export {default as 'no-async-fn-without-await'} from './no-async-fn-without-await.js';
|
|
17
|
+
export {default as 'no-callback-and-promise'} from './no-callback-and-promise.js';
|
|
18
|
+
export {default as 'no-commented-tests'} from './no-commented-tests.js';
|
|
19
|
+
export {default as 'no-conditional-assertion'} from './no-conditional-assertion.js';
|
|
20
|
+
export {default as 'no-conditional-in-test'} from './no-conditional-in-test.js';
|
|
21
|
+
export {default as 'no-conditional-tests'} from './no-conditional-tests.js';
|
|
22
|
+
export {default as 'no-conflicting-modifiers'} from './no-conflicting-modifiers.js';
|
|
23
|
+
export {default as 'no-done-callback'} from './no-done-callback.js';
|
|
24
|
+
export {default as 'no-duplicate-hooks'} from './no-duplicate-hooks.js';
|
|
25
|
+
export {default as 'no-export'} from './no-export.js';
|
|
26
|
+
export {default as 'no-identical-assertion-arguments'} from './no-identical-assertion-arguments.js';
|
|
27
|
+
export {default as 'no-identical-title'} from './no-identical-title.js';
|
|
28
|
+
export {default as 'no-incorrect-deep-equal'} from './no-incorrect-deep-equal.js';
|
|
29
|
+
export {default as 'no-incorrect-strict-equal'} from './no-incorrect-strict-equal.js';
|
|
30
|
+
export {default as 'no-loop-static-title'} from './no-loop-static-title.js';
|
|
31
|
+
export {default as 'no-misused-concurrency'} from './no-misused-concurrency.js';
|
|
32
|
+
export {default as 'no-mock-timers-destructured-import'} from './no-mock-timers-destructured-import.js';
|
|
33
|
+
export {default as 'no-nested-tests'} from './no-nested-tests.js';
|
|
34
|
+
export {default as 'no-only-test'} from './no-only-test.js';
|
|
35
|
+
export {default as 'no-skip-test'} from './no-skip-test.js';
|
|
36
|
+
export {default as 'no-skip-without-reason'} from './no-skip-without-reason.js';
|
|
37
|
+
export {default as 'no-skip-without-return'} from './no-skip-without-return.js';
|
|
38
|
+
export {default as 'no-standalone-assert'} from './no-standalone-assert.js';
|
|
39
|
+
export {default as 'no-test-inside-hook'} from './no-test-inside-hook.js';
|
|
40
|
+
export {default as 'no-test-return-statement'} from './no-test-return-statement.js';
|
|
41
|
+
export {default as 'no-todo-test'} from './no-todo-test.js';
|
|
42
|
+
export {default as 'no-unawaited-rejects'} from './no-unawaited-rejects.js';
|
|
43
|
+
export {default as 'no-unawaited-subtest'} from './no-unawaited-subtest.js';
|
|
44
|
+
export {default as 'no-unknown-test-options'} from './no-unknown-test-options.js';
|
|
45
|
+
export {default as 'no-useless-assertion'} from './no-useless-assertion.js';
|
|
46
|
+
export {default as 'prefer-assert-match'} from './prefer-assert-match.js';
|
|
47
|
+
export {default as 'prefer-assert-throws'} from './prefer-assert-throws.js';
|
|
48
|
+
export {default as 'prefer-async-await'} from './prefer-async-await.js';
|
|
49
|
+
export {default as 'prefer-context-mock'} from './prefer-context-mock.js';
|
|
50
|
+
export {default as 'prefer-diagnostic'} from './prefer-diagnostic.js';
|
|
51
|
+
export {default as 'prefer-equality-assertion'} from './prefer-equality-assertion.js';
|
|
52
|
+
export {default as 'prefer-hooks-on-top'} from './prefer-hooks-on-top.js';
|
|
53
|
+
export {default as 'prefer-lowercase-title'} from './prefer-lowercase-title.js';
|
|
54
|
+
export {default as 'prefer-mock-method'} from './prefer-mock-method.js';
|
|
55
|
+
export {default as 'prefer-strict-assert'} from './prefer-strict-assert.js';
|
|
56
|
+
export {default as 'prefer-test-context-assert'} from './prefer-test-context-assert.js';
|
|
57
|
+
export {default as 'prefer-todo'} from './prefer-todo.js';
|
|
58
|
+
export {default as 'require-assertion'} from './require-assertion.js';
|
|
59
|
+
export {default as 'require-await-concurrent-subtests'} from './require-await-concurrent-subtests.js';
|
|
60
|
+
export {default as 'require-context-assert-with-plan'} from './require-context-assert-with-plan.js';
|
|
61
|
+
export {default as 'require-hook'} from './require-hook.js';
|
|
62
|
+
export {default as 'require-throws-expectation'} from './require-throws-expectation.js';
|
|
63
|
+
export {default as 'require-top-level-describe'} from './require-top-level-describe.js';
|
|
64
|
+
export {default as 'test-title-format'} from './test-title-format.js';
|
|
65
|
+
export {default as 'test-title'} from './test-title.js';
|
|
66
|
+
export {default as 'valid-describe-callback'} from './valid-describe-callback.js';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
createContextTracker,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'max-assertions';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'Too many assertions ({{count}}). Maximum allowed is {{max}}.',
|
|
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
|
+
const {max} = context.options[0];
|
|
22
|
+
const tracker = createContextTracker(imports);
|
|
23
|
+
|
|
24
|
+
// One frame per enclosing test/subtest; assertions count toward the innermost.
|
|
25
|
+
const frames = [];
|
|
26
|
+
|
|
27
|
+
context.on('CallExpression', node => {
|
|
28
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test' || tracker.isSubtestCall(node);
|
|
29
|
+
tracker.update(node);
|
|
30
|
+
|
|
31
|
+
if (isTest) {
|
|
32
|
+
frames.push({node, count: 0});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (frames.length > 0 && parseAssertionCall(node, imports)) {
|
|
37
|
+
frames.at(-1).count += 1;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
context.onExit('CallExpression', node => {
|
|
42
|
+
tracker.leave(node);
|
|
43
|
+
|
|
44
|
+
if (frames.at(-1)?.node !== node) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const {count} = frames.pop();
|
|
49
|
+
if (count > max) {
|
|
50
|
+
return {
|
|
51
|
+
node,
|
|
52
|
+
messageId: MESSAGE_ID,
|
|
53
|
+
data: {count, max},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
60
|
+
const config = {
|
|
61
|
+
create,
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'suggestion',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Enforce a maximum number of assertions in a test.',
|
|
66
|
+
recommended: false,
|
|
67
|
+
},
|
|
68
|
+
schema: [
|
|
69
|
+
{
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
max: {
|
|
73
|
+
type: 'integer',
|
|
74
|
+
minimum: 1,
|
|
75
|
+
description: 'The maximum number of assertions allowed in a test.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
defaultOptions: [{max: 5}],
|
|
82
|
+
messages,
|
|
83
|
+
languages: ['js/js'],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default config;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, createSuiteDepthTracker} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'max-nested-describe';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Describe blocks are nested too deeply ({{depth}}). Maximum allowed is {{max}}.',
|
|
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
|
+
const {max} = context.options[0];
|
|
17
|
+
|
|
18
|
+
const tracker = createSuiteDepthTracker();
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
const parsed = parseTestCall(node, imports);
|
|
22
|
+
if (parsed?.kind !== 'suite') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tracker.enterSuite(node);
|
|
27
|
+
|
|
28
|
+
if (tracker.depth > max) {
|
|
29
|
+
return {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
data: {depth: tracker.depth, max},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
context.onExit('CallExpression', node => {
|
|
38
|
+
tracker.exitSuite(node);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
43
|
+
const config = {
|
|
44
|
+
create,
|
|
45
|
+
meta: {
|
|
46
|
+
type: 'suggestion',
|
|
47
|
+
docs: {
|
|
48
|
+
description: 'Enforce a maximum depth for nested `describe` blocks.',
|
|
49
|
+
recommended: true,
|
|
50
|
+
},
|
|
51
|
+
schema: [
|
|
52
|
+
{
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
max: {
|
|
56
|
+
type: 'integer',
|
|
57
|
+
minimum: 1,
|
|
58
|
+
description: 'The maximum allowed depth of nested `describe` blocks.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
defaultOptions: [{max: 5}],
|
|
65
|
+
messages,
|
|
66
|
+
languages: ['js/js'],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default config;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
nearestTestCallbackKind,
|
|
5
|
+
} from './utils/node-test.js';
|
|
6
|
+
|
|
7
|
+
const MESSAGE_ID = 'no-assert-in-describe';
|
|
8
|
+
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: 'Assertion runs when the suite is built, not when a test runs. Move it into a test or hook.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
14
|
+
const create = context => {
|
|
15
|
+
const imports = resolveImports(context);
|
|
16
|
+
if (!imports.isTestFile) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
if (!parseAssertionCall(node, imports)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (nearestTestCallbackKind(node, imports) !== 'suite') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
37
|
+
const config = {
|
|
38
|
+
create,
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Disallow assertions directly inside a `describe` body.',
|
|
43
|
+
recommended: true,
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
messages,
|
|
47
|
+
languages: ['js/js'],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default config;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
nearestTestCallbackKind,
|
|
5
|
+
} from './utils/node-test.js';
|
|
6
|
+
|
|
7
|
+
const MESSAGE_ID = 'no-assert-in-hook';
|
|
8
|
+
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: 'Avoid assertions inside a hook. A hook failure is attributed to every affected test rather than reported as a focused failure. Assert inside a test instead.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
14
|
+
const create = context => {
|
|
15
|
+
const imports = resolveImports(context);
|
|
16
|
+
if (!imports.isTestFile) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
if (!parseAssertionCall(node, imports)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (nearestTestCallbackKind(node, imports) !== 'hook') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
node,
|
|
31
|
+
messageId: MESSAGE_ID,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
37
|
+
const config = {
|
|
38
|
+
create,
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'suggestion',
|
|
41
|
+
docs: {
|
|
42
|
+
description: 'Disallow assertions inside hooks.',
|
|
43
|
+
recommended: false,
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
messages,
|
|
47
|
+
languages: ['js/js'],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default config;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import isFunction from './ast/is-function.js';
|
|
3
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
4
|
+
|
|
5
|
+
const MESSAGE_ID_ERROR = 'no-assert-throws-async/error';
|
|
6
|
+
const MESSAGE_ID_SUGGESTION = 'no-assert-throws-async/suggestion';
|
|
7
|
+
|
|
8
|
+
const messages = {
|
|
9
|
+
[MESSAGE_ID_ERROR]: '`{{method}}` does not catch an async function, which never throws synchronously. Use `{{replacement}}` instead.',
|
|
10
|
+
[MESSAGE_ID_SUGGESTION]: 'Replace with `{{replacement}}`.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SYNC_TO_ASYNC = new Map([
|
|
14
|
+
['throws', 'rejects'],
|
|
15
|
+
['doesNotThrow', 'doesNotReject'],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
Walk up the ancestor chain to find the nearest enclosing function node.
|
|
20
|
+
Returns `undefined` if we hit the program root first.
|
|
21
|
+
*/
|
|
22
|
+
function findEnclosingFunction(node) {
|
|
23
|
+
let current = node.parent;
|
|
24
|
+
while (current) {
|
|
25
|
+
if (isFunction(current)) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
current = current.parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
34
|
+
const create = context => {
|
|
35
|
+
const imports = resolveImports(context);
|
|
36
|
+
if (!imports.isAssertOrTestFile) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
context.on('CallExpression', node => {
|
|
41
|
+
const assertion = parseAssertionCall(node, imports);
|
|
42
|
+
if (!assertion) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const replacement = SYNC_TO_ASYNC.get(assertion.method);
|
|
47
|
+
if (!replacement) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [firstArgument] = node.arguments;
|
|
52
|
+
if (!firstArgument) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const callback = unwrapTypeScriptExpression(firstArgument);
|
|
57
|
+
if (!isFunction(callback) || !callback.async) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {callee} = node;
|
|
62
|
+
const data = {method: assertion.method, replacement};
|
|
63
|
+
const problem = {
|
|
64
|
+
node: callee,
|
|
65
|
+
messageId: MESSAGE_ID_ERROR,
|
|
66
|
+
data,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Only the member forms (`assert.throws`, `t.assert.throws`) can be rewritten. A bare named
|
|
70
|
+
// import (`throws`) would reference an unimported `rejects`, so leave it reported but unfixed.
|
|
71
|
+
if (callee.type === 'MemberExpression') {
|
|
72
|
+
const isAwaited = node.parent?.type === 'AwaitExpression';
|
|
73
|
+
const enclosingFunction = findEnclosingFunction(node);
|
|
74
|
+
// Prepend `await` only where it is both valid and needed: a bare call statement inside an
|
|
75
|
+
// async function. Otherwise just switch the method and let `no-unawaited-rejects` guide the await.
|
|
76
|
+
const shouldAwait = !isAwaited
|
|
77
|
+
&& node.parent?.type === 'ExpressionStatement'
|
|
78
|
+
&& enclosingFunction?.async === true;
|
|
79
|
+
|
|
80
|
+
problem.suggest = [
|
|
81
|
+
{
|
|
82
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
83
|
+
data,
|
|
84
|
+
* fix(fixer) {
|
|
85
|
+
yield fixer.replaceText(callee.property, replacement);
|
|
86
|
+
if (shouldAwait) {
|
|
87
|
+
yield fixer.insertTextBefore(node, 'await ');
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return problem;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
99
|
+
const config = {
|
|
100
|
+
create,
|
|
101
|
+
meta: {
|
|
102
|
+
type: 'problem',
|
|
103
|
+
docs: {
|
|
104
|
+
description: 'Disallow passing an async function to `assert.throws()`/`assert.doesNotThrow()`.',
|
|
105
|
+
recommended: 'unopinionated',
|
|
106
|
+
},
|
|
107
|
+
hasSuggestions: true,
|
|
108
|
+
schema: [],
|
|
109
|
+
messages,
|
|
110
|
+
languages: ['js/js'],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default config;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {resolveImports, parseAssertionCall} from './utils/node-test.js';
|
|
2
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
3
|
+
import {isStringExpression} from './ast/index.js';
|
|
4
|
+
|
|
5
|
+
const MESSAGE_ID_ERROR = 'no-assert-throws-string/error';
|
|
6
|
+
const MESSAGE_ID_SUGGESTION = 'no-assert-throws-string/suggestion';
|
|
7
|
+
|
|
8
|
+
const messages = {
|
|
9
|
+
[MESSAGE_ID_ERROR]: 'The second argument to `{{method}}()` is the failure message, not an error matcher, so the thrown error is not validated.',
|
|
10
|
+
[MESSAGE_ID_SUGGESTION]: 'Match the error message with `{message: …}`.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const THROWS_METHODS = new Set(['throws', 'rejects']);
|
|
14
|
+
|
|
15
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
16
|
+
const create = context => {
|
|
17
|
+
const {sourceCode} = context;
|
|
18
|
+
const imports = resolveImports(context);
|
|
19
|
+
if (!imports.isAssertOrTestFile) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
context.on('CallExpression', node => {
|
|
24
|
+
const parsed = parseAssertionCall(node, imports);
|
|
25
|
+
if (!parsed || !THROWS_METHODS.has(parsed.method)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const errorArgument = node.arguments[1];
|
|
30
|
+
if (!errorArgument || !isStringExpression(unwrapTypeScriptExpression(errorArgument))) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
node: errorArgument,
|
|
36
|
+
messageId: MESSAGE_ID_ERROR,
|
|
37
|
+
data: {method: parsed.method},
|
|
38
|
+
suggest: [
|
|
39
|
+
{
|
|
40
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
41
|
+
/** @param {import('eslint').Rule.RuleFixer} fixer */
|
|
42
|
+
fix: fixer => fixer.replaceText(errorArgument, `{message: ${sourceCode.getText(errorArgument)}}`),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
50
|
+
const config = {
|
|
51
|
+
create,
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description: 'Disallow a string as the error matcher of `assert.throws()`/`assert.rejects()`.',
|
|
56
|
+
recommended: 'unopinionated',
|
|
57
|
+
},
|
|
58
|
+
hasSuggestions: true,
|
|
59
|
+
schema: [],
|
|
60
|
+
messages,
|
|
61
|
+
languages: ['js/js'],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default config;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-async-describe';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: '`node:test` does not await a `{{name}}` callback, so any test registered after an `await` is silently dropped. Make the callback synchronous.',
|
|
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
|
+
context.on('CallExpression', node => {
|
|
17
|
+
const parsed = parseTestCall(node, imports);
|
|
18
|
+
if (parsed?.kind !== 'suite') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const callback = getTestCallback(node);
|
|
23
|
+
if (!callback?.async) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
node: callback,
|
|
29
|
+
messageId: MESSAGE_ID,
|
|
30
|
+
data: {name: parsed.name},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
36
|
+
const config = {
|
|
37
|
+
create,
|
|
38
|
+
meta: {
|
|
39
|
+
type: 'problem',
|
|
40
|
+
docs: {
|
|
41
|
+
description: 'Disallow `async` `describe` callbacks.',
|
|
42
|
+
recommended: 'unopinionated',
|
|
43
|
+
},
|
|
44
|
+
schema: [],
|
|
45
|
+
messages,
|
|
46
|
+
languages: ['js/js'],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default config;
|