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,98 @@
|
|
|
1
|
+
/* eslint-disable complexity */
|
|
2
|
+
/**
|
|
3
|
+
@param {
|
|
4
|
+
{
|
|
5
|
+
property?: string,
|
|
6
|
+
properties?: string[],
|
|
7
|
+
object?: string,
|
|
8
|
+
objects?: string[],
|
|
9
|
+
optional?: boolean,
|
|
10
|
+
computed?: boolean
|
|
11
|
+
} | string | string[]
|
|
12
|
+
} [options]
|
|
13
|
+
@returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export default function isMemberExpression(node, options) {
|
|
16
|
+
if (node?.type !== 'MemberExpression') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof options === 'string') {
|
|
21
|
+
options = {properties: [options]};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(options)) {
|
|
25
|
+
options = {properties: options};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
property,
|
|
30
|
+
properties,
|
|
31
|
+
object,
|
|
32
|
+
objects,
|
|
33
|
+
optional,
|
|
34
|
+
computed,
|
|
35
|
+
} = {
|
|
36
|
+
property: '',
|
|
37
|
+
properties: [],
|
|
38
|
+
object: '',
|
|
39
|
+
...options,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (property) {
|
|
43
|
+
properties = [property];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (object) {
|
|
47
|
+
objects = [object];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
(optional === true && (node.optional !== optional))
|
|
52
|
+
|| (
|
|
53
|
+
optional === false
|
|
54
|
+
// `node.optional` can be `undefined` in some parsers
|
|
55
|
+
&& node.optional
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
Array.isArray(properties)
|
|
63
|
+
&& properties.length > 0
|
|
64
|
+
) {
|
|
65
|
+
if (
|
|
66
|
+
node.property.type !== 'Identifier'
|
|
67
|
+
|| !properties.includes(node.property.name)
|
|
68
|
+
) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
computed ??= false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
(computed === true && (node.computed !== computed))
|
|
77
|
+
|| (
|
|
78
|
+
computed === false
|
|
79
|
+
// `node.computed` can be `undefined` in some parsers
|
|
80
|
+
&& node.computed
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
Array.isArray(objects)
|
|
88
|
+
&& objects.length > 0
|
|
89
|
+
&& (
|
|
90
|
+
node.object.type !== 'Identifier'
|
|
91
|
+
|| !objects.includes(node.object.name)
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import isMemberExpression from './is-member-expression.js';
|
|
2
|
+
import {isCallExpression} from './call-or-new-expression.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@param {
|
|
6
|
+
{
|
|
7
|
+
// `isCallExpression` options
|
|
8
|
+
argumentsLength?: number,
|
|
9
|
+
minimumArguments?: number,
|
|
10
|
+
maximumArguments?: number,
|
|
11
|
+
optionalCall?: boolean,
|
|
12
|
+
allowSpreadElement?: boolean,
|
|
13
|
+
|
|
14
|
+
// `isMemberExpression` options
|
|
15
|
+
method?: string,
|
|
16
|
+
methods?: string[],
|
|
17
|
+
object?: string,
|
|
18
|
+
objects?: string[],
|
|
19
|
+
optionalMember?: boolean,
|
|
20
|
+
computed?: boolean
|
|
21
|
+
} | string | string[]
|
|
22
|
+
} [options]
|
|
23
|
+
@returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export default function isMethodCall(node, options) {
|
|
26
|
+
if (typeof options === 'string') {
|
|
27
|
+
options = {methods: [options]};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(options)) {
|
|
31
|
+
options = {methods: options};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
optionalCall,
|
|
36
|
+
optionalMember,
|
|
37
|
+
method,
|
|
38
|
+
methods,
|
|
39
|
+
} = {
|
|
40
|
+
method: '',
|
|
41
|
+
methods: [],
|
|
42
|
+
...options,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
isCallExpression(node, {
|
|
47
|
+
argumentsLength: options.argumentsLength,
|
|
48
|
+
minimumArguments: options.minimumArguments,
|
|
49
|
+
maximumArguments: options.maximumArguments,
|
|
50
|
+
allowSpreadElement: options.allowSpreadElement,
|
|
51
|
+
optional: optionalCall,
|
|
52
|
+
})
|
|
53
|
+
&& isMemberExpression(node.callee, {
|
|
54
|
+
object: options.object,
|
|
55
|
+
objects: options.objects,
|
|
56
|
+
computed: options.computed,
|
|
57
|
+
property: method,
|
|
58
|
+
properties: methods,
|
|
59
|
+
optional: optionalMember,
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function isLiteral(node, value) {
|
|
2
|
+
if (node?.type !== 'Literal') {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return node.value === value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const isStringLiteral = node => node?.type === 'Literal' && typeof node.value === 'string';
|
|
10
|
+
|
|
11
|
+
// A node whose value is always a string: a string literal or any template literal.
|
|
12
|
+
export const isStringExpression = node => isStringLiteral(node) || node?.type === 'TemplateLiteral';
|
|
13
|
+
|
|
14
|
+
export const isBooleanLiteral = (node, value) =>
|
|
15
|
+
node?.type === 'Literal'
|
|
16
|
+
&& typeof node.value === 'boolean'
|
|
17
|
+
&& (value === undefined || node.value === value);
|
|
18
|
+
|
|
19
|
+
export const getStaticStringValue = node => {
|
|
20
|
+
if (isStringLiteral(node)) {
|
|
21
|
+
return node.value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
node?.type === 'TemplateLiteral'
|
|
26
|
+
&& node.expressions.length === 0
|
|
27
|
+
) {
|
|
28
|
+
return node.quasis[0].value.cooked;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const isRegexLiteral = node => node.type === 'Literal' && Boolean(node.regex);
|
|
@@ -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,86 @@
|
|
|
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 inDescribe = tracker.depth > 0;
|
|
29
|
+
const expected = inDescribe ? 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: inDescribe ? 'inside a `describe`' : 'at the top level',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (parsed.kind === 'suite') {
|
|
44
|
+
tracker.enterSuite(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return problem;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
context.onExit('CallExpression', node => {
|
|
51
|
+
tracker.exitSuite(node);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
56
|
+
const config = {
|
|
57
|
+
create,
|
|
58
|
+
meta: {
|
|
59
|
+
type: 'suggestion',
|
|
60
|
+
docs: {
|
|
61
|
+
description: 'Enforce consistent use of `test` or `it`.',
|
|
62
|
+
recommended: false,
|
|
63
|
+
},
|
|
64
|
+
schema: [
|
|
65
|
+
{
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
fn: {
|
|
69
|
+
enum: ['test', 'it'],
|
|
70
|
+
description: 'The name to use for top-level test cases.',
|
|
71
|
+
},
|
|
72
|
+
withinDescribe: {
|
|
73
|
+
enum: ['test', 'it'],
|
|
74
|
+
description: 'The name to use for test cases inside a `describe`.',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
defaultOptions: [{fn: 'test', withinDescribe: 'it'}],
|
|
81
|
+
messages,
|
|
82
|
+
languages: ['js/js'],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
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, '');
|