@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,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestOptions,
|
|
5
|
+
findOptionsProperty,
|
|
6
|
+
MODIFIERS,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID_PREFER_CHAINED = 'consistent-modifier-style/prefer-chained';
|
|
10
|
+
const MESSAGE_ID_PREFER_OPTIONS = 'consistent-modifier-style/prefer-options';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID_PREFER_CHAINED]: 'Use the chained modifier `.{{modifier}}` instead of the `{{modifier}}` option.',
|
|
14
|
+
[MESSAGE_ID_PREFER_OPTIONS]: 'Use the `{{modifier}}` option instead of the chained `.{{modifier}}`.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
18
|
+
const create = context => {
|
|
19
|
+
const imports = resolveImports(context);
|
|
20
|
+
if (!imports.isTestFile) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {style} = context.options[0];
|
|
25
|
+
|
|
26
|
+
context.on('CallExpression', node => {
|
|
27
|
+
const parsed = parseTestCall(node, imports);
|
|
28
|
+
if (!parsed) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Only tests and suites have `.only`/`.skip`/`.todo`; hooks have no chained modifier form.
|
|
33
|
+
if (parsed.kind === 'hook') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (style === 'chained') {
|
|
38
|
+
// Flag modifiers expressed through the options object, but only `modifier: true`. A string
|
|
39
|
+
// reason (`{skip: 'why'}`), `false`, or a dynamic value has no equivalent chained form.
|
|
40
|
+
const options = getTestOptions(node);
|
|
41
|
+
const problems = [];
|
|
42
|
+
for (const modifier of MODIFIERS) {
|
|
43
|
+
const property = findOptionsProperty(options, modifier);
|
|
44
|
+
if (property?.value.type === 'Literal' && property.value.value === true) {
|
|
45
|
+
problems.push({
|
|
46
|
+
node: property,
|
|
47
|
+
messageId: MESSAGE_ID_PREFER_CHAINED,
|
|
48
|
+
data: {modifier},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return problems;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// `style === 'options'` — flag chained modifiers. `parsed.modifiers` includes any member
|
|
57
|
+
// after the test binding, so restrict to the real modifiers (`only`/`skip`/`todo`).
|
|
58
|
+
return parsed.modifiers
|
|
59
|
+
.filter(modifier => MODIFIERS.has(modifier.name))
|
|
60
|
+
.map(modifier => ({
|
|
61
|
+
node: modifier,
|
|
62
|
+
messageId: MESSAGE_ID_PREFER_OPTIONS,
|
|
63
|
+
data: {modifier: modifier.name},
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
69
|
+
const config = {
|
|
70
|
+
create,
|
|
71
|
+
meta: {
|
|
72
|
+
type: 'suggestion',
|
|
73
|
+
docs: {
|
|
74
|
+
description: 'Enforce a consistent style for test modifiers.',
|
|
75
|
+
recommended: false,
|
|
76
|
+
},
|
|
77
|
+
schema: [
|
|
78
|
+
{
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
style: {
|
|
82
|
+
enum: ['chained', 'options'],
|
|
83
|
+
description: 'Whether modifiers should be chained (`test.skip()`) or passed as options (`{skip: true}`).',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
defaultOptions: [{style: 'chained'}],
|
|
90
|
+
messages,
|
|
91
|
+
languages: ['js/js'],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default config;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'consistent-test-context-name';
|
|
9
|
+
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID]: 'Name the test context parameter `{{expected}}` instead of `{{actual}}`.',
|
|
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 expected = context.options[0].name;
|
|
22
|
+
const tracker = createContextTracker(imports);
|
|
23
|
+
|
|
24
|
+
context.on('CallExpression', node => {
|
|
25
|
+
const isSubtest = tracker.isSubtestCall(node);
|
|
26
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test';
|
|
27
|
+
tracker.update(node);
|
|
28
|
+
|
|
29
|
+
if (!isTest && !isSubtest) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const parameter = getTestCallback(node)?.params[0];
|
|
34
|
+
if (parameter?.type === 'Identifier' && parameter.name !== expected) {
|
|
35
|
+
return {
|
|
36
|
+
node: parameter,
|
|
37
|
+
messageId: MESSAGE_ID,
|
|
38
|
+
data: {expected, actual: parameter.name},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
context.onExit('CallExpression', node => {
|
|
44
|
+
tracker.leave(node);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
49
|
+
const config = {
|
|
50
|
+
create,
|
|
51
|
+
meta: {
|
|
52
|
+
type: 'suggestion',
|
|
53
|
+
docs: {
|
|
54
|
+
description: 'Enforce a consistent name for the test context parameter.',
|
|
55
|
+
recommended: 'unopinionated',
|
|
56
|
+
},
|
|
57
|
+
schema: [
|
|
58
|
+
{
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'The required name for the test context parameter.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
defaultOptions: [{name: 't'}],
|
|
70
|
+
messages,
|
|
71
|
+
languages: ['js/js'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default config;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {resolveImports} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'consistent-test-filename';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Test file name `{{name}}` does not match the required pattern `{{pattern}}`.',
|
|
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 filename = context.physicalFilename ?? context.filename;
|
|
17
|
+
// No real file (for example, linting stdin), so there is nothing to check.
|
|
18
|
+
if (!filename || filename === '<input>' || filename === '<text>') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {pattern} = context.options[0];
|
|
23
|
+
|
|
24
|
+
let patternRegExp;
|
|
25
|
+
try {
|
|
26
|
+
patternRegExp = new RegExp(pattern, 'v');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`Invalid \`pattern\` option for \`consistent-test-filename\`: ${error.message}`, {cause: error});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const name = filename.split(/[/\\]/).pop();
|
|
32
|
+
if (patternRegExp.test(name)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
context.on('Program', node => ({
|
|
37
|
+
node,
|
|
38
|
+
messageId: MESSAGE_ID,
|
|
39
|
+
data: {name, pattern},
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
44
|
+
const config = {
|
|
45
|
+
create,
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'suggestion',
|
|
48
|
+
docs: {
|
|
49
|
+
description: 'Enforce a consistent test file name pattern.',
|
|
50
|
+
recommended: false,
|
|
51
|
+
},
|
|
52
|
+
schema: [
|
|
53
|
+
{
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
pattern: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'A regular expression the test file name must match.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
defaultOptions: [{pattern: String.raw`\.test\.[cm]?[jt]sx?$`}],
|
|
65
|
+
messages,
|
|
66
|
+
languages: ['js/js'],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default config;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, createSuiteDepthTracker} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'consistent-test-it';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Prefer `{{expected}}` over `{{actual}}` {{location}}.',
|
|
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 {fn: topLevelName, withinDescribe: withinDescribeName} = context.options[0];
|
|
17
|
+
|
|
18
|
+
const tracker = createSuiteDepthTracker();
|
|
19
|
+
|
|
20
|
+
context.on('CallExpression', node => {
|
|
21
|
+
const parsed = parseTestCall(node, imports);
|
|
22
|
+
if (!parsed) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let problem;
|
|
27
|
+
if (parsed.kind === 'test') {
|
|
28
|
+
const isInDescribe = tracker.depth > 0;
|
|
29
|
+
const expected = isInDescribe ? withinDescribeName : topLevelName;
|
|
30
|
+
if (parsed.name !== expected) {
|
|
31
|
+
problem = {
|
|
32
|
+
node,
|
|
33
|
+
messageId: MESSAGE_ID,
|
|
34
|
+
data: {
|
|
35
|
+
expected,
|
|
36
|
+
actual: parsed.name,
|
|
37
|
+
location: isInDescribe ? 'inside a `describe`' : 'at the top level',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
} else if (parsed.kind === 'suite') {
|
|
42
|
+
tracker.enterSuite(node);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return problem;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
context.onExit('CallExpression', node => {
|
|
49
|
+
tracker.exitSuite(node);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
54
|
+
const config = {
|
|
55
|
+
create,
|
|
56
|
+
meta: {
|
|
57
|
+
type: 'suggestion',
|
|
58
|
+
docs: {
|
|
59
|
+
description: 'Enforce consistent use of `test` or `it`.',
|
|
60
|
+
recommended: false,
|
|
61
|
+
},
|
|
62
|
+
schema: [
|
|
63
|
+
{
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
fn: {
|
|
67
|
+
enum: ['test', 'it'],
|
|
68
|
+
description: 'The name to use for top-level test cases.',
|
|
69
|
+
},
|
|
70
|
+
withinDescribe: {
|
|
71
|
+
enum: ['test', 'it'],
|
|
72
|
+
description: 'The name to use for test cases inside a `describe`.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
defaultOptions: [{fn: 'test', withinDescribe: 'it'}],
|
|
79
|
+
messages,
|
|
80
|
+
languages: ['js/js'],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default config;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {isCommaToken} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {getParentheses} from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
|
6
|
+
@import * as ESLint from 'eslint';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
@param {ESTree.CallExpressionArgument} node
|
|
11
|
+
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
|
12
|
+
@returns {Array<number>}
|
|
13
|
+
*/
|
|
14
|
+
function getArgumentRemovalRange(node, context) {
|
|
15
|
+
const callOrNewExpression = node.parent;
|
|
16
|
+
const index = callOrNewExpression.arguments.indexOf(node);
|
|
17
|
+
const parentheses = getParentheses(node, context);
|
|
18
|
+
const firstToken = parentheses[0] || node;
|
|
19
|
+
const lastToken = parentheses.at(-1) || node;
|
|
20
|
+
const {sourceCode} = context;
|
|
21
|
+
|
|
22
|
+
let [start] = sourceCode.getRange(firstToken);
|
|
23
|
+
let [, end] = sourceCode.getRange(lastToken);
|
|
24
|
+
|
|
25
|
+
if (callOrNewExpression.arguments.length === 1) {
|
|
26
|
+
// The only argument: also drop a dangling trailing comma if present (`fn(a,)`).
|
|
27
|
+
const tokenAfter = sourceCode.getTokenAfter(lastToken);
|
|
28
|
+
if (isCommaToken(tokenAfter)) {
|
|
29
|
+
[, end] = sourceCode.getRange(tokenAfter);
|
|
30
|
+
}
|
|
31
|
+
} else if (index === 0) {
|
|
32
|
+
// First of several: remove it through the following comma and the gap after it, so
|
|
33
|
+
// `fn(a, b)` becomes `fn(b)` rather than `fn( b)`.
|
|
34
|
+
const commaToken = sourceCode.getTokenAfter(lastToken);
|
|
35
|
+
const nextArgumentToken = sourceCode.getTokenAfter(commaToken);
|
|
36
|
+
[end] = sourceCode.getRange(nextArgumentToken);
|
|
37
|
+
} else {
|
|
38
|
+
// Otherwise remove the comma that precedes it.
|
|
39
|
+
const commaToken = sourceCode.getTokenBefore(firstToken);
|
|
40
|
+
[start] = sourceCode.getRange(commaToken);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [start, end];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
@param {ESLint.Rule.RuleFixer} fixer
|
|
48
|
+
@param {ESTree.CallExpressionArgument} node
|
|
49
|
+
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
|
50
|
+
@returns {ESLint.Rule.Fix}
|
|
51
|
+
*/
|
|
52
|
+
export default function removeArgument(fixer, node, context) {
|
|
53
|
+
return fixer.removeRange(getArgumentRemovalRange(node, context));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
getArgumentRemovalRange,
|
|
58
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {getParenthesizedRange} from '../utils/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import {TSESTree as ESTree} from '@typescript-eslint/types';
|
|
5
|
+
@import * as ESLint from 'eslint';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
|
|
10
|
+
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
|
11
|
+
@param {string} text
|
|
12
|
+
@returns {ESLint.Rule.ReportFixer}
|
|
13
|
+
*/
|
|
14
|
+
export function replaceMemberExpressionProperty(fixer, memberExpression, context, text) {
|
|
15
|
+
const [, start] = getParenthesizedRange(memberExpression.object, context);
|
|
16
|
+
const [, end] = context.sourceCode.getRange(memberExpression);
|
|
17
|
+
return fixer.replaceTextRange([start, end], text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
@param {ESTree.MemberExpression} memberExpression - The `MemberExpression` to fix.
|
|
22
|
+
@param {ESLint.Rule.RuleContext} context - The ESLint rule context object.
|
|
23
|
+
@returns {ESLint.Rule.ReportFixer}
|
|
24
|
+
*/
|
|
25
|
+
export const removeMemberExpressionProperty = (fixer, memberExpression, context) => replaceMemberExpressionProperty(fixer, memberExpression, context, '');
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
// A trailing comment on the last hook's line would stay put while the statement text moves,
|
|
36
|
+
// misattributing it to whichever hook ends up last. The reorder replaces statement text only.
|
|
37
|
+
const lastHook = block.body[max];
|
|
38
|
+
const [trailingComment] = sourceCode.getCommentsAfter(lastHook);
|
|
39
|
+
if (trailingComment && sourceCode.getLoc(trailingComment).start.line === sourceCode.getLoc(lastHook).end.line) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Stable sort preserves the original order of same-named hooks.
|
|
44
|
+
const sorted = hooks.toSorted((a, b) => HOOK_ORDER_INDEX[a.name] - HOOK_ORDER_INDEX[b.name]);
|
|
45
|
+
|
|
46
|
+
return function * (fixer) {
|
|
47
|
+
for (const [index, hook] of hooks.entries()) {
|
|
48
|
+
if (sorted[index].statement !== hook.statement) {
|
|
49
|
+
yield fixer.replaceText(hook.statement, sourceCode.getText(sorted[index].statement));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getBlockProblems(block, hooks, sourceCode) {
|
|
56
|
+
const problems = [];
|
|
57
|
+
let fix;
|
|
58
|
+
let isFixComputed = false;
|
|
59
|
+
|
|
60
|
+
for (const [position, hook] of hooks.entries()) {
|
|
61
|
+
const index = HOOK_ORDER_INDEX[hook.name];
|
|
62
|
+
// A hook that appears before the current one but belongs later means the current hook
|
|
63
|
+
// is out of order and should have come first.
|
|
64
|
+
const earlierConflict = hooks.slice(0, position).find(other => HOOK_ORDER_INDEX[other.name] > index);
|
|
65
|
+
if (!earlierConflict) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Compute the single block-wide reorder fix once, lazily, and share it across the
|
|
70
|
+
// block's problems; ESLint applies it once and the re-lint finds the block sorted.
|
|
71
|
+
if (!isFixComputed) {
|
|
72
|
+
fix = getReorderFix(block, hooks, sourceCode);
|
|
73
|
+
isFixComputed = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
problems.push({
|
|
77
|
+
node: hook.statement.expression,
|
|
78
|
+
messageId: MESSAGE_ID,
|
|
79
|
+
data: {current: hook.name, invalid: earlierConflict.name},
|
|
80
|
+
fix,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return problems;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
88
|
+
const create = context => {
|
|
89
|
+
const {sourceCode} = context;
|
|
90
|
+
const imports = resolveImports(context);
|
|
91
|
+
if (!imports.isTestFile) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Group bare-statement hooks by their containing block, in source order. Hooks used as a
|
|
96
|
+
// sub-expression (`const x = before(…)`, `await before(…)`) cannot be moved safely and are
|
|
97
|
+
// skipped. The containing block (a `describe` body or the program) is the ordering scope.
|
|
98
|
+
const hooksByBlock = new Map();
|
|
99
|
+
|
|
100
|
+
context.on('CallExpression', node => {
|
|
101
|
+
const parsed = parseTestCall(node, imports);
|
|
102
|
+
if (parsed?.kind !== 'hook' || node.parent.type !== 'ExpressionStatement') {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const block = node.parent.parent;
|
|
107
|
+
if (block?.type !== 'BlockStatement' && block?.type !== 'Program') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let hooks = hooksByBlock.get(block);
|
|
112
|
+
if (!hooks) {
|
|
113
|
+
hooks = [];
|
|
114
|
+
hooksByBlock.set(block, hooks);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hooks.push({name: parsed.name, statement: node.parent});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
context.onExit('Program', () => {
|
|
121
|
+
const problems = [];
|
|
122
|
+
|
|
123
|
+
for (const [block, hooks] of hooksByBlock) {
|
|
124
|
+
problems.push(...getBlockProblems(block, hooks, sourceCode));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return problems;
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
132
|
+
const config = {
|
|
133
|
+
create,
|
|
134
|
+
meta: {
|
|
135
|
+
type: 'suggestion',
|
|
136
|
+
docs: {
|
|
137
|
+
description: 'Enforce a consistent order of hook declarations.',
|
|
138
|
+
recommended: 'unopinionated',
|
|
139
|
+
},
|
|
140
|
+
fixable: 'code',
|
|
141
|
+
schema: [],
|
|
142
|
+
messages,
|
|
143
|
+
languages: ['js/js'],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export default config;
|
package/rules/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated file, DO NOT edit
|
|
2
|
+
|
|
3
|
+
export {default as 'assertion-arguments'} from './assertion-arguments.js';
|
|
4
|
+
export {default as 'consistent-assert-style'} from './consistent-assert-style.js';
|
|
5
|
+
export {default as 'consistent-assert-throws-callback-style'} from './consistent-assert-throws-callback-style.js';
|
|
6
|
+
export {default as 'consistent-modifier-style'} from './consistent-modifier-style.js';
|
|
7
|
+
export {default as 'consistent-test-context-name'} from './consistent-test-context-name.js';
|
|
8
|
+
export {default as 'consistent-test-filename'} from './consistent-test-filename.js';
|
|
9
|
+
export {default as 'consistent-test-it'} from './consistent-test-it.js';
|
|
10
|
+
export {default as 'hooks-order'} from './hooks-order.js';
|
|
11
|
+
export {default as 'max-assertions'} from './max-assertions.js';
|
|
12
|
+
export {default as 'max-nested-describe'} from './max-nested-describe.js';
|
|
13
|
+
export {default as 'no-assert-in-describe'} from './no-assert-in-describe.js';
|
|
14
|
+
export {default as 'no-assert-in-hook'} from './no-assert-in-hook.js';
|
|
15
|
+
export {default as 'no-assert-match-string'} from './no-assert-match-string.js';
|
|
16
|
+
export {default as 'no-assert-throws-async'} from './no-assert-throws-async.js';
|
|
17
|
+
export {default as 'no-assert-throws-call'} from './no-assert-throws-call.js';
|
|
18
|
+
export {default as 'no-assert-throws-multiple-statements'} from './no-assert-throws-multiple-statements.js';
|
|
19
|
+
export {default as 'no-assert-throws-string'} from './no-assert-throws-string.js';
|
|
20
|
+
export {default as 'no-async-describe'} from './no-async-describe.js';
|
|
21
|
+
export {default as 'no-async-fn-without-await'} from './no-async-fn-without-await.js';
|
|
22
|
+
export {default as 'no-callback-and-promise'} from './no-callback-and-promise.js';
|
|
23
|
+
export {default as 'no-commented-tests'} from './no-commented-tests.js';
|
|
24
|
+
export {default as 'no-compound-assertion'} from './no-compound-assertion.js';
|
|
25
|
+
export {default as 'no-conditional-assertion'} from './no-conditional-assertion.js';
|
|
26
|
+
export {default as 'no-conditional-in-test'} from './no-conditional-in-test.js';
|
|
27
|
+
export {default as 'no-conditional-tests'} from './no-conditional-tests.js';
|
|
28
|
+
export {default as 'no-conflicting-modifiers'} from './no-conflicting-modifiers.js';
|
|
29
|
+
export {default as 'no-constant-assertion'} from './no-constant-assertion.js';
|
|
30
|
+
export {default as 'no-done-callback'} from './no-done-callback.js';
|
|
31
|
+
export {default as 'no-duplicate-assertions'} from './no-duplicate-assertions.js';
|
|
32
|
+
export {default as 'no-duplicate-hooks'} from './no-duplicate-hooks.js';
|
|
33
|
+
export {default as 'no-duplicate-plan'} from './no-duplicate-plan.js';
|
|
34
|
+
export {default as 'no-export'} from './no-export.js';
|
|
35
|
+
export {default as 'no-identical-assertion-arguments'} from './no-identical-assertion-arguments.js';
|
|
36
|
+
export {default as 'no-identical-title'} from './no-identical-title.js';
|
|
37
|
+
export {default as 'no-incorrect-deep-equal'} from './no-incorrect-deep-equal.js';
|
|
38
|
+
export {default as 'no-incorrect-strict-equal'} from './no-incorrect-strict-equal.js';
|
|
39
|
+
export {default as 'no-loop-static-title'} from './no-loop-static-title.js';
|
|
40
|
+
export {default as 'no-misused-concurrency'} from './no-misused-concurrency.js';
|
|
41
|
+
export {default as 'no-mock-timers-destructured-import'} from './no-mock-timers-destructured-import.js';
|
|
42
|
+
export {default as 'no-nested-tests'} from './no-nested-tests.js';
|
|
43
|
+
export {default as 'no-only-test'} from './no-only-test.js';
|
|
44
|
+
export {default as 'no-parent-test-context'} from './no-parent-test-context.js';
|
|
45
|
+
export {default as 'no-process-env-mutation'} from './no-process-env-mutation.js';
|
|
46
|
+
export {default as 'no-process-exit-in-test'} from './no-process-exit-in-test.js';
|
|
47
|
+
export {default as 'no-skip-test'} from './no-skip-test.js';
|
|
48
|
+
export {default as 'no-skip-without-reason'} from './no-skip-without-reason.js';
|
|
49
|
+
export {default as 'no-skip-without-return'} from './no-skip-without-return.js';
|
|
50
|
+
export {default as 'no-sleep-in-test'} from './no-sleep-in-test.js';
|
|
51
|
+
export {default as 'no-snapshot-in-loop'} from './no-snapshot-in-loop.js';
|
|
52
|
+
export {default as 'no-standalone-assert'} from './no-standalone-assert.js';
|
|
53
|
+
export {default as 'no-test-inside-hook'} from './no-test-inside-hook.js';
|
|
54
|
+
export {default as 'no-test-return-statement'} from './no-test-return-statement.js';
|
|
55
|
+
export {default as 'no-todo-test'} from './no-todo-test.js';
|
|
56
|
+
export {default as 'no-unawaited-promise-assertion'} from './no-unawaited-promise-assertion.js';
|
|
57
|
+
export {default as 'no-unawaited-rejects'} from './no-unawaited-rejects.js';
|
|
58
|
+
export {default as 'no-unawaited-subtest'} from './no-unawaited-subtest.js';
|
|
59
|
+
export {default as 'no-unknown-test-options'} from './no-unknown-test-options.js';
|
|
60
|
+
export {default as 'no-useless-assertion'} from './no-useless-assertion.js';
|
|
61
|
+
export {default as 'prefer-assert-match'} from './prefer-assert-match.js';
|
|
62
|
+
export {default as 'prefer-assert-throws'} from './prefer-assert-throws.js';
|
|
63
|
+
export {default as 'prefer-async-await'} from './prefer-async-await.js';
|
|
64
|
+
export {default as 'prefer-context-mock'} from './prefer-context-mock.js';
|
|
65
|
+
export {default as 'prefer-diagnostic'} from './prefer-diagnostic.js';
|
|
66
|
+
export {default as 'prefer-equality-assertion'} from './prefer-equality-assertion.js';
|
|
67
|
+
export {default as 'prefer-hooks-on-top'} from './prefer-hooks-on-top.js';
|
|
68
|
+
export {default as 'prefer-lowercase-title'} from './prefer-lowercase-title.js';
|
|
69
|
+
export {default as 'prefer-mock-method'} from './prefer-mock-method.js';
|
|
70
|
+
export {default as 'prefer-strict-assert'} from './prefer-strict-assert.js';
|
|
71
|
+
export {default as 'prefer-test-context-assert'} from './prefer-test-context-assert.js';
|
|
72
|
+
export {default as 'prefer-todo'} from './prefer-todo.js';
|
|
73
|
+
export {default as 'require-assertion'} from './require-assertion.js';
|
|
74
|
+
export {default as 'require-await-concurrent-subtests'} from './require-await-concurrent-subtests.js';
|
|
75
|
+
export {default as 'require-context-assert-with-plan'} from './require-context-assert-with-plan.js';
|
|
76
|
+
export {default as 'require-hook'} from './require-hook.js';
|
|
77
|
+
export {default as 'require-mock-timers-advance'} from './require-mock-timers-advance.js';
|
|
78
|
+
export {default as 'require-mock-timers-apis'} from './require-mock-timers-apis.js';
|
|
79
|
+
export {default as 'require-throws-expectation'} from './require-throws-expectation.js';
|
|
80
|
+
export {default as 'require-throws-validator-return-true'} from './require-throws-validator-return-true.js';
|
|
81
|
+
export {default as 'require-top-level-describe'} from './require-top-level-describe.js';
|
|
82
|
+
export {default as 'test-title-format'} from './test-title-format.js';
|
|
83
|
+
export {default as 'test-title'} from './test-title.js';
|
|
84
|
+
export {default as 'valid-describe-callback'} from './valid-describe-callback.js';
|