@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,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
parseAssertionCall,
|
|
5
|
+
getTestCallback,
|
|
6
|
+
createContextTracker,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID = 'no-duplicate-assertions';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID]: 'Duplicate adjacent assertion.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const STRICT_MODE_METHODS = new Map([
|
|
17
|
+
['equal', 'strictEqual'],
|
|
18
|
+
['notEqual', 'notStrictEqual'],
|
|
19
|
+
['deepEqual', 'deepStrictEqual'],
|
|
20
|
+
['notDeepEqual', 'notDeepStrictEqual'],
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function getAssertionMethod(assertion) {
|
|
24
|
+
if (assertion.isStrict && STRICT_MODE_METHODS.has(assertion.method)) {
|
|
25
|
+
return STRICT_MODE_METHODS.get(assertion.method);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return assertion.method;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getTestContextAssertionName(node) {
|
|
32
|
+
const {callee} = node;
|
|
33
|
+
if (
|
|
34
|
+
callee.type !== 'MemberExpression'
|
|
35
|
+
|| callee.computed
|
|
36
|
+
|| callee.object.type !== 'MemberExpression'
|
|
37
|
+
|| callee.object.computed
|
|
38
|
+
|| callee.object.object.type !== 'Identifier'
|
|
39
|
+
|| callee.object.property.type !== 'Identifier'
|
|
40
|
+
|| callee.object.property.name !== 'assert'
|
|
41
|
+
) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return callee.object.object.name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getAssertionKey(assertionExpression, context, imports, tracker) {
|
|
49
|
+
const {node, isAwaited} = assertionExpression;
|
|
50
|
+
const assertion = parseAssertionCall(node, imports);
|
|
51
|
+
const testContextName = getTestContextAssertionName(node);
|
|
52
|
+
if (
|
|
53
|
+
!assertion
|
|
54
|
+
|| (
|
|
55
|
+
testContextName !== undefined
|
|
56
|
+
&& !tracker.isContextName(testContextName)
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const argumentKeys = node.arguments.map(argument => {
|
|
63
|
+
if (argument.type === 'SpreadElement') {
|
|
64
|
+
return `...${context.sourceCode.getText(unwrapTypeScriptExpression(argument.argument))}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return context.sourceCode.getText(unwrapTypeScriptExpression(argument));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return JSON.stringify([isAwaited, getAssertionMethod(assertion), argumentKeys]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getAssertionExpression(statement) {
|
|
74
|
+
if (statement.type !== 'ExpressionStatement') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const {expression} = statement;
|
|
79
|
+
if (expression.type === 'CallExpression') {
|
|
80
|
+
return {
|
|
81
|
+
node: expression,
|
|
82
|
+
isAwaited: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
expression.type === 'AwaitExpression'
|
|
88
|
+
&& expression.argument.type === 'CallExpression'
|
|
89
|
+
) {
|
|
90
|
+
return {
|
|
91
|
+
node: expression.argument,
|
|
92
|
+
isAwaited: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
100
|
+
const create = context => {
|
|
101
|
+
const imports = resolveImports(context);
|
|
102
|
+
if (!imports.isAssertOrTestFile) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tracker = createContextTracker(imports);
|
|
107
|
+
const testCallbackBodies = new WeakSet();
|
|
108
|
+
|
|
109
|
+
context.on('CallExpression', node => {
|
|
110
|
+
const isTest = parseTestCall(node, imports)?.kind === 'test' || tracker.isSubtestCall(node);
|
|
111
|
+
tracker.update(node);
|
|
112
|
+
|
|
113
|
+
const callback = getTestCallback(node);
|
|
114
|
+
if (
|
|
115
|
+
!isTest
|
|
116
|
+
|| !callback
|
|
117
|
+
|| callback.body.type !== 'BlockStatement'
|
|
118
|
+
) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
testCallbackBodies.add(callback.body);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
context.onExit('BlockStatement', function * (node) {
|
|
126
|
+
if (!testCallbackBodies.has(node)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let previousAssertionKey;
|
|
131
|
+
for (const statement of node.body) {
|
|
132
|
+
const assertionExpression = getAssertionExpression(statement);
|
|
133
|
+
const assertionKey = assertionExpression
|
|
134
|
+
&& getAssertionKey(assertionExpression, context, imports, tracker);
|
|
135
|
+
|
|
136
|
+
if (!assertionKey) {
|
|
137
|
+
previousAssertionKey = undefined;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (assertionKey === previousAssertionKey) {
|
|
142
|
+
yield {
|
|
143
|
+
node: statement,
|
|
144
|
+
messageId: MESSAGE_ID,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
previousAssertionKey = assertionKey;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
context.onExit('CallExpression', node => {
|
|
153
|
+
tracker.leave(node);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
158
|
+
const config = {
|
|
159
|
+
create,
|
|
160
|
+
meta: {
|
|
161
|
+
type: 'suggestion',
|
|
162
|
+
docs: {
|
|
163
|
+
description: 'Disallow adjacent duplicate assertions.',
|
|
164
|
+
recommended: 'unopinionated',
|
|
165
|
+
},
|
|
166
|
+
schema: [],
|
|
167
|
+
messages,
|
|
168
|
+
languages: ['js/js'],
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default config;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {resolveImports, parseTestCall, getTestCallback} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-duplicate-hooks';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Duplicate `{{name}}` hook in the same scope.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
10
|
+
const create = context => {
|
|
11
|
+
const imports = resolveImports(context);
|
|
12
|
+
if (!imports.isTestFile) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Stack of scopes; each scope tracks the hook names already declared in it.
|
|
17
|
+
const scopeStack = [new Set()];
|
|
18
|
+
// Calls whose callback opened a scope, so we can pop on exit.
|
|
19
|
+
const pushedCalls = new Set();
|
|
20
|
+
|
|
21
|
+
context.on('CallExpression', node => {
|
|
22
|
+
const parsed = parseTestCall(node, imports);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let problem;
|
|
28
|
+
if (parsed.kind === 'hook') {
|
|
29
|
+
const scope = scopeStack.at(-1);
|
|
30
|
+
if (scope.has(parsed.name)) {
|
|
31
|
+
problem = {
|
|
32
|
+
node,
|
|
33
|
+
messageId: MESSAGE_ID,
|
|
34
|
+
data: {name: parsed.name},
|
|
35
|
+
};
|
|
36
|
+
} else {
|
|
37
|
+
scope.add(parsed.name);
|
|
38
|
+
}
|
|
39
|
+
} else if (parsed.kind === 'test' || parsed.kind === 'suite') {
|
|
40
|
+
const callback = getTestCallback(node);
|
|
41
|
+
if (callback) {
|
|
42
|
+
scopeStack.push(new Set());
|
|
43
|
+
pushedCalls.add(node);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return problem;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
context.onExit('CallExpression', node => {
|
|
51
|
+
if (!pushedCalls.has(node)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pushedCalls.delete(node);
|
|
56
|
+
scopeStack.pop();
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
61
|
+
const config = {
|
|
62
|
+
create,
|
|
63
|
+
meta: {
|
|
64
|
+
type: 'suggestion',
|
|
65
|
+
docs: {
|
|
66
|
+
description: 'Disallow duplicate hooks within the same scope.',
|
|
67
|
+
recommended: true,
|
|
68
|
+
},
|
|
69
|
+
schema: [],
|
|
70
|
+
messages,
|
|
71
|
+
languages: ['js/js'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default config;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {findVariable, getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
MODIFIERS,
|
|
4
|
+
resolveImports,
|
|
5
|
+
parseTestCall,
|
|
6
|
+
getTestCallback,
|
|
7
|
+
getSubtestReceiver,
|
|
8
|
+
getTestOptions,
|
|
9
|
+
findOptionsProperty,
|
|
10
|
+
} from './utils/node-test.js';
|
|
11
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
12
|
+
|
|
13
|
+
const MESSAGE_ID_DUPLICATE_CALL = 'no-duplicate-plan/duplicate-call';
|
|
14
|
+
const MESSAGE_ID_PLAN_OPTION = 'no-duplicate-plan/plan-option';
|
|
15
|
+
const MAX_PLAN_COUNT = 4_294_967_295;
|
|
16
|
+
|
|
17
|
+
const messages = {
|
|
18
|
+
[MESSAGE_ID_DUPLICATE_CALL]: 'Do not call `{{context}}.plan()` more than once in the same test.',
|
|
19
|
+
[MESSAGE_ID_PLAN_OPTION]: 'Do not call `{{context}}.plan()` when this test already has a `plan` option.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getPlanContextIdentifier(node) {
|
|
23
|
+
const {callee} = node;
|
|
24
|
+
if (
|
|
25
|
+
node.optional !== true
|
|
26
|
+
&& callee.type === 'MemberExpression'
|
|
27
|
+
&& !callee.computed
|
|
28
|
+
&& callee.optional !== true
|
|
29
|
+
&& callee.property.type === 'Identifier'
|
|
30
|
+
&& callee.property.name === 'plan'
|
|
31
|
+
) {
|
|
32
|
+
const object = unwrapTypeScriptExpression(callee.object);
|
|
33
|
+
return object.type === 'Identifier' ? object : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getIdentifierVariable(sourceCode, identifier) {
|
|
40
|
+
return findVariable(sourceCode.getScope(identifier), identifier);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTestCall(parsed) {
|
|
44
|
+
return parsed !== undefined && parsed.kind === 'test' && parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasSkipModifier(node) {
|
|
48
|
+
node = unwrapTypeScriptExpression(node);
|
|
49
|
+
|
|
50
|
+
while (node.type === 'MemberExpression') {
|
|
51
|
+
if (
|
|
52
|
+
!node.computed
|
|
53
|
+
&& node.property.type === 'Identifier'
|
|
54
|
+
&& node.property.name === 'skip'
|
|
55
|
+
) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
node = unwrapTypeScriptExpression(node.object);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasEnabledSkipOption(node, sourceCode) {
|
|
66
|
+
const property = findOptionsProperty(getTestOptions(node), 'skip');
|
|
67
|
+
if (property === undefined) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const staticValue = getStaticValue(property.value, sourceCode.getScope(property.value));
|
|
72
|
+
return staticValue !== null && Boolean(staticValue.value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isSkippedTestCall(node, sourceCode) {
|
|
76
|
+
return hasSkipModifier(node.callee) || hasEnabledSkipOption(node, sourceCode);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasEnabledPlanOption(node, sourceCode) {
|
|
80
|
+
const property = findOptionsProperty(getTestOptions(node), 'plan');
|
|
81
|
+
if (property === undefined) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const staticValue = getStaticValue(property.value, sourceCode.getScope(property.value));
|
|
86
|
+
return typeof staticValue?.value === 'number' && Number.isSafeInteger(staticValue.value) && staticValue.value > 0 && staticValue.value <= MAX_PLAN_COUNT;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isInsideSkippedCallback(node, skippedCallbacks) {
|
|
90
|
+
let current = node;
|
|
91
|
+
while (current) {
|
|
92
|
+
if (skippedCallbacks.has(current)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
current = current.parent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
103
|
+
const create = context => {
|
|
104
|
+
const imports = resolveImports(context);
|
|
105
|
+
if (!imports.isTestFile) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const {sourceCode} = context;
|
|
110
|
+
const frames = [];
|
|
111
|
+
const skippedCallbacks = new WeakSet();
|
|
112
|
+
|
|
113
|
+
const isSubtestCall = node => {
|
|
114
|
+
const receiver = getSubtestReceiver(node);
|
|
115
|
+
if (receiver === undefined) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const receiverVariable = getIdentifierVariable(sourceCode, receiver);
|
|
120
|
+
return receiverVariable !== undefined && frames.some(frame => frame.contextVariable === receiverVariable);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
context.on('CallExpression', node => {
|
|
124
|
+
const parsed = parseTestCall(node, imports);
|
|
125
|
+
const isImportedTestCall = isTestCall(parsed);
|
|
126
|
+
const isTest = isImportedTestCall || isSubtestCall(node);
|
|
127
|
+
|
|
128
|
+
if (isTest) {
|
|
129
|
+
if (isSkippedTestCall(node, sourceCode)) {
|
|
130
|
+
const callback = getTestCallback(node);
|
|
131
|
+
if (callback) {
|
|
132
|
+
skippedCallbacks.add(callback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const parameter = getTestCallback(node)?.params[0];
|
|
139
|
+
if (parameter?.type !== 'Identifier') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const hasPlanOption = hasEnabledPlanOption(node, sourceCode);
|
|
144
|
+
frames.push({
|
|
145
|
+
node,
|
|
146
|
+
contextName: parameter.name,
|
|
147
|
+
contextVariable: getIdentifierVariable(sourceCode, parameter),
|
|
148
|
+
hasPlan: hasPlanOption,
|
|
149
|
+
hasPlanOption,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const contextIdentifier = getPlanContextIdentifier(node);
|
|
155
|
+
if (contextIdentifier === undefined) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isInsideSkippedCallback(node, skippedCallbacks)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const contextVariable = getIdentifierVariable(sourceCode, contextIdentifier);
|
|
164
|
+
if (contextVariable === undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (let index = frames.length - 1; index >= 0; index -= 1) {
|
|
169
|
+
const frame = frames[index];
|
|
170
|
+
if (frame.contextVariable !== contextVariable) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (frame.hasPlan) {
|
|
175
|
+
return {
|
|
176
|
+
node,
|
|
177
|
+
messageId: frame.hasPlanOption ? MESSAGE_ID_PLAN_OPTION : MESSAGE_ID_DUPLICATE_CALL,
|
|
178
|
+
data: {context: frame.contextName},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
frame.hasPlan = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
context.onExit('CallExpression', node => {
|
|
188
|
+
if (frames.at(-1)?.node === node) {
|
|
189
|
+
frames.pop();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
195
|
+
const config = {
|
|
196
|
+
create,
|
|
197
|
+
meta: {
|
|
198
|
+
type: 'problem',
|
|
199
|
+
docs: {
|
|
200
|
+
description: 'Disallow setting a test plan more than once in the same test.',
|
|
201
|
+
recommended: 'unopinionated',
|
|
202
|
+
},
|
|
203
|
+
schema: [],
|
|
204
|
+
messages,
|
|
205
|
+
languages: ['js/js'],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default config;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {resolveImports} from './utils/node-test.js';
|
|
2
|
+
|
|
3
|
+
const MESSAGE_ID = 'no-export';
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Do not export from a test file.',
|
|
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 report = node => ({node, messageId: MESSAGE_ID});
|
|
17
|
+
|
|
18
|
+
context.on('ExportNamedDeclaration', node => {
|
|
19
|
+
// `export {}` exports nothing — it is only a marker to make a file a module. Leave it alone.
|
|
20
|
+
if (!node.declaration && !node.source && node.specifiers.length === 0) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return report(node);
|
|
25
|
+
});
|
|
26
|
+
context.on('ExportDefaultDeclaration', report);
|
|
27
|
+
context.on('ExportAllDeclaration', report);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
31
|
+
const config = {
|
|
32
|
+
create,
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'suggestion',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Disallow exports from test files.',
|
|
37
|
+
recommended: true,
|
|
38
|
+
},
|
|
39
|
+
schema: [],
|
|
40
|
+
messages,
|
|
41
|
+
languages: ['js/js'],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default config;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseAssertionCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
isAssertionCallWithSupportedContext,
|
|
6
|
+
} from './utils/node-test.js';
|
|
7
|
+
import {isSameReference} from './utils/index.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID_ALWAYS_PASSES = 'no-identical-assertion-arguments/always-passes';
|
|
10
|
+
const MESSAGE_ID_ALWAYS_FAILS = 'no-identical-assertion-arguments/always-fails';
|
|
11
|
+
|
|
12
|
+
const messages = {
|
|
13
|
+
[MESSAGE_ID_ALWAYS_PASSES]: 'Both arguments are the same, so this assertion always passes.',
|
|
14
|
+
[MESSAGE_ID_ALWAYS_FAILS]: 'Both arguments are the same, so this assertion always fails.',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Two-operand `node:assert` comparisons. The negated ones always fail on identical operands.
|
|
18
|
+
const POSITIVE_METHODS = new Set(['equal', 'strictEqual', 'deepEqual', 'deepStrictEqual']);
|
|
19
|
+
const NEGATED_METHODS = new Set(['notEqual', 'notStrictEqual', 'notDeepEqual', 'notDeepStrictEqual']);
|
|
20
|
+
|
|
21
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
22
|
+
const create = context => {
|
|
23
|
+
const imports = resolveImports(context);
|
|
24
|
+
// Activate on a `node:assert` import, or in a test file where `t.assert.*` may be used.
|
|
25
|
+
if (!imports.isAssertOrTestFile) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tracker = createContextTracker(imports, {trackHooks: true});
|
|
30
|
+
|
|
31
|
+
context.on('CallExpression', node => {
|
|
32
|
+
tracker.update(node);
|
|
33
|
+
|
|
34
|
+
const assertion = parseAssertionCall(node, imports);
|
|
35
|
+
if (!assertion || !isAssertionCallWithSupportedContext(node, tracker)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isNegated = NEGATED_METHODS.has(assertion.method);
|
|
40
|
+
if (!isNegated && !POSITIVE_METHODS.has(assertion.method)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const [first, second] = node.arguments;
|
|
45
|
+
if (
|
|
46
|
+
!first
|
|
47
|
+
|| !second
|
|
48
|
+
|| first.type === 'SpreadElement'
|
|
49
|
+
|| second.type === 'SpreadElement'
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!isSameReference(first, second)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
node,
|
|
60
|
+
messageId: isNegated ? MESSAGE_ID_ALWAYS_FAILS : MESSAGE_ID_ALWAYS_PASSES,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
context.onExit('CallExpression', node => {
|
|
65
|
+
tracker.leave(node);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
70
|
+
const config = {
|
|
71
|
+
create,
|
|
72
|
+
meta: {
|
|
73
|
+
type: 'problem',
|
|
74
|
+
docs: {
|
|
75
|
+
description: 'Disallow comparing a value to itself in an assertion.',
|
|
76
|
+
recommended: 'unopinionated',
|
|
77
|
+
},
|
|
78
|
+
schema: [],
|
|
79
|
+
messages,
|
|
80
|
+
languages: ['js/js'],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default config;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
getTestTitle,
|
|
5
|
+
getStaticString,
|
|
6
|
+
getTestCallback,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID = 'no-identical-title/duplicate';
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
[MESSAGE_ID]: 'Test title is already used by another test in the same scope.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
16
|
+
const create = context => {
|
|
17
|
+
const imports = resolveImports(context);
|
|
18
|
+
if (!imports.isTestFile) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Stack of title sets, one per scope level.
|
|
24
|
+
The bottom of the stack (index 0) is the module top-level.
|
|
25
|
+
Each describe/suite callback body pushes a new set on entry and pops it on exit.
|
|
26
|
+
*/
|
|
27
|
+
const scopeStack = [new Set()];
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
Map from a suite callback function node to the call expression that created it,
|
|
31
|
+
so we can push/pop the scope when entering/exiting the callback body.
|
|
32
|
+
*/
|
|
33
|
+
const suiteCallbackNodes = new WeakSet();
|
|
34
|
+
|
|
35
|
+
context.on('CallExpression', node => {
|
|
36
|
+
const parsed = parseTestCall(node, imports);
|
|
37
|
+
if (!parsed || parsed.kind === 'hook') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Track suite callbacks for scope push/pop.
|
|
42
|
+
if (parsed.kind === 'suite') {
|
|
43
|
+
const callback = getTestCallback(node);
|
|
44
|
+
if (callback) {
|
|
45
|
+
suiteCallbackNodes.add(callback);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const titleNode = getTestTitle(node, context);
|
|
50
|
+
if (!titleNode) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const titleValue = getStaticString(titleNode, context);
|
|
55
|
+
if (titleValue === undefined) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const currentScope = scopeStack.at(-1);
|
|
60
|
+
if (currentScope.has(titleValue)) {
|
|
61
|
+
return {
|
|
62
|
+
node: titleNode,
|
|
63
|
+
messageId: MESSAGE_ID,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
currentScope.add(titleValue);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Push/pop a scope around each suite callback body.
|
|
71
|
+
const functionTypes = ['FunctionExpression', 'ArrowFunctionExpression'];
|
|
72
|
+
|
|
73
|
+
context.on(functionTypes, node => {
|
|
74
|
+
if (suiteCallbackNodes.has(node)) {
|
|
75
|
+
scopeStack.push(new Set());
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
context.onExit(functionTypes, node => {
|
|
80
|
+
if (suiteCallbackNodes.has(node)) {
|
|
81
|
+
scopeStack.pop();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
87
|
+
const config = {
|
|
88
|
+
create,
|
|
89
|
+
meta: {
|
|
90
|
+
type: 'problem',
|
|
91
|
+
docs: {
|
|
92
|
+
description: 'Disallow identical test titles within the same scope.',
|
|
93
|
+
recommended: 'unopinionated',
|
|
94
|
+
},
|
|
95
|
+
schema: [],
|
|
96
|
+
messages,
|
|
97
|
+
languages: ['js/js'],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default config;
|