@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.
Files changed (128) hide show
  1. package/dist-cjser/index.cjs +8561 -0
  2. package/index.d.ts +11 -0
  3. package/index.js +49 -0
  4. package/license +9 -0
  5. package/package.json +141 -0
  6. package/readme.md +172 -0
  7. package/rules/assertion-arguments.js +147 -0
  8. package/rules/ast/call-or-new-expression.js +94 -0
  9. package/rules/ast/function-types.js +7 -0
  10. package/rules/ast/index.js +17 -0
  11. package/rules/ast/is-expression-statement.js +7 -0
  12. package/rules/ast/is-function.js +5 -0
  13. package/rules/ast/is-loop.js +5 -0
  14. package/rules/ast/is-member-expression.js +92 -0
  15. package/rules/ast/is-method-call.js +62 -0
  16. package/rules/ast/literal.js +32 -0
  17. package/rules/ast/loop-types.js +9 -0
  18. package/rules/consistent-assert-style.js +169 -0
  19. package/rules/consistent-assert-throws-callback-style.js +241 -0
  20. package/rules/consistent-modifier-style.js +95 -0
  21. package/rules/consistent-test-context-name.js +75 -0
  22. package/rules/consistent-test-filename.js +70 -0
  23. package/rules/consistent-test-it.js +84 -0
  24. package/rules/fix/index.js +5 -0
  25. package/rules/fix/remove-argument.js +58 -0
  26. package/rules/fix/replace-member-expression-property.js +25 -0
  27. package/rules/hooks-order.js +147 -0
  28. package/rules/index.js +84 -0
  29. package/rules/max-assertions.js +88 -0
  30. package/rules/max-nested-describe.js +70 -0
  31. package/rules/no-assert-in-describe.js +61 -0
  32. package/rules/no-assert-in-hook.js +61 -0
  33. package/rules/no-assert-match-string.js +166 -0
  34. package/rules/no-assert-throws-async.js +113 -0
  35. package/rules/no-assert-throws-call.js +106 -0
  36. package/rules/no-assert-throws-multiple-statements.js +194 -0
  37. package/rules/no-assert-throws-string.js +78 -0
  38. package/rules/no-async-describe.js +50 -0
  39. package/rules/no-async-fn-without-await.js +74 -0
  40. package/rules/no-callback-and-promise.js +56 -0
  41. package/rules/no-commented-tests.js +64 -0
  42. package/rules/no-compound-assertion.js +131 -0
  43. package/rules/no-conditional-assertion.js +108 -0
  44. package/rules/no-conditional-in-test.js +66 -0
  45. package/rules/no-conditional-tests.js +75 -0
  46. package/rules/no-conflicting-modifiers.js +73 -0
  47. package/rules/no-constant-assertion.js +218 -0
  48. package/rules/no-done-callback.js +58 -0
  49. package/rules/no-duplicate-assertions.js +172 -0
  50. package/rules/no-duplicate-hooks.js +75 -0
  51. package/rules/no-duplicate-plan.js +209 -0
  52. package/rules/no-export.js +45 -0
  53. package/rules/no-identical-assertion-arguments.js +84 -0
  54. package/rules/no-identical-title.js +101 -0
  55. package/rules/no-incorrect-deep-equal.js +113 -0
  56. package/rules/no-incorrect-strict-equal.js +99 -0
  57. package/rules/no-loop-static-title.js +93 -0
  58. package/rules/no-misused-concurrency.js +85 -0
  59. package/rules/no-mock-timers-destructured-import.js +150 -0
  60. package/rules/no-nested-tests.js +71 -0
  61. package/rules/no-only-test.js +11 -0
  62. package/rules/no-parent-test-context.js +244 -0
  63. package/rules/no-process-env-mutation.js +495 -0
  64. package/rules/no-process-exit-in-test.js +96 -0
  65. package/rules/no-skip-test.js +11 -0
  66. package/rules/no-skip-without-reason.js +90 -0
  67. package/rules/no-skip-without-return.js +123 -0
  68. package/rules/no-sleep-in-test.js +482 -0
  69. package/rules/no-snapshot-in-loop.js +132 -0
  70. package/rules/no-standalone-assert.js +51 -0
  71. package/rules/no-test-inside-hook.js +68 -0
  72. package/rules/no-test-return-statement.js +217 -0
  73. package/rules/no-todo-test.js +11 -0
  74. package/rules/no-unawaited-promise-assertion.js +366 -0
  75. package/rules/no-unawaited-rejects.js +87 -0
  76. package/rules/no-unawaited-subtest.js +66 -0
  77. package/rules/no-unknown-test-options.js +77 -0
  78. package/rules/no-useless-assertion.js +60 -0
  79. package/rules/prefer-assert-match.js +253 -0
  80. package/rules/prefer-assert-throws.js +97 -0
  81. package/rules/prefer-async-await.js +203 -0
  82. package/rules/prefer-context-mock.js +55 -0
  83. package/rules/prefer-diagnostic.js +94 -0
  84. package/rules/prefer-equality-assertion.js +124 -0
  85. package/rules/prefer-hooks-on-top.js +75 -0
  86. package/rules/prefer-lowercase-title.js +119 -0
  87. package/rules/prefer-mock-method.js +115 -0
  88. package/rules/prefer-strict-assert.js +82 -0
  89. package/rules/prefer-test-context-assert.js +162 -0
  90. package/rules/prefer-todo.js +98 -0
  91. package/rules/require-assertion.js +99 -0
  92. package/rules/require-await-concurrent-subtests.js +119 -0
  93. package/rules/require-context-assert-with-plan.js +127 -0
  94. package/rules/require-hook.js +108 -0
  95. package/rules/require-mock-timers-advance.js +313 -0
  96. package/rules/require-mock-timers-apis.js +383 -0
  97. package/rules/require-throws-expectation.js +65 -0
  98. package/rules/require-throws-validator-return-true.js +156 -0
  99. package/rules/require-top-level-describe.js +89 -0
  100. package/rules/rule/index.js +9 -0
  101. package/rules/rule/to-eslint-create.js +35 -0
  102. package/rules/rule/to-eslint-listener.js +52 -0
  103. package/rules/rule/to-eslint-problem.js +36 -0
  104. package/rules/rule/to-eslint-rule-fixer.js +47 -0
  105. package/rules/rule/to-eslint-rule.js +36 -0
  106. package/rules/rule/to-eslint-rules.js +8 -0
  107. package/rules/rule/unicorn-context.js +34 -0
  108. package/rules/rule/unicorn-listeners.js +56 -0
  109. package/rules/rule/utilities.js +26 -0
  110. package/rules/shared/test-modifier-rule.js +92 -0
  111. package/rules/test-title-format.js +86 -0
  112. package/rules/test-title.js +139 -0
  113. package/rules/utils/contains-suspension-point.js +35 -0
  114. package/rules/utils/get-comments.js +15 -0
  115. package/rules/utils/get-documentation-url.js +9 -0
  116. package/rules/utils/get-enclosing-function.js +18 -0
  117. package/rules/utils/index.js +15 -0
  118. package/rules/utils/is-conditional-branch.js +37 -0
  119. package/rules/utils/is-promise-type.js +26 -0
  120. package/rules/utils/is-same-reference.js +179 -0
  121. package/rules/utils/is-value-not-usable.js +5 -0
  122. package/rules/utils/node-test.js +883 -0
  123. package/rules/utils/parentheses/get-parent-syntax-opening-parenthesis.js +78 -0
  124. package/rules/utils/parentheses/iterate-surrounding-parentheses.js +80 -0
  125. package/rules/utils/parentheses/parentheses.js +69 -0
  126. package/rules/utils/types.js +5 -0
  127. package/rules/utils/unwrap-typescript-expression.js +16 -0
  128. package/rules/valid-describe-callback.js +63 -0
