@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,495 @@
|
|
|
1
|
+
import {findVariable} from '@eslint-community/eslint-utils';
|
|
2
|
+
import isFunction from './ast/is-function.js';
|
|
3
|
+
import {
|
|
4
|
+
resolveImports,
|
|
5
|
+
parseTestCall,
|
|
6
|
+
getSubtestReceiver,
|
|
7
|
+
getTestCallback,
|
|
8
|
+
MODIFIERS,
|
|
9
|
+
} from './utils/node-test.js';
|
|
10
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_ID = 'no-process-env-mutation';
|
|
13
|
+
|
|
14
|
+
const messages = {
|
|
15
|
+
[MESSAGE_ID]: 'Do not mutate `process.env` inside a test. Use a hook that restores the original value.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PROCESS_MODULES = new Set(['node:process', 'process']);
|
|
19
|
+
|
|
20
|
+
const OBJECT_MUTATORS = new Set([
|
|
21
|
+
'assign',
|
|
22
|
+
'defineProperties',
|
|
23
|
+
'defineProperty',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const REFLECT_MUTATORS = new Set([
|
|
27
|
+
'deleteProperty',
|
|
28
|
+
'defineProperty',
|
|
29
|
+
'set',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const unwrapExpression = node => {
|
|
33
|
+
let unwrapped = node && unwrapTypeScriptExpression(node);
|
|
34
|
+
while (unwrapped?.type === 'ChainExpression') {
|
|
35
|
+
unwrapped = unwrapTypeScriptExpression(unwrapped.expression);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return unwrapped;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getStaticPropertyName = node => {
|
|
42
|
+
if (node.type === 'Identifier') {
|
|
43
|
+
return node.name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return getStaticExpressionPropertyName(node);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getStaticExpressionPropertyName = node => {
|
|
50
|
+
if (node.type === 'Literal' && (typeof node.value === 'string' || typeof node.value === 'number')) {
|
|
51
|
+
return String(node.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
55
|
+
return node.quasis[0].value.cooked;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getMemberPropertyName = node => {
|
|
60
|
+
if (node.type !== 'MemberExpression') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!node.computed && node.property.type === 'Identifier') {
|
|
65
|
+
return node.property.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.computed) {
|
|
69
|
+
return getStaticExpressionPropertyName(unwrapExpression(node.property));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const getImportSpecifierName = specifier => {
|
|
74
|
+
if (specifier.imported.type === 'Identifier') {
|
|
75
|
+
return specifier.imported.name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof specifier.imported.value === 'string') {
|
|
79
|
+
return specifier.imported.value;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isUnshadowedGlobal = (context, node, name) => {
|
|
84
|
+
if (node.type !== 'Identifier' || node.name !== name) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const variable = findVariable(context.sourceCode.getScope(node), node);
|
|
89
|
+
return !variable || variable.defs.length === 0;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const isImportBinding = (context, node, names) => {
|
|
93
|
+
if (node.type !== 'Identifier' || !names.has(node.name)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const variable = findVariable(context.sourceCode.getScope(node), node);
|
|
98
|
+
return variable?.defs.some(definition => definition.type === 'ImportBinding') ?? false;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getRootIdentifier = node => {
|
|
102
|
+
node = unwrapExpression(node);
|
|
103
|
+
if (node?.type === 'Identifier') {
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (node?.type === 'MemberExpression') {
|
|
108
|
+
return getRootIdentifier(node.object);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getNearestFunction = node => {
|
|
113
|
+
let {parent} = node;
|
|
114
|
+
while (parent && !isFunction(parent)) {
|
|
115
|
+
parent = parent.parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parent;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const collectProcessModuleBindings = context => {
|
|
122
|
+
const processNames = new Set();
|
|
123
|
+
const environmentNames = new Set();
|
|
124
|
+
|
|
125
|
+
for (const node of context.sourceCode.ast.body) {
|
|
126
|
+
if (node.type !== 'ImportDeclaration' || !PROCESS_MODULES.has(node.source.value)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const specifier of node.specifiers) {
|
|
131
|
+
if (specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier') {
|
|
132
|
+
processNames.add(specifier.local.name);
|
|
133
|
+
} else if (specifier.type === 'ImportSpecifier') {
|
|
134
|
+
const importedName = getImportSpecifierName(specifier);
|
|
135
|
+
if (importedName === 'default') {
|
|
136
|
+
processNames.add(specifier.local.name);
|
|
137
|
+
} else if (importedName === 'env') {
|
|
138
|
+
environmentNames.add(specifier.local.name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {processNames, environmentNames};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
148
|
+
const create = context => {
|
|
149
|
+
const imports = resolveImports(context);
|
|
150
|
+
if (!imports.isTestFile) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const {processNames, environmentNames} = collectProcessModuleBindings(context);
|
|
155
|
+
const {sourceCode} = context;
|
|
156
|
+
const testStack = [];
|
|
157
|
+
const trackedCalls = new Set();
|
|
158
|
+
|
|
159
|
+
const isProcessObject = node => {
|
|
160
|
+
node = unwrapExpression(node);
|
|
161
|
+
return node?.type === 'Identifier'
|
|
162
|
+
&& (
|
|
163
|
+
isImportBinding(context, node, processNames)
|
|
164
|
+
|| (node.name === 'process' && isUnshadowedGlobal(context, node, 'process'))
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const isEnvironmentDestructuringProperty = (property, localName) => {
|
|
169
|
+
if (property.type !== 'Property' || property.computed || getStaticPropertyName(property.key) !== 'env') {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const {value} = property;
|
|
174
|
+
if (value.type === 'Identifier') {
|
|
175
|
+
return value.name === localName;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return value.type === 'AssignmentPattern'
|
|
179
|
+
&& value.left.type === 'Identifier'
|
|
180
|
+
&& value.left.name === localName;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const isEnvironmentAlias = (node, seenVariables) => {
|
|
184
|
+
if (node.type !== 'Identifier') {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const variable = findVariable(sourceCode.getScope(node), node);
|
|
189
|
+
if (!variable || seenVariables.has(variable)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
seenVariables.add(variable);
|
|
194
|
+
|
|
195
|
+
const definition = variable.defs.length === 1 ? variable.defs[0] : undefined;
|
|
196
|
+
const declarator = definition?.type === 'Variable' ? definition.node : undefined;
|
|
197
|
+
if (!declarator?.init || declarator.parent?.kind !== 'const') {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (declarator.id.type === 'Identifier') {
|
|
202
|
+
return isEnvironmentObject(declarator.init, seenVariables);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return declarator.id.type === 'ObjectPattern'
|
|
206
|
+
&& isProcessObject(declarator.init)
|
|
207
|
+
&& declarator.id.properties.some(property => isEnvironmentDestructuringProperty(property, node.name));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const isEnvironmentObject = (node, seenVariables = new Set()) => {
|
|
211
|
+
node = unwrapExpression(node);
|
|
212
|
+
if (!node) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (node.type === 'Identifier') {
|
|
217
|
+
return isImportBinding(context, node, environmentNames) || isEnvironmentAlias(node, seenVariables);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return node.type === 'MemberExpression'
|
|
221
|
+
&& getMemberPropertyName(node) === 'env'
|
|
222
|
+
&& isProcessObject(node.object);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const isEnvironmentMember = node => {
|
|
226
|
+
node = unwrapExpression(node);
|
|
227
|
+
return node?.type === 'MemberExpression' && isEnvironmentObject(node.object);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getEnvironmentAssignmentTarget = node => {
|
|
231
|
+
node = unwrapExpression(node);
|
|
232
|
+
if (!node) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (node.type === 'MemberExpression' && (isEnvironmentObject(node) || isEnvironmentMember(node))) {
|
|
237
|
+
return node;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (node.type === 'AssignmentPattern') {
|
|
241
|
+
return getEnvironmentAssignmentTarget(node.left);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (node.type === 'RestElement') {
|
|
245
|
+
return getEnvironmentAssignmentTarget(node.argument);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (node.type === 'ArrayPattern') {
|
|
249
|
+
for (const element of node.elements) {
|
|
250
|
+
const target = getEnvironmentAssignmentTarget(element);
|
|
251
|
+
if (target) {
|
|
252
|
+
return target;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (node.type === 'ObjectPattern') {
|
|
260
|
+
for (const property of node.properties) {
|
|
261
|
+
const target = getEnvironmentAssignmentTarget(property.type === 'Property' ? property.value : property.argument);
|
|
262
|
+
if (target) {
|
|
263
|
+
return target;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const getContextVariable = callback => {
|
|
270
|
+
const [parameter] = callback.params;
|
|
271
|
+
if (parameter?.type !== 'Identifier') {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return findVariable(sourceCode.getScope(parameter), parameter);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const isTestImportCall = node => {
|
|
279
|
+
const parsed = parseTestCall(node, imports);
|
|
280
|
+
if (
|
|
281
|
+
parsed?.kind !== 'test'
|
|
282
|
+
|| parsed.modifiers.some(modifier => !MODIFIERS.has(modifier.name))
|
|
283
|
+
) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const root = getRootIdentifier(node.callee);
|
|
288
|
+
return root?.type === 'Identifier'
|
|
289
|
+
&& (
|
|
290
|
+
isImportBinding(context, root, imports.locals)
|
|
291
|
+
|| (root.name === imports.namespace && isImportBinding(context, root, new Set([imports.namespace])))
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const isSubtestCall = node => {
|
|
296
|
+
const receiver = getSubtestReceiver(node);
|
|
297
|
+
if (!receiver) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const variable = findVariable(sourceCode.getScope(receiver), receiver);
|
|
302
|
+
return testStack.some(test => test.contextVariable && test.contextVariable === variable);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const enterTestCall = node => {
|
|
306
|
+
if (!isTestImportCall(node) && !isSubtestCall(node)) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const callback = getTestCallback(node);
|
|
311
|
+
if (!callback) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
testStack.push({
|
|
316
|
+
callback,
|
|
317
|
+
contextVariable: getContextVariable(callback),
|
|
318
|
+
});
|
|
319
|
+
trackedCalls.add(node);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const leaveTestCall = node => {
|
|
323
|
+
if (!trackedCalls.has(node)) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
trackedCalls.delete(node);
|
|
328
|
+
testStack.pop();
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const isInsideTestCallback = node => {
|
|
332
|
+
const test = testStack.at(-1);
|
|
333
|
+
if (!test) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return getNearestFunction(node) === test.callback;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const getMutatingProcessEnvironmentTarget = node => {
|
|
341
|
+
if (!isInsideTestCallback(node)) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (node.type === 'AssignmentExpression') {
|
|
346
|
+
return getEnvironmentAssignmentTarget(node.left);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (node.type === 'UpdateExpression') {
|
|
350
|
+
if (isEnvironmentMember(node.argument)) {
|
|
351
|
+
return unwrapExpression(node.argument);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
node.type === 'UnaryExpression'
|
|
359
|
+
&& node.operator === 'delete'
|
|
360
|
+
&& (isEnvironmentObject(node.argument) || isEnvironmentMember(node.argument))
|
|
361
|
+
) {
|
|
362
|
+
return unwrapExpression(node.argument);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const getMutatingCallTarget = node => {
|
|
367
|
+
if (!isInsideTestCallback(node)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const callee = unwrapExpression(node.callee);
|
|
372
|
+
if (
|
|
373
|
+
callee?.type !== 'MemberExpression'
|
|
374
|
+
|| node.arguments.length === 0
|
|
375
|
+
|| !isEnvironmentObject(node.arguments[0])
|
|
376
|
+
) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const method = getMemberPropertyName(callee);
|
|
381
|
+
const object = unwrapExpression(callee.object);
|
|
382
|
+
if (
|
|
383
|
+
method
|
|
384
|
+
&& (
|
|
385
|
+
(OBJECT_MUTATORS.has(method) && isUnshadowedGlobal(context, object, 'Object'))
|
|
386
|
+
|| (REFLECT_MUTATORS.has(method) && isUnshadowedGlobal(context, object, 'Reflect'))
|
|
387
|
+
)
|
|
388
|
+
) {
|
|
389
|
+
return unwrapExpression(node.arguments[0]);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const getMutatingLoopTarget = node => {
|
|
394
|
+
if (!isInsideTestCallback(node)) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return getEnvironmentAssignmentTarget(node.left);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
context.on('CallExpression', node => {
|
|
402
|
+
enterTestCall(node);
|
|
403
|
+
|
|
404
|
+
const target = getMutatingCallTarget(node);
|
|
405
|
+
if (!target) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
node: target,
|
|
411
|
+
messageId: MESSAGE_ID,
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
context.onExit('CallExpression', node => {
|
|
416
|
+
leaveTestCall(node);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
context.on('AssignmentExpression', node => {
|
|
420
|
+
const target = getMutatingProcessEnvironmentTarget(node);
|
|
421
|
+
if (!target) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
node: target,
|
|
427
|
+
messageId: MESSAGE_ID,
|
|
428
|
+
};
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
context.on('UpdateExpression', node => {
|
|
432
|
+
const target = getMutatingProcessEnvironmentTarget(node);
|
|
433
|
+
if (!target) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
node: target,
|
|
439
|
+
messageId: MESSAGE_ID,
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
context.on('UnaryExpression', node => {
|
|
444
|
+
const target = getMutatingProcessEnvironmentTarget(node);
|
|
445
|
+
if (!target) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
node: target,
|
|
451
|
+
messageId: MESSAGE_ID,
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
context.on('ForInStatement', node => {
|
|
456
|
+
const target = getMutatingLoopTarget(node);
|
|
457
|
+
if (!target) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
node: target,
|
|
463
|
+
messageId: MESSAGE_ID,
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
context.on('ForOfStatement', node => {
|
|
468
|
+
const target = getMutatingLoopTarget(node);
|
|
469
|
+
if (!target) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
node: target,
|
|
475
|
+
messageId: MESSAGE_ID,
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
481
|
+
const config = {
|
|
482
|
+
create,
|
|
483
|
+
meta: {
|
|
484
|
+
type: 'problem',
|
|
485
|
+
docs: {
|
|
486
|
+
description: 'Disallow mutating `process.env` inside tests.',
|
|
487
|
+
recommended: true,
|
|
488
|
+
},
|
|
489
|
+
schema: [],
|
|
490
|
+
messages,
|
|
491
|
+
languages: ['js/js'],
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
export default config;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {resolveImports} from './utils/node-test.js';
|
|
2
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID_PROCESS_EXIT = 'processExit';
|
|
5
|
+
const MESSAGE_ID_PROCESS_EXIT_CODE = 'processExitCode';
|
|
6
|
+
|
|
7
|
+
const messages = {
|
|
8
|
+
[MESSAGE_ID_PROCESS_EXIT]: 'Do not call `process.exit()` in a test file. Throw an error or use an assertion instead.',
|
|
9
|
+
[MESSAGE_ID_PROCESS_EXIT_CODE]: 'Do not set `process.exitCode` in a test file. Throw an error or use an assertion instead.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const unwrapExpression = node => {
|
|
13
|
+
let unwrapped = node && unwrapTypeScriptExpression(node);
|
|
14
|
+
while (unwrapped?.type === 'ChainExpression') {
|
|
15
|
+
unwrapped = unwrapTypeScriptExpression(unwrapped.expression);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return unwrapped;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const getProcessProperty = (node, propertyName) => {
|
|
22
|
+
const unwrapped = unwrapExpression(node);
|
|
23
|
+
if (
|
|
24
|
+
unwrapped?.type !== 'MemberExpression'
|
|
25
|
+
|| unwrapped.computed
|
|
26
|
+
|| unwrapped.property.type !== 'Identifier'
|
|
27
|
+
|| unwrapped.property.name !== propertyName
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const object = unwrapExpression(unwrapped.object);
|
|
33
|
+
if (object?.type === 'Identifier' && object.name === 'process') {
|
|
34
|
+
return unwrapped;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
39
|
+
const create = context => {
|
|
40
|
+
const imports = resolveImports(context);
|
|
41
|
+
if (!imports.isTestFile) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
context.on('CallExpression', node => {
|
|
46
|
+
if (!getProcessProperty(node.callee, 'exit')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
node,
|
|
52
|
+
messageId: MESSAGE_ID_PROCESS_EXIT,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
context.on('AssignmentExpression', node => {
|
|
57
|
+
const exitCode = getProcessProperty(node.left, 'exitCode');
|
|
58
|
+
if (!exitCode) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
node: exitCode,
|
|
64
|
+
messageId: MESSAGE_ID_PROCESS_EXIT_CODE,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
context.on('UpdateExpression', node => {
|
|
69
|
+
const exitCode = getProcessProperty(node.argument, 'exitCode');
|
|
70
|
+
if (!exitCode) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
node: exitCode,
|
|
76
|
+
messageId: MESSAGE_ID_PROCESS_EXIT_CODE,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
82
|
+
const config = {
|
|
83
|
+
create,
|
|
84
|
+
meta: {
|
|
85
|
+
type: 'problem',
|
|
86
|
+
docs: {
|
|
87
|
+
description: 'Disallow process exit control in test files.',
|
|
88
|
+
recommended: 'unopinionated',
|
|
89
|
+
},
|
|
90
|
+
schema: [],
|
|
91
|
+
messages,
|
|
92
|
+
languages: ['js/js'],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default config;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import createTestModifierRule from './shared/test-modifier-rule.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
4
|
+
const config = createTestModifierRule({
|
|
5
|
+
modifier: 'skip',
|
|
6
|
+
description: 'Disallow the `.skip` test modifier.',
|
|
7
|
+
errorMessage: 'Do not skip tests.',
|
|
8
|
+
recommended: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default config;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveImports,
|
|
3
|
+
parseTestCall,
|
|
4
|
+
createContextTracker,
|
|
5
|
+
getTestOptions,
|
|
6
|
+
findOptionsProperty,
|
|
7
|
+
} from './utils/node-test.js';
|
|
8
|
+
import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
|
|
9
|
+
|
|
10
|
+
const MESSAGE_ID_OPTION = 'no-skip-without-reason/option';
|
|
11
|
+
const MESSAGE_ID_CALL = 'no-skip-without-reason/call';
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
[MESSAGE_ID_OPTION]: 'Give `{{modifier}}` a reason string instead of `true` explaining why.',
|
|
15
|
+
[MESSAGE_ID_CALL]: 'Pass a reason message to `{{context}}.{{modifier}}()`.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const REASON_MODIFIERS = new Set(['skip', 'todo']);
|
|
19
|
+
|
|
20
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
21
|
+
const create = context => {
|
|
22
|
+
const imports = resolveImports(context);
|
|
23
|
+
if (!imports.isTestFile) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tracker = createContextTracker(imports);
|
|
28
|
+
|
|
29
|
+
context.on('CallExpression', node => {
|
|
30
|
+
const problems = [];
|
|
31
|
+
|
|
32
|
+
// Options form: `{skip: true}` / `{todo: true}` on a test/suite/hook.
|
|
33
|
+
if (parseTestCall(node, imports)) {
|
|
34
|
+
const options = getTestOptions(node);
|
|
35
|
+
for (const modifier of REASON_MODIFIERS) {
|
|
36
|
+
const property = findOptionsProperty(options, modifier);
|
|
37
|
+
const value = property && unwrapTypeScriptExpression(property.value);
|
|
38
|
+
if (value?.type === 'Literal' && value.value === true) {
|
|
39
|
+
problems.push({
|
|
40
|
+
node: property,
|
|
41
|
+
messageId: MESSAGE_ID_OPTION,
|
|
42
|
+
data: {modifier},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Context method form: `t.skip()` / `t.todo()` with no reason message.
|
|
49
|
+
const {callee} = node;
|
|
50
|
+
if (
|
|
51
|
+
node.arguments.length === 0
|
|
52
|
+
&& callee.type === 'MemberExpression'
|
|
53
|
+
&& !callee.computed
|
|
54
|
+
&& callee.property.type === 'Identifier'
|
|
55
|
+
&& REASON_MODIFIERS.has(callee.property.name)
|
|
56
|
+
&& callee.object.type === 'Identifier'
|
|
57
|
+
&& tracker.isContextIdentifier(callee.object)
|
|
58
|
+
) {
|
|
59
|
+
problems.push({
|
|
60
|
+
node,
|
|
61
|
+
messageId: MESSAGE_ID_CALL,
|
|
62
|
+
data: {context: callee.object.name, modifier: callee.property.name},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tracker.update(node);
|
|
67
|
+
return problems;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
context.onExit('CallExpression', node => {
|
|
71
|
+
tracker.leave(node);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
76
|
+
const config = {
|
|
77
|
+
create,
|
|
78
|
+
meta: {
|
|
79
|
+
type: 'suggestion',
|
|
80
|
+
docs: {
|
|
81
|
+
description: 'Require a reason when skipping or marking a test as todo.',
|
|
82
|
+
recommended: false,
|
|
83
|
+
},
|
|
84
|
+
schema: [],
|
|
85
|
+
messages,
|
|
86
|
+
languages: ['js/js'],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default config;
|