@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,883 @@
1
+ import {findVariable, getStaticValue} from '@eslint-community/eslint-utils';
2
+ import isFunction from '../ast/is-function.js';
3
+ import unwrapTypeScriptExpression from './unwrap-typescript-expression.js';
4
+
5
+ /*
6
+ Detection helpers for Node.js's built-in test runner (`node:test`).
7
+
8
+ Unlike AVA, `node:test` is import-based. Rules first resolve the local names that
9
+ the file imported from `node:test` / `node:assert`, then match calls against them.
10
+
11
+ ```js
12
+ import test, {describe, it, before} from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import {strictEqual} from 'node:assert';
15
+ ```
16
+ */
17
+
18
+ /** Canonical test/suite/hook function names exported from `node:test`. */
19
+ const TEST_FUNCTIONS = new Set(['test', 'it']);
20
+ const SUITE_FUNCTIONS = new Set(['describe', 'suite']);
21
+ const HOOK_FUNCTIONS = new Set(['before', 'after', 'beforeEach', 'afterEach']);
22
+ const ALL_TEST_EXPORTS = new Set([...TEST_FUNCTIONS, ...SUITE_FUNCTIONS, ...HOOK_FUNCTIONS, 'mock']);
23
+
24
+ export {TEST_FUNCTIONS, SUITE_FUNCTIONS, HOOK_FUNCTIONS};
25
+
26
+ /** Modifier names usable as `test.only()`, `test.skip()`, `test.todo()`. */
27
+ const MODIFIERS = new Set(['only', 'skip', 'todo']);
28
+ export {MODIFIERS};
29
+
30
+ const TEST_MODULES = new Set(['node:test', 'test']);
31
+ const ASSERT_MODULES = new Set(['node:assert', 'node:assert/strict', 'assert', 'assert/strict']);
32
+
33
+ /**
34
+ Scan a file's top-level imports and resolve the local bindings for
35
+ `node:test` and `node:assert`.
36
+
37
+ @returns {{
38
+ locals: Map<string, string>,
39
+ namespace: string | undefined,
40
+ assertNamespace: Set<string>,
41
+ assertNamed: Map<string, string>,
42
+ strictAssertLocals: Set<string>,
43
+ mockLocals: Set<string>,
44
+ sourceCode: import('eslint').SourceCode,
45
+ isTestFile: boolean,
46
+ hasAssert: boolean,
47
+ isAssertOrTestFile: boolean,
48
+ }}
49
+ */
50
+ // Cache the result per AST so the many rules sharing this helper only scan the file once.
51
+ const importsCache = new WeakMap();
52
+
53
+ export function resolveImports(context) {
54
+ const {ast} = context.sourceCode;
55
+ const cached = importsCache.get(ast);
56
+ if (cached) {
57
+ return cached;
58
+ }
59
+
60
+ const result = scanImports(context);
61
+ importsCache.set(ast, result);
62
+ return result;
63
+ }
64
+
65
+ /** Classify an import source as the test module, the assert module, or neither. */
66
+ function moduleKind(source) {
67
+ if (TEST_MODULES.has(source)) {
68
+ return 'test';
69
+ }
70
+
71
+ if (ASSERT_MODULES.has(source)) {
72
+ return 'assert';
73
+ }
74
+
75
+ return undefined;
76
+ }
77
+
78
+ /**
79
+ Record an assert binding. A named import passes the canonical `importedName`; a whole-module
80
+ binding (`import assert from …`, `import * as assert …`) passes `undefined`. Strict-mode sources
81
+ also mark the local as already-strict.
82
+ */
83
+ function addAssertBinding(bindings, localName, importedName, isStrict) {
84
+ if (importedName === undefined) {
85
+ bindings.assertNamespace.add(localName);
86
+ } else {
87
+ bindings.assertNamed.set(localName, importedName);
88
+ }
89
+
90
+ if (isStrict) {
91
+ bindings.strictAssertLocals.add(localName);
92
+ }
93
+ }
94
+
95
+ /** Collect bindings from an ESM `import` declaration. */
96
+ function collectFromImport(node, bindings) {
97
+ const {value: source} = node.source;
98
+ const kind = moduleKind(source);
99
+ if (!kind) {
100
+ return;
101
+ }
102
+
103
+ const isStrict = source.endsWith('/strict');
104
+
105
+ for (const specifier of node.specifiers) {
106
+ const localName = specifier.local.name;
107
+
108
+ // Named import: `import {describe} from 'node:test'` / `import {strictEqual} from 'node:assert'`.
109
+ if (specifier.type === 'ImportSpecifier') {
110
+ if (specifier.imported.type !== 'Identifier') {
111
+ continue;
112
+ }
113
+
114
+ if (kind === 'assert') {
115
+ if (specifier.imported.name === 'strict') {
116
+ addAssertBinding(bindings, localName, undefined, true);
117
+ } else {
118
+ addAssertBinding(bindings, localName, specifier.imported.name, isStrict);
119
+ }
120
+ } else if (ALL_TEST_EXPORTS.has(specifier.imported.name)) {
121
+ bindings.locals.set(localName, specifier.imported.name);
122
+ }
123
+ } else if (kind === 'assert') {
124
+ // Default or namespace import of the whole assert module.
125
+ addAssertBinding(bindings, localName, undefined, isStrict);
126
+ } else if (specifier.type === 'ImportDefaultSpecifier') {
127
+ // `import test from 'node:test'` -> the default export is the callable `test` function, which
128
+ // also exposes the named exports as properties. Bind it as both the `test` local (bare
129
+ // `test(…)`, `test.only(…)`) and a namespace (`test.describe(…)`).
130
+ bindings.locals.set(localName, 'test');
131
+ bindings.namespace = localName;
132
+ } else {
133
+ // `import * as nodeTest from 'node:test'`.
134
+ bindings.namespace = localName;
135
+ }
136
+ }
137
+ }
138
+
139
+ function scanImports(context) {
140
+ const bindings = {
141
+ sourceCode: context.sourceCode,
142
+ // Map of local identifier name -> canonical `node:test` export name.
143
+ locals: new Map(),
144
+ // `import * as nodeTest from 'node:test'` -> namespace local name.
145
+ namespace: undefined,
146
+ // Local names bound to the whole `node:assert` module (`import assert from …`).
147
+ assertNamespace: new Set(),
148
+ // Map of local name -> canonical `node:assert` method name (named imports).
149
+ assertNamed: new Map(),
150
+ // Local names bound to a strict-mode assert module (`node:assert/strict`), where
151
+ // the legacy methods (`equal`/`deepEqual`/…) already behave as their strict counterparts.
152
+ strictAssertLocals: new Set(),
153
+ };
154
+
155
+ for (const node of context.sourceCode.ast.body) {
156
+ if (node.type === 'ImportDeclaration' && typeof node.source.value === 'string') {
157
+ collectFromImport(node, bindings);
158
+ }
159
+ }
160
+
161
+ const {locals, namespace, assertNamespace, assertNamed} = bindings;
162
+ // The file imports test/suite/hook bindings from `node:test`.
163
+ const isTestFile = locals.size > 0 || namespace !== undefined;
164
+ // The file imports anything from `node:assert`.
165
+ const hasAssert = assertNamespace.size > 0 || assertNamed.size > 0;
166
+ return {
167
+ ...bindings,
168
+ sourceCode: context.sourceCode,
169
+ // Local names bound to the `mock` export (`import {mock} from 'node:test'`, renamed too).
170
+ mockLocals: new Set([...locals].filter(([, canonical]) => canonical === 'mock').map(([local]) => local)),
171
+ isTestFile,
172
+ hasAssert,
173
+ // Assertion rules activate on either: a `node:assert` import, or a test file (where `t.assert.*`
174
+ // works without importing `node:assert`).
175
+ isAssertOrTestFile: hasAssert || isTestFile,
176
+ };
177
+ }
178
+
179
+ function getVariable(identifier, imports) {
180
+ return findVariable(imports.sourceCode.getScope(identifier), identifier);
181
+ }
182
+
183
+ function getDeclaredVariable(identifier, node, imports) {
184
+ return imports.sourceCode.getDeclaredVariables(node).find(variable => variable.identifiers.includes(identifier));
185
+ }
186
+
187
+ function isImportedBindingReference(identifier, imports) {
188
+ return getVariable(identifier, imports)?.defs.some(definition => definition.type === 'ImportBinding') ?? false;
189
+ }
190
+
191
+ /** Whether a node references the global `mock` — a named/renamed import, `namespace.mock`, or `test.mock`/`it.mock`. */
192
+ export function isGlobalMock(node, imports) {
193
+ return (
194
+ (
195
+ node.type === 'Identifier'
196
+ && imports.mockLocals.has(node.name)
197
+ && isImportedBindingReference(node, imports)
198
+ )
199
+ || (
200
+ node.type === 'MemberExpression'
201
+ && !node.computed
202
+ && node.property.type === 'Identifier'
203
+ && node.property.name === 'mock'
204
+ && node.object.type === 'Identifier'
205
+ && (
206
+ node.object.name === imports.namespace
207
+ || TEST_FUNCTIONS.has(imports.locals.get(node.object.name))
208
+ )
209
+ && isImportedBindingReference(node.object, imports)
210
+ )
211
+ );
212
+ }
213
+
214
+ function computeCalleeChain(node) {
215
+ const members = [];
216
+
217
+ while (node) {
218
+ node = unwrapTypeScriptExpression(node);
219
+
220
+ if (node.type === 'ChainExpression') {
221
+ node = node.expression;
222
+ continue;
223
+ }
224
+
225
+ if (node.type === 'Identifier') {
226
+ return {root: node, members};
227
+ }
228
+
229
+ if (
230
+ node.type === 'MemberExpression'
231
+ && !node.computed
232
+ && node.property.type === 'Identifier'
233
+ ) {
234
+ members.unshift(node.property);
235
+ node = node.object;
236
+ continue;
237
+ }
238
+
239
+ return undefined;
240
+ }
241
+
242
+ return undefined;
243
+ }
244
+
245
+ /*
246
+ Cache the chain per callee node. It is a pure function of the node, and both `parseTestCall` and `getSubtestReceiver` (used by ~30 rules) walk the same callee on every `CallExpression`, so the first walk is reused across all of them. The returned object is shared, so callers must treat it (and its `members` array) as read-only.
247
+ */
248
+ const calleeChainCache = new WeakMap();
249
+
250
+ // Sentinel stored for callees with no chain, so a cache hit is a single `get` even when the
251
+ // computed result is `undefined` (the common case for non-matching callees).
252
+ const NO_CHAIN = Symbol('no chain');
253
+
254
+ /**
255
+ Walk a callee chain into its root identifier and the member property nodes after it.
256
+
257
+ Unwraps TypeScript wrappers and optional chaining while walking.
258
+
259
+ @returns {{root: import('estree').Identifier, members: import('estree').Identifier[]} | undefined}
260
+ */
261
+ export function getCalleeChain(node) {
262
+ if (!node) {
263
+ return undefined;
264
+ }
265
+
266
+ const cached = calleeChainCache.get(node);
267
+ if (cached !== undefined) {
268
+ return cached === NO_CHAIN ? undefined : cached;
269
+ }
270
+
271
+ const result = computeCalleeChain(node);
272
+ calleeChainCache.set(node, result ?? NO_CHAIN);
273
+ return result;
274
+ }
275
+
276
+ /** Classify a canonical export name as a test, suite, or hook (or `undefined`). */
277
+ function getCallKind(name) {
278
+ if (TEST_FUNCTIONS.has(name)) {
279
+ return 'test';
280
+ }
281
+
282
+ if (SUITE_FUNCTIONS.has(name)) {
283
+ return 'suite';
284
+ }
285
+
286
+ if (HOOK_FUNCTIONS.has(name)) {
287
+ return 'hook';
288
+ }
289
+
290
+ return undefined;
291
+ }
292
+
293
+ /*
294
+ Memoize the `parse*Call` classifiers by node. The same `CallExpression` is parsed by many rules during one lint run (34 rules call `parseTestCall`, 21 call `parseAssertionCall`), and `imports` is stable per file (it is itself cached per AST), so the first parse can be reused across all of them. Keyed by node with an `imports` guard for safety.
295
+
296
+ The cached result object is shared between callers, so treat it as read-only — never mutate the returned `modifiers` array or reassign its fields.
297
+ */
298
+ const parseTestCallCache = new WeakMap();
299
+ const parseAssertionCallCache = new WeakMap();
300
+
301
+ const memoizeByNode = (cache, compute) => (callExpression, imports) => {
302
+ if (callExpression.type !== 'CallExpression') {
303
+ return undefined;
304
+ }
305
+
306
+ const cached = cache.get(callExpression);
307
+ if (cached && cached.imports === imports) {
308
+ return cached.result;
309
+ }
310
+
311
+ const result = compute(callExpression, imports);
312
+ cache.set(callExpression, {imports, result});
313
+ return result;
314
+ };
315
+
316
+ /**
317
+ Classify a `CallExpression` as a `node:test` test/suite/hook call.
318
+
319
+ @returns {{
320
+ name: string,
321
+ kind: 'test' | 'suite' | 'hook',
322
+ modifiers: import('estree').Identifier[],
323
+ } | undefined}
324
+ */
325
+ export const parseTestCall = memoizeByNode(parseTestCallCache, (callExpression, imports) => {
326
+ const chain = getCalleeChain(callExpression.callee);
327
+ if (!chain) {
328
+ return undefined;
329
+ }
330
+
331
+ const {root, members} = chain;
332
+ if (!isImportedBindingReference(root, imports)) {
333
+ return undefined;
334
+ }
335
+
336
+ let name;
337
+ let modifiers;
338
+
339
+ if (
340
+ imports.namespace
341
+ && root.name === imports.namespace
342
+ && members.length > 0
343
+ && ALL_TEST_EXPORTS.has(members[0].name)
344
+ ) {
345
+ // `nodeTest.test.only(…)` — namespace member access into a known export.
346
+ const [first, ...rest] = members;
347
+ name = first.name;
348
+ modifiers = rest;
349
+ } else if (imports.locals.has(root.name)) {
350
+ // `test.only(…)` / bare `test(…)` — a callable test binding. A binding that is both a local and
351
+ // the namespace (`import test from 'node:test'`) reaches here for member chains whose first
352
+ // segment is not a known export, e.g. `test.only(…)`.
353
+ name = imports.locals.get(root.name);
354
+ modifiers = members;
355
+ } else {
356
+ return undefined;
357
+ }
358
+
359
+ const kind = getCallKind(name);
360
+ if (!kind) {
361
+ return undefined;
362
+ }
363
+
364
+ return {name, kind, modifiers};
365
+ });
366
+
367
+ /** Get the modifier identifier node with the given name (`only`/`skip`/`todo`), or `undefined`. */
368
+ export const findModifier = (modifiers, name) => modifiers.find(modifier => modifier.name === name);
369
+
370
+ export function isHookMemberTestCall(parsed) {
371
+ return parsed?.kind === 'test'
372
+ && parsed.modifiers.length === 1
373
+ && HOOK_FUNCTIONS.has(parsed.modifiers[0].name);
374
+ }
375
+
376
+ /**
377
+ For a subtest-shaped call (`receiver.test(…)`, optionally with chained `.only`/`.skip`/`.todo` modifiers), return the receiver identifier node. Otherwise `undefined`.
378
+ */
379
+ export function getSubtestReceiver(callExpression) {
380
+ if (callExpression.type !== 'CallExpression') {
381
+ return undefined;
382
+ }
383
+
384
+ const chain = getCalleeChain(callExpression.callee);
385
+ if (
386
+ chain
387
+ && chain.members[0]?.name === 'test'
388
+ && chain.members.slice(1).every(member => MODIFIERS.has(member.name))
389
+ ) {
390
+ return chain.root;
391
+ }
392
+
393
+ return undefined;
394
+ }
395
+
396
+ /**
397
+ Track the test-context parameter names (`t`) introduced by enclosing test, subtest, and optionally hook callbacks.
398
+
399
+ Subtests (`t.test(…)`) are method calls, not imported bindings, so recognizing them requires knowing the enclosing context name. Drive the tracker from a `CallExpression` visitor: query `isSubtestCall`/`isContextName` first (against the current stack), then call `update(node)` to push this call's own context, and `leave(node)` on exit.
400
+
401
+ Set `trackHooks` to also track hook context parameters for rules that inspect `t.assert.*()`.
402
+
403
+ @returns {{
404
+ isSubtestCall: (node: import('estree').Node) => boolean,
405
+ isContextIdentifier: (node: import('estree').Node | undefined) => boolean,
406
+ isContextName: (name: string | undefined) => boolean,
407
+ current: () => string | undefined,
408
+ currentCallback: () => import('estree').Node | undefined,
409
+ update: (node: import('estree').Node) => void,
410
+ leave: (node: import('estree').Node) => void,
411
+ }}
412
+ */
413
+ export function createContextTracker(imports, {trackHooks = false} = {}) {
414
+ const names = [];
415
+ const variables = [];
416
+ const callbacks = [];
417
+ const pushedCalls = new Set();
418
+
419
+ const isContextIdentifier = node => {
420
+ if (node?.type !== 'Identifier') {
421
+ return false;
422
+ }
423
+
424
+ const variable = getVariable(node, imports);
425
+ return variable !== undefined && variables.includes(variable);
426
+ };
427
+
428
+ const isSubtestCall = node => {
429
+ const receiver = getSubtestReceiver(node);
430
+ return isContextIdentifier(receiver);
431
+ };
432
+
433
+ const isTrackedHookCall = parsed => trackHooks && (
434
+ (
435
+ parsed?.kind === 'hook'
436
+ && parsed.modifiers.length === 0
437
+ )
438
+ || isHookMemberTestCall(parsed)
439
+ );
440
+
441
+ const isTrackedCallbackCall = node => {
442
+ const parsed = parseTestCall(node, imports);
443
+ return (
444
+ (
445
+ parsed?.kind === 'test'
446
+ && parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name))
447
+ )
448
+ || isTrackedHookCall(parsed)
449
+ );
450
+ };
451
+
452
+ return {
453
+ isSubtestCall,
454
+ isContextIdentifier,
455
+ isContextName: name => name !== undefined && names.includes(name),
456
+ // The name of the innermost enclosing tracked context, or `undefined` when its
457
+ // callback declared no context parameter (or we are not inside a tracked callback).
458
+ current: () => names.at(-1),
459
+ // The callback function node of the innermost enclosing tracked callback. The context parameter is
460
+ // only in scope inside this node, so a node visited in the call's title/options arguments (which
461
+ // the traversal reaches before the callback) is not actually within the context's scope.
462
+ currentCallback: () => callbacks.at(-1),
463
+ update(node) {
464
+ if (!(isTrackedCallbackCall(node) || isSubtestCall(node))) {
465
+ return;
466
+ }
467
+
468
+ const parsed = parseTestCall(node, imports);
469
+ const callback = isTrackedHookCall(parsed) ? getHookCallback(node) : getTestCallback(node);
470
+ if (callback) {
471
+ const parameter = callback.params[0];
472
+
473
+ names.push(parameter?.type === 'Identifier' ? parameter.name : undefined);
474
+ variables.push(parameter?.type === 'Identifier' ? getDeclaredVariable(parameter, callback, imports) : undefined);
475
+ callbacks.push(callback);
476
+ pushedCalls.add(node);
477
+ }
478
+ },
479
+ leave(node) {
480
+ if (!pushedCalls.has(node)) {
481
+ return;
482
+ }
483
+
484
+ pushedCalls.delete(node);
485
+ names.pop();
486
+ variables.pop();
487
+ callbacks.pop();
488
+ },
489
+ };
490
+ }
491
+
492
+ function getContextAssertIdentifier(node) {
493
+ const {callee} = node;
494
+ if (
495
+ callee.type === 'MemberExpression'
496
+ && !callee.computed
497
+ && callee.object.type === 'MemberExpression'
498
+ && !callee.object.computed
499
+ && callee.object.object.type === 'Identifier'
500
+ && callee.object.property.type === 'Identifier'
501
+ && callee.object.property.name === 'assert'
502
+ ) {
503
+ return callee.object.object;
504
+ }
505
+
506
+ return undefined;
507
+ }
508
+
509
+ /**
510
+ Check whether a `t.assert.*()` call belongs to a tracked test context.
511
+ */
512
+ export function isAssertionCallWithSupportedContext(node, tracker) {
513
+ const contextAssertIdentifier = getContextAssertIdentifier(node);
514
+ return contextAssertIdentifier === undefined || tracker.isContextIdentifier(contextAssertIdentifier);
515
+ }
516
+
517
+ /**
518
+ Track the nesting depth of enclosing `describe`/`suite` blocks across a `CallExpression` visitor.
519
+
520
+ `depth` reflects the suites currently on the stack. Call `enterSuite(node)` once a call has been classified as a suite, and `exitSuite(node)` from the matching `CallExpression:exit` listener (it ignores nodes that were never entered, so it is safe to call for every exit). Reading `depth` before `enterSuite` gives the enclosing depth; reading it after includes the just-entered suite.
521
+
522
+ @returns {{depth: number, enterSuite: (node: import('estree').Node) => void, exitSuite: (node: import('estree').Node) => void}}
523
+ */
524
+ export function createSuiteDepthTracker() {
525
+ const suiteCalls = new Set();
526
+ let depth = 0;
527
+
528
+ return {
529
+ get depth() {
530
+ return depth;
531
+ },
532
+ enterSuite(node) {
533
+ depth += 1;
534
+ suiteCalls.add(node);
535
+ },
536
+ exitSuite(node) {
537
+ if (!suiteCalls.has(node)) {
538
+ return;
539
+ }
540
+
541
+ suiteCalls.delete(node);
542
+ depth -= 1;
543
+ },
544
+ };
545
+ }
546
+
547
+ /**
548
+ Get the title argument node of a test/suite call, if its first argument is a static string.
549
+ */
550
+ export function getTestTitle(callExpression, context) {
551
+ const {sourceCode} = context;
552
+ const first = unwrapTypeScriptExpression(callExpression.arguments[0]);
553
+ if (!first) {
554
+ return undefined;
555
+ }
556
+
557
+ if (first.type === 'Literal' && typeof first.value === 'string') {
558
+ return first;
559
+ }
560
+
561
+ if (first.type === 'TemplateLiteral') {
562
+ return first;
563
+ }
564
+
565
+ const staticValue = getStaticValue(first, sourceCode.getScope(first));
566
+ return typeof staticValue?.value === 'string' ? first : undefined;
567
+ }
568
+
569
+ /** Get the static string value of a node, if it resolves to one. */
570
+ export function getStaticString(node, context) {
571
+ if (!node) {
572
+ return undefined;
573
+ }
574
+
575
+ const {sourceCode} = context;
576
+ node = unwrapTypeScriptExpression(node);
577
+
578
+ if (node.type === 'Literal' && typeof node.value === 'string') {
579
+ return node.value;
580
+ }
581
+
582
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
583
+ return node.quasis[0].value.cooked ?? undefined;
584
+ }
585
+
586
+ const staticValue = getStaticValue(node, sourceCode.getScope(node));
587
+ return typeof staticValue?.value === 'string' ? staticValue.value : undefined;
588
+ }
589
+
590
+ /**
591
+ Get the inline function implementation argument of a hook call, if any.
592
+
593
+ `node:test` hooks take the callback as the first argument, with optional trailing options.
594
+ */
595
+ export function getHookCallback(callExpression) {
596
+ const firstArgument = unwrapTypeScriptExpression(callExpression.arguments[0]);
597
+ if (firstArgument && isFunction(firstArgument)) {
598
+ return firstArgument;
599
+ }
600
+
601
+ return undefined;
602
+ }
603
+
604
+ /**
605
+ Get the inline function implementation argument of a test/suite call, or a hook call where the callback is last.
606
+ */
607
+ export function getTestCallback(callExpression) {
608
+ for (let index = callExpression.arguments.length - 1; index >= 0; index -= 1) {
609
+ const argument = unwrapTypeScriptExpression(callExpression.arguments[index]);
610
+ if (isFunction(argument)) {
611
+ return argument;
612
+ }
613
+
614
+ // Stop at the first non-function trailing argument (options/title).
615
+ if (argument.type !== 'SpreadElement') {
616
+ return undefined;
617
+ }
618
+ }
619
+
620
+ return undefined;
621
+ }
622
+
623
+ /*
624
+ The number of parameters before the first default or rest parameter — the same value as `Function.prototype.length`. `node:test` uses this arity to decide whether to pass a `done` callback, so a declared second parameter means the function opted into callback style.
625
+ */
626
+ export function getEffectiveArity(parameters) {
627
+ let count = 0;
628
+ for (const parameter of parameters) {
629
+ if (parameter.type === 'AssignmentPattern' || parameter.type === 'RestElement') {
630
+ break;
631
+ }
632
+
633
+ count += 1;
634
+ }
635
+
636
+ return count;
637
+ }
638
+
639
+ /**
640
+ Get the options `ObjectExpression` argument of a test/suite/hook call, if any.
641
+ */
642
+ export function getTestOptions(callExpression) {
643
+ for (const argument of callExpression.arguments) {
644
+ const unwrapped = unwrapTypeScriptExpression(argument);
645
+ if (unwrapped.type === 'ObjectExpression') {
646
+ return unwrapped;
647
+ }
648
+ }
649
+
650
+ return undefined;
651
+ }
652
+
653
+ /**
654
+ Find a boolean-ish property (`only`/`skip`/`todo`) in an options object.
655
+ */
656
+ export function findOptionsProperty(optionsObject, name) {
657
+ if (optionsObject?.type !== 'ObjectExpression') {
658
+ return undefined;
659
+ }
660
+
661
+ return optionsObject.properties.find(property =>
662
+ property.type === 'Property'
663
+ && !property.computed
664
+ && (
665
+ (property.key.type === 'Identifier' && property.key.name === name)
666
+ || (property.key.type === 'Literal' && property.key.value === name)
667
+ ));
668
+ }
669
+
670
+ /**
671
+ Find an options property (`only`/`skip`/`todo`) that is set to an *enabled* value, i.e. present and not a statically-falsy value. `node:test` checks these options for truthiness, so `false`, `null`, `0`, `''`, and `undefined` all mean disabled; a truthy literal (`true`), a skip/todo reason string, or a dynamic value all count as enabled. Returns the property node, or `undefined`.
672
+ */
673
+ export function findEnabledOptionsProperty(optionsObject, name) {
674
+ const property = findOptionsProperty(optionsObject, name);
675
+ const value = property && unwrapTypeScriptExpression(property.value);
676
+ if (
677
+ property
678
+ && !(value.type === 'Identifier' && value.name === 'undefined')
679
+ && (value.type !== 'Literal' || value.value)
680
+ ) {
681
+ return property;
682
+ }
683
+
684
+ return undefined;
685
+ }
686
+
687
+ /**
688
+ Determine the kind (`test`/`suite`/`hook`) of the nearest enclosing test-related callback.
689
+
690
+ Returns `undefined` when the nearest enclosing function is a regular function (e.g. a helper), or there is none. Subtests (`t.test(…)`) are method calls rather than imported bindings, so they are recognized structurally and classified as `'test'`.
691
+ */
692
+ export function nearestTestCallbackKind(node, imports) {
693
+ let current = node.parent;
694
+ while (current) {
695
+ if (isFunction(current)) {
696
+ const call = current.parent;
697
+ if (call?.type === 'CallExpression') {
698
+ const parsed = parseTestCall(call, imports);
699
+ if (
700
+ parsed?.kind === 'hook'
701
+ && parsed.modifiers.length === 0
702
+ && getHookCallback(call) === current
703
+ ) {
704
+ return 'hook';
705
+ }
706
+
707
+ if (isHookMemberTestCall(parsed) && getHookCallback(call) === current) {
708
+ return 'hook';
709
+ }
710
+
711
+ if (parsed && getTestCallback(call) === current) {
712
+ return parsed.kind;
713
+ }
714
+
715
+ if (getSubtestReceiver(call) !== undefined && getTestCallback(call) === current) {
716
+ return 'test';
717
+ }
718
+ }
719
+
720
+ // Inside some other function — not directly in a test/suite/hook body.
721
+ return undefined;
722
+ }
723
+
724
+ current = current.parent;
725
+ }
726
+
727
+ return undefined;
728
+ }
729
+
730
+ function isAssertNamespaceIdentifier(node, imports) {
731
+ return (
732
+ node.type === 'Identifier'
733
+ && imports.assertNamespace.has(node.name)
734
+ && isImportedBindingReference(node, imports)
735
+ );
736
+ }
737
+
738
+ function isNamedStrictAssertIdentifier(node, imports) {
739
+ return (
740
+ node.type === 'Identifier'
741
+ && imports.assertNamed.get(node.name) === 'strict'
742
+ && isImportedBindingReference(node, imports)
743
+ );
744
+ }
745
+
746
+ function isAssertStrictMember(node, imports) {
747
+ return (
748
+ node.type === 'MemberExpression'
749
+ && !node.computed
750
+ && isAssertNamespaceIdentifier(node.object, imports)
751
+ && node.property.type === 'Identifier'
752
+ && node.property.name === 'strict'
753
+ );
754
+ }
755
+
756
+ function isTestContextAssertMember(node) {
757
+ return (
758
+ node.type === 'MemberExpression'
759
+ && !node.computed
760
+ && node.object.type === 'Identifier'
761
+ && node.property.type === 'Identifier'
762
+ && node.property.name === 'assert'
763
+ );
764
+ }
765
+
766
+ function parseAssertionMemberCall(callee, imports) {
767
+ if (
768
+ callee.type !== 'MemberExpression'
769
+ || callee.computed
770
+ || callee.property.type !== 'Identifier'
771
+ ) {
772
+ return;
773
+ }
774
+
775
+ const {object} = callee;
776
+
777
+ if (callee.property.name === 'strict' && isAssertNamespaceIdentifier(object, imports)) {
778
+ return {
779
+ method: 'ok',
780
+ methodNode: undefined,
781
+ isStrict: true,
782
+ };
783
+ }
784
+
785
+ // `assert.strictEqual(…)`
786
+ if (isAssertNamespaceIdentifier(object, imports)) {
787
+ return {
788
+ method: callee.property.name,
789
+ methodNode: callee.property,
790
+ isStrict: imports.strictAssertLocals.has(object.name),
791
+ };
792
+ }
793
+
794
+ // `strictAssert.equal(…)` where `strictAssert` is `import {strict as strictAssert} from 'node:assert'`.
795
+ if (isNamedStrictAssertIdentifier(object, imports)) {
796
+ return {
797
+ method: callee.property.name,
798
+ methodNode: callee.property,
799
+ isStrict: true,
800
+ };
801
+ }
802
+
803
+ // `assert.strict.equal(…)`
804
+ if (isAssertStrictMember(object, imports)) {
805
+ return {
806
+ method: callee.property.name,
807
+ methodNode: callee.property,
808
+ isStrict: true,
809
+ };
810
+ }
811
+
812
+ // `t.assert.strictEqual(…)`: `t.assert` is always loose mode. The receiver must be a plain identifier (a test context parameter); deeper chains like `a.b.assert.equal(…)`, `this.assert`, or `foo().assert` are unrelated objects that merely have an `assert` property.
813
+ if (isTestContextAssertMember(object)) {
814
+ return {
815
+ method: callee.property.name,
816
+ methodNode: callee.property,
817
+ isStrict: false,
818
+ contextReceiver: object.object,
819
+ };
820
+ }
821
+ }
822
+
823
+ /**
824
+ Classify a `CallExpression` as a `node:assert` assertion call.
825
+
826
+ Matches:
827
+ - `assert.strictEqual(…)` / `assert(…)` (namespace import)
828
+ - `assert.strict.equal(…)` / `assert.strict(…)` / `strictAssert.equal(…)` / `strictAssert(…)` (strict namespace)
829
+ - `strictEqual(…)` (named import)
830
+ - `t.assert.strictEqual(…)` (`TestContext#assert`)
831
+
832
+ `methodNode` is the identifier node holding the method name, which fixers rewrite. It is the callee itself for a named import, the property for member method calls, and `undefined` for callable assert forms like `assert(…)`, `assert.strict(…)`, or `strictAssert(…)`. `isStrict` is `true` when the binding resolves to a strict-mode assert API, where the legacy methods already behave strictly.
833
+
834
+ @returns {{method: string, methodNode: import('estree').Node | undefined, isStrict: boolean, contextReceiver?: import('estree').Identifier}|undefined}
835
+ */
836
+ export const parseAssertionCall = memoizeByNode(parseAssertionCallCache, (callExpression, imports) => {
837
+ const {callee} = callExpression;
838
+
839
+ if (
840
+ callee.type === 'Identifier'
841
+ && imports.assertNamed.get(callee.name) === 'strict'
842
+ && isImportedBindingReference(callee, imports)
843
+ ) {
844
+ return {
845
+ method: 'ok',
846
+ methodNode: undefined,
847
+ isStrict: true,
848
+ };
849
+ }
850
+
851
+ // `strictEqual(…)` — named import.
852
+ if (
853
+ callee.type === 'Identifier'
854
+ && imports.assertNamed.has(callee.name)
855
+ && isImportedBindingReference(callee, imports)
856
+ ) {
857
+ return {
858
+ method: imports.assertNamed.get(callee.name),
859
+ methodNode: callee,
860
+ isStrict: imports.strictAssertLocals.has(callee.name),
861
+ };
862
+ }
863
+
864
+ if (
865
+ callee.type === 'Identifier'
866
+ && imports.assertNamespace.has(callee.name)
867
+ && isImportedBindingReference(callee, imports)
868
+ ) {
869
+ // `assert(value)` — the bare assert function (alias of `ok`); no method identifier to rewrite.
870
+ return {
871
+ method: 'ok',
872
+ methodNode: undefined,
873
+ isStrict: imports.strictAssertLocals.has(callee.name),
874
+ };
875
+ }
876
+
877
+ const memberAssertionCall = parseAssertionMemberCall(callee, imports);
878
+ if (memberAssertionCall) {
879
+ return memberAssertionCall;
880
+ }
881
+
882
+ return undefined;
883
+ });