@@ -0,0 +1,366 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {
3
+ resolveImports,
4
+ parseTestCall,
5
+ getTestCallback,
6
+ parseAssertionCall,
7
+ getSubtestReceiver,
8
+ getCalleeChain,
9
+ MODIFIERS,
10
+ } from './utils/node-test.js';
11
+ import {isFunction} from './ast/index.js';
12
+ import {getEnclosingFunction, unwrapTypeScriptExpression, isTypeScriptExpressionWrapper} from './utils/index.js';
13
+
14
+ const MESSAGE_ID = 'no-unawaited-promise-assertion';
15
+
16
+ const PROMISE_METHODS = new Set(['then', 'catch', 'finally']);
17
+
18
+ const messages = {
19
+ [MESSAGE_ID]: 'Assertion in a floating `{{method}}()` callback is not awaited by the test. Await or return the Promise chain.',
20
+ };
21
+
22
+ function unwrapChainExpression(node) {
23
+ node = unwrapTypeScriptExpression(node);
24
+ return node?.type === 'ChainExpression' ? node.expression : node;
25
+ }
26
+
27
+ function getFloatingExpression(node) {
28
+ let expression = node;
29
+ let {parent} = node;
30
+
31
+ while (parent?.type === 'ChainExpression' || isTypeScriptExpressionWrapper(parent)) {
32
+ expression = parent;
33
+ parent = parent.parent;
34
+ }
35
+
36
+ if (parent?.type === 'UnaryExpression' && parent.operator === 'void') {
37
+ const unaryExpression = parent;
38
+ parent = parent.parent;
39
+
40
+ if (parent?.type === 'ExpressionStatement') {
41
+ return {
42
+ expression: unaryExpression.argument,
43
+ canFix: false,
44
+ };
45
+ }
46
+
47
+ return undefined;
48
+ }
49
+
50
+ if (parent?.type !== 'ExpressionStatement') {
51
+ return undefined;
52
+ }
53
+
54
+ return {
55
+ expression,
56
+ canFix: true,
57
+ };
58
+ }
59
+
60
+ function getPromiseChainCalls(node) {
61
+ const calls = [];
62
+ node = unwrapChainExpression(node);
63
+
64
+ while (node?.type === 'CallExpression') {
65
+ const callee = unwrapChainExpression(node.callee);
66
+ if (
67
+ callee?.type !== 'MemberExpression'
68
+ || callee.computed
69
+ || callee.property.type !== 'Identifier'
70
+ ) {
71
+ break;
72
+ }
73
+
74
+ if (!PROMISE_METHODS.has(callee.property.name)) {
75
+ if (
76
+ calls.length > 0
77
+ && unwrapChainExpression(callee.object)?.type === 'CallExpression'
78
+ ) {
79
+ return [];
80
+ }
81
+
82
+ return calls;
83
+ }
84
+
85
+ calls.push({
86
+ node,
87
+ method: callee.property.name,
88
+ });
89
+
90
+ node = unwrapChainExpression(callee.object);
91
+ }
92
+
93
+ return calls;
94
+ }
95
+
96
+ function getPromiseCallbackArguments(call) {
97
+ const {method} = call;
98
+ const argumentIndexes = method === 'then' ? [0, 1] : [0];
99
+
100
+ return argumentIndexes
101
+ .map(index => unwrapTypeScriptExpression(call.node.arguments[index]))
102
+ .filter(argument => argument && isFunction(argument));
103
+ }
104
+
105
+ function getContextParameter(callback) {
106
+ const parameter = callback.params[0];
107
+ return parameter?.type === 'Identifier' ? parameter : undefined;
108
+ }
109
+
110
+ function getContextAssertReceiver(node) {
111
+ const {callee} = node;
112
+ if (
113
+ callee.type === 'MemberExpression'
114
+ && callee.object.type === 'MemberExpression'
115
+ && !callee.object.computed
116
+ && callee.object.object.type === 'Identifier'
117
+ && callee.object.property.type === 'Identifier'
118
+ && callee.object.property.name === 'assert'
119
+ ) {
120
+ return callee.object.object;
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ function isTrackedContextAssertCall(node, contextParameters, sourceCode) {
127
+ const receiver = getContextAssertReceiver(node);
128
+ if (!receiver) {
129
+ return false;
130
+ }
131
+
132
+ const variable = findVariable(sourceCode.getScope(receiver), receiver);
133
+ return variable?.identifiers.some(identifier => contextParameters.includes(identifier)) === true;
134
+ }
135
+
136
+ function isTrackedSubtestCall(node, contextParameters, sourceCode) {
137
+ const receiver = getSubtestReceiver(node);
138
+ if (!receiver) {
139
+ return false;
140
+ }
141
+
142
+ const variable = findVariable(sourceCode.getScope(receiver), receiver);
143
+ return variable?.identifiers.some(identifier => contextParameters.includes(identifier)) === true;
144
+ }
145
+
146
+ function isImportBinding(identifier, sourceCode) {
147
+ const variable = findVariable(sourceCode.getScope(identifier), identifier);
148
+ return variable?.defs.some(definition => definition.type === 'ImportBinding') === true;
149
+ }
150
+
151
+ function isImportedTestCall(node, sourceCode) {
152
+ const root = getCalleeChain(node.callee)?.root;
153
+ return Boolean(root) && isImportBinding(root, sourceCode);
154
+ }
155
+
156
+ function hasOnlyTestModifiers(parsed) {
157
+ return parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name));
158
+ }
159
+
160
+ function getImportedAssertReference(node, imports) {
161
+ const {callee} = node;
162
+
163
+ if (
164
+ callee.type === 'Identifier'
165
+ && (
166
+ imports.assertNamed.has(callee.name)
167
+ || imports.assertNamespace.has(callee.name)
168
+ )
169
+ ) {
170
+ return callee;
171
+ }
172
+
173
+ if (
174
+ callee.type === 'MemberExpression'
175
+ && callee.object.type === 'Identifier'
176
+ && imports.assertNamespace.has(callee.object.name)
177
+ ) {
178
+ return callee.object;
179
+ }
180
+
181
+ return undefined;
182
+ }
183
+
184
+ function parseScopedAssertionCall(node, imports, contextParameters, sourceCode) {
185
+ const parsed = parseAssertionCall(node, imports);
186
+ if (!parsed) {
187
+ return undefined;
188
+ }
189
+
190
+ if (getContextAssertReceiver(node)) {
191
+ return isTrackedContextAssertCall(node, contextParameters, sourceCode) ? parsed : undefined;
192
+ }
193
+
194
+ const importedAssertReference = getImportedAssertReference(node, imports);
195
+ return importedAssertReference && isImportBinding(importedAssertReference, sourceCode) ? parsed : undefined;
196
+ }
197
+
198
+ function findAssertion(node, state) {
199
+ const {
200
+ imports,
201
+ contextParameters,
202
+ sourceCode,
203
+ visitorKeys,
204
+ } = state;
205
+
206
+ node = unwrapTypeScriptExpression(node);
207
+ if (!node) {
208
+ return undefined;
209
+ }
210
+
211
+ if (isFunction(node)) {
212
+ return undefined;
213
+ }
214
+
215
+ if (
216
+ node.type === 'CallExpression'
217
+ && parseScopedAssertionCall(node, imports, contextParameters, sourceCode)
218
+ ) {
219
+ return node;
220
+ }
221
+
222
+ for (const key of visitorKeys[node.type] ?? []) {
223
+ const child = node[key];
224
+ for (const childNode of Array.isArray(child) ? child : [child]) {
225
+ const assertion = childNode?.type && findAssertion(childNode, state);
226
+ if (assertion) {
227
+ return assertion;
228
+ }
229
+ }
230
+ }
231
+
232
+ return undefined;
233
+ }
234
+
235
+ function getAssertionProblems(chainCalls, state) {
236
+ return chainCalls.flatMap(call => getPromiseCallbackArguments(call).flatMap(callback => {
237
+ const assertion = findAssertion(callback.body, state);
238
+ if (!assertion) {
239
+ return [];
240
+ }
241
+
242
+ return [{
243
+ node: assertion,
244
+ messageId: MESSAGE_ID,
245
+ data: {
246
+ method: call.method,
247
+ },
248
+ }];
249
+ }));
250
+ }
251
+
252
+ function getTestBoundaryCallback(node, imports, contextParameters, sourceCode) {
253
+ const parsed = parseTestCall(node, imports);
254
+ if (parsed) {
255
+ if (parsed.kind !== 'test' && parsed.kind !== 'hook') {
256
+ return undefined;
257
+ }
258
+
259
+ if (!hasOnlyTestModifiers(parsed)) {
260
+ return undefined;
261
+ }
262
+
263
+ if (!isImportedTestCall(node, sourceCode)) {
264
+ return undefined;
265
+ }
266
+
267
+ return getTestCallback(node);
268
+ }
269
+
270
+ return isTrackedSubtestCall(node, contextParameters, sourceCode) ? getTestCallback(node) : undefined;
271
+ }
272
+
273
+ function hasStaticBlockBetween(node, boundary) {
274
+ for (let current = node; current && current !== boundary; current = current.parent) {
275
+ if (current.type === 'StaticBlock') {
276
+ return true;
277
+ }
278
+ }
279
+
280
+ return false;
281
+ }
282
+
283
+ /** @param {import('eslint').Rule.RuleContext} context */
284
+ const create = context => {
285
+ const imports = resolveImports(context);
286
+ if (!imports.isTestFile) {
287
+ return;
288
+ }
289
+
290
+ const {sourceCode} = context;
291
+ const {visitorKeys} = sourceCode;
292
+ const callbackStack = [];
293
+
294
+ context.on('CallExpression', node => {
295
+ const contextParameters = callbackStack.map(frame => frame.contextParameter).filter(Boolean);
296
+ const boundaryCallback = getTestBoundaryCallback(node, imports, contextParameters, sourceCode);
297
+
298
+ if (boundaryCallback) {
299
+ callbackStack.push({
300
+ node,
301
+ callback: boundaryCallback,
302
+ contextParameter: getContextParameter(boundaryCallback),
303
+ });
304
+ return;
305
+ }
306
+
307
+ const activeCallback = callbackStack.at(-1)?.callback;
308
+ if (!activeCallback || getEnclosingFunction(node) !== activeCallback) {
309
+ return;
310
+ }
311
+
312
+ const floatingExpression = getFloatingExpression(node);
313
+ if (!floatingExpression) {
314
+ return;
315
+ }
316
+
317
+ const chainCalls = getPromiseChainCalls(floatingExpression.expression);
318
+ if (chainCalls.length === 0) {
319
+ return;
320
+ }
321
+
322
+ const problems = getAssertionProblems(chainCalls, {
323
+ imports,
324
+ contextParameters,
325
+ sourceCode,
326
+ visitorKeys,
327
+ });
328
+ if (problems.length === 0) {
329
+ return;
330
+ }
331
+
332
+ if (
333
+ floatingExpression.canFix
334
+ && activeCallback.async
335
+ && !hasStaticBlockBetween(floatingExpression.expression, activeCallback)
336
+ ) {
337
+ problems[0].fix = fixer => fixer.insertTextBefore(floatingExpression.expression, 'await ');
338
+ }
339
+
340
+ return problems;
341
+ });
342
+
343
+ context.onExit('CallExpression', node => {
344
+ if (callbackStack.at(-1)?.node === node) {
345
+ callbackStack.pop();
346
+ }
347
+ });
348
+ };
349
+
350
+ /** @type {import('eslint').Rule.RuleModule} */
351
+ const config = {
352
+ create,
353
+ meta: {
354
+ type: 'problem',
355
+ docs: {
356
+ description: 'Disallow assertions inside unawaited Promise callbacks.',
357
+ recommended: 'unopinionated',
358
+ },
359
+ fixable: 'code',
360
+ schema: [],
361
+ messages,
362
+ languages: ['js/js'],
363
+ },
364
+ };
365
+
366
+ export default config;
@@ -0,0 +1,87 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ createContextTracker,
5
+ isAssertionCallWithSupportedContext,
6
+ } from './utils/node-test.js';
7
+ import {getEnclosingFunction} from './utils/index.js';
8
+
9
+ const MESSAGE_ID = 'no-unawaited-rejects/error';
10
+
11
+ const messages = {
12
+ [MESSAGE_ID]: '`assert.{{method}}()` must be awaited or returned.',
13
+ };
14
+
15
+ const REJECTS_METHODS = new Set(['rejects', 'doesNotReject']);
16
+
17
+ /** @param {import('eslint').Rule.RuleContext} context */
18
+ const create = context => {
19
+ const imports = resolveImports(context);
20
+
21
+ if (!imports.isAssertOrTestFile) {
22
+ return;
23
+ }
24
+
25
+ const tracker = createContextTracker(imports, {trackHooks: true});
26
+
27
+ context.on('CallExpression', node => {
28
+ tracker.update(node);
29
+
30
+ const parsed = parseAssertionCall(node, imports);
31
+ if (!parsed || !isAssertionCallWithSupportedContext(node, tracker)) {
32
+ return;
33
+ }
34
+
35
+ if (!REJECTS_METHODS.has(parsed.method)) {
36
+ return;
37
+ }
38
+
39
+ // The call must be an ExpressionStatement (i.e. bare call, not awaited/returned/assigned).
40
+ const {parent} = node;
41
+ if (!parent || parent.type !== 'ExpressionStatement') {
42
+ return;
43
+ }
44
+
45
+ const enclosingFunction = getEnclosingFunction(node);
46
+ const isInAsyncFunction = enclosingFunction?.async === true;
47
+
48
+ if (isInAsyncFunction) {
49
+ // Can autofix with `await`.
50
+ return {
51
+ node,
52
+ messageId: MESSAGE_ID,
53
+ data: {method: parsed.method},
54
+ fix: fixer => fixer.insertTextBefore(node, 'await '),
55
+ };
56
+ }
57
+
58
+ // Not in an async function — report without a fix (await would be a syntax error here).
59
+ return {
60
+ node,
61
+ messageId: MESSAGE_ID,
62
+ data: {method: parsed.method},
63
+ };
64
+ });
65
+
66
+ context.onExit('CallExpression', node => {
67
+ tracker.leave(node);
68
+ });
69
+ };
70
+
71
+ /** @type {import('eslint').Rule.RuleModule} */
72
+ const config = {
73
+ create,
74
+ meta: {
75
+ type: 'problem',
76
+ docs: {
77
+ description: 'Require `assert.rejects()`/`assert.doesNotReject()` to be awaited or returned.',
78
+ recommended: 'unopinionated',
79
+ },
80
+ fixable: 'code',
81
+ schema: [],
82
+ messages,
83
+ languages: ['js/js'],
84
+ },
85
+ };
86
+
87
+ export default config;
@@ -0,0 +1,66 @@
1
+ import {resolveImports, createContextTracker, getSubtestReceiver} from './utils/node-test.js';
2
+ import {getEnclosingFunction} from './utils/index.js';
3
+
4
+ const MESSAGE_ID = 'no-unawaited-subtest';
5
+
6
+ const messages = {
7
+ [MESSAGE_ID]: 'Subtest `{{name}}.test()` must be awaited or returned, otherwise it is cancelled when the parent test finishes.',
8
+ };
9
+
10
+ /** @param {import('eslint').Rule.RuleContext} context */
11
+ const create = context => {
12
+ const imports = resolveImports(context);
13
+ if (!imports.isTestFile) {
14
+ return;
15
+ }
16
+
17
+ const tracker = createContextTracker(imports);
18
+
19
+ context.on('CallExpression', node => {
20
+ // Whether this is a floating subtest must be decided against the current stack,
21
+ // before this call pushes its own context.
22
+ const subtest = tracker.isSubtestCall(node);
23
+
24
+ let problem;
25
+ if (subtest && node.parent.type === 'ExpressionStatement') {
26
+ const {name} = getSubtestReceiver(node);
27
+ const enclosingFunction = getEnclosingFunction(node);
28
+
29
+ problem = {
30
+ node,
31
+ messageId: MESSAGE_ID,
32
+ data: {name},
33
+ };
34
+
35
+ // `await` is only valid (and a behavior-preserving fix) inside an async function.
36
+ if (enclosingFunction?.async) {
37
+ problem.fix = fixer => fixer.insertTextBefore(node, 'await ');
38
+ }
39
+ }
40
+
41
+ tracker.update(node);
42
+ return problem;
43
+ });
44
+
45
+ context.onExit('CallExpression', node => {
46
+ tracker.leave(node);
47
+ });
48
+ };
49
+
50
+ /** @type {import('eslint').Rule.RuleModule} */
51
+ const config = {
52
+ create,
53
+ meta: {
54
+ type: 'problem',
55
+ docs: {
56
+ description: 'Require subtests created with the test context to be awaited or returned.',
57
+ recommended: true,
58
+ },
59
+ fixable: 'code',
60
+ schema: [],
61
+ messages,
62
+ languages: ['js/js'],
63
+ },
64
+ };
65
+
66
+ export default config;
@@ -0,0 +1,77 @@
1
+ import {resolveImports, parseTestCall, getTestOptions} from './utils/node-test.js';
2
+
3
+ const MESSAGE_ID = 'no-unknown-test-options';
4
+
5
+ const messages = {
6
+ [MESSAGE_ID]: '`{{name}}` is not a recognized {{kind}} option.',
7
+ };
8
+
9
+ /*
10
+ The option keys `node:test` recognizes. An unknown key is silently ignored, so a typo like
11
+ `{skp: true}` quietly runs the test as normal. This list tracks the runner and may need
12
+ updating as `node:test` gains options.
13
+ */
14
+ const TEST_OPTIONS = new Set(['concurrency', 'expectFailure', 'only', 'plan', 'signal', 'skip', 'tags', 'timeout', 'todo']);
15
+ const HOOK_OPTIONS = new Set(['signal', 'timeout']);
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
+ context.on('CallExpression', function * (node) {
25
+ const parsed = parseTestCall(node, imports);
26
+ if (!parsed) {
27
+ return;
28
+ }
29
+
30
+ const options = getTestOptions(node);
31
+ if (!options) {
32
+ return;
33
+ }
34
+
35
+ const known = parsed.kind === 'hook' ? HOOK_OPTIONS : TEST_OPTIONS;
36
+
37
+ for (const property of options.properties) {
38
+ if (property.type !== 'Property' || property.computed) {
39
+ continue;
40
+ }
41
+
42
+ let name;
43
+ if (property.key.type === 'Identifier') {
44
+ name = property.key.name;
45
+ } else if (property.key.type === 'Literal' && typeof property.key.value === 'string') {
46
+ name = property.key.value;
47
+ } else {
48
+ continue;
49
+ }
50
+
51
+ if (!known.has(name)) {
52
+ yield {
53
+ node: property.key,
54
+ messageId: MESSAGE_ID,
55
+ data: {name, kind: parsed.kind},
56
+ };
57
+ }
58
+ }
59
+ });
60
+ };
61
+
62
+ /** @type {import('eslint').Rule.RuleModule} */
63
+ const config = {
64
+ create,
65
+ meta: {
66
+ type: 'problem',
67
+ docs: {
68
+ description: 'Disallow unknown options in test and hook option objects.',
69
+ recommended: true,
70
+ },
71
+ schema: [],
72
+ messages,
73
+ languages: ['js/js'],
74
+ },
75
+ };
76
+
77
+ export default config;
@@ -0,0 +1,60 @@
1
+ import {
2
+ resolveImports,
3
+ parseAssertionCall,
4
+ createContextTracker,
5
+ isAssertionCallWithSupportedContext,
6
+ } from './utils/node-test.js';
7
+
8
+ const MESSAGE_ID = 'no-useless-assertion';
9
+
10
+ const messages = {
11
+ [MESSAGE_ID]: '`{{method}}()` is not useful. It catches an error only to rethrow it, so call the code directly instead.',
12
+ };
13
+
14
+ const USELESS_METHODS = new Set(['doesNotThrow', 'doesNotReject']);
15
+
16
+ /** @param {import('eslint').Rule.RuleContext} context */
17
+ const create = context => {
18
+ const imports = resolveImports(context);
19
+ if (!imports.isAssertOrTestFile) {
20
+ return;
21
+ }
22
+
23
+ const tracker = createContextTracker(imports, {trackHooks: true});
24
+
25
+ context.on('CallExpression', node => {
26
+ tracker.update(node);
27
+
28
+ const parsed = parseAssertionCall(node, imports);
29
+ if (!parsed || !USELESS_METHODS.has(parsed.method) || !isAssertionCallWithSupportedContext(node, tracker)) {
30
+ return;
31
+ }
32
+
33
+ return {
34
+ node,
35
+ messageId: MESSAGE_ID,
36
+ data: {method: parsed.method},
37
+ };
38
+ });
39
+
40
+ context.onExit('CallExpression', node => {
41
+ tracker.leave(node);
42
+ });
43
+ };
44
+
45
+ /** @type {import('eslint').Rule.RuleModule} */
46
+ const config = {
47
+ create,
48
+ meta: {
49
+ type: 'suggestion',
50
+ docs: {
51
+ description: 'Disallow `assert.doesNotThrow()` and `assert.doesNotReject()`.',
52
+ recommended: true,
53
+ },
54
+ schema: [],
55
+ messages,
56
+ languages: ['js/js'],
57
+ },
58
+ };
59
+
60
+ export default config;