@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,313 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {
3
+ resolveImports,
4
+ parseTestCall,
5
+ getTestCallback,
6
+ getSubtestReceiver,
7
+ HOOK_FUNCTIONS,
8
+ isGlobalMock,
9
+ MODIFIERS,
10
+ } from './utils/node-test.js';
11
+ import {getEnclosingFunction} from './utils/index.js';
12
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
13
+
14
+ const MESSAGE_ID = 'require-mock-timers-advance';
15
+ const messages = {
16
+ [MESSAGE_ID]: '`mock.timers.enable()` must be followed by `tick()` or `runAll()` so mocked timer callbacks can run.',
17
+ };
18
+
19
+ const ADVANCE_METHODS = new Set(['tick', 'runAll']);
20
+
21
+ function getStaticPropertyName(node) {
22
+ if (node.type === 'Identifier') {
23
+ return node.name;
24
+ }
25
+
26
+ if (node.type === 'Literal' && typeof node.value === 'string') {
27
+ return node.value;
28
+ }
29
+ }
30
+
31
+ function getPropertyName(property) {
32
+ if (property.type !== 'Property') {
33
+ return;
34
+ }
35
+
36
+ if (!property.computed) {
37
+ return getStaticPropertyName(property.key);
38
+ }
39
+
40
+ if (property.key.type === 'Literal' && typeof property.key.value === 'string') {
41
+ return property.key.value;
42
+ }
43
+ }
44
+
45
+ function isDynamicProperty(property) {
46
+ return property.type === 'Property' && property.computed && property.key.type !== 'Literal';
47
+ }
48
+
49
+ function mayEnableTimerApis(callExpression) {
50
+ const argument = unwrapTypeScriptExpression(callExpression.arguments[0]);
51
+ if (argument?.type !== 'ObjectExpression') {
52
+ return true;
53
+ }
54
+
55
+ let apisValue;
56
+ for (const property of argument.properties) {
57
+ if (property.type === 'SpreadElement') {
58
+ if (apisValue) {
59
+ return true;
60
+ }
61
+
62
+ continue;
63
+ }
64
+
65
+ if (getPropertyName(property) === 'apis') {
66
+ apisValue = property.value;
67
+ } else if (apisValue && isDynamicProperty(property)) {
68
+ return true;
69
+ }
70
+ }
71
+
72
+ if (!apisValue) {
73
+ return true;
74
+ }
75
+
76
+ const unwrappedApisValue = unwrapTypeScriptExpression(apisValue);
77
+ if (unwrappedApisValue.type !== 'ArrayExpression') {
78
+ return true;
79
+ }
80
+
81
+ for (const rawElement of unwrappedApisValue.elements) {
82
+ const element = rawElement && unwrapTypeScriptExpression(rawElement);
83
+ if (!element) {
84
+ continue;
85
+ }
86
+
87
+ if (element.type !== 'Literal' || typeof element.value !== 'string') {
88
+ return true;
89
+ }
90
+
91
+ if (element.value === 'Date') {
92
+ continue;
93
+ }
94
+
95
+ return true;
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ function isImportedIdentifier(node, sourceCode) {
102
+ const variable = findVariable(sourceCode.getScope(node), node);
103
+ return variable?.defs.some(({type}) => type === 'ImportBinding') ?? false;
104
+ }
105
+
106
+ function getMockTimersReceiverKey(node, imports, sourceCode, contextVariables) {
107
+ if (
108
+ node.type !== 'MemberExpression'
109
+ || node.computed
110
+ || node.optional
111
+ || getStaticPropertyName(node.property) !== 'timers'
112
+ ) {
113
+ return;
114
+ }
115
+
116
+ if (isGlobalMock(node.object, imports)) {
117
+ return 'global';
118
+ }
119
+
120
+ if (
121
+ node.object.type === 'MemberExpression'
122
+ && !node.object.computed
123
+ && !node.object.optional
124
+ && getStaticPropertyName(node.object.property) === 'mock'
125
+ && node.object.object.type === 'Identifier'
126
+ ) {
127
+ const variable = findVariable(sourceCode.getScope(node.object.object), node.object.object);
128
+ if (!variable || !contextVariables.includes(variable)) {
129
+ return;
130
+ }
131
+
132
+ return `context:${node.object.object.name}`;
133
+ }
134
+ }
135
+
136
+ function getMockTimersCall(callExpression, imports, sourceCode, contextVariables) {
137
+ const {callee} = callExpression;
138
+ if (
139
+ callee.type !== 'MemberExpression'
140
+ || callee.computed
141
+ || callee.optional
142
+ ) {
143
+ return;
144
+ }
145
+
146
+ const method = getStaticPropertyName(callee.property);
147
+ const receiverKey = getMockTimersReceiverKey(callee.object, imports, sourceCode, contextVariables);
148
+ if (!method || !receiverKey) {
149
+ return;
150
+ }
151
+
152
+ return {method, receiverKey};
153
+ }
154
+
155
+ function satisfyPending(scope, receiverKey) {
156
+ for (const pending of scope.pending) {
157
+ if (!pending.satisfied && pending.receiverKey === receiverKey) {
158
+ pending.satisfied = true;
159
+ }
160
+ }
161
+ }
162
+
163
+ function getContextVariable(callback, sourceCode) {
164
+ const [parameter] = callback.params;
165
+ if (parameter?.type !== 'Identifier') {
166
+ return;
167
+ }
168
+
169
+ return findVariable(sourceCode.getScope(parameter), parameter);
170
+ }
171
+
172
+ function getCalleeRootIdentifier(node) {
173
+ while (
174
+ node.type === 'MemberExpression'
175
+ && !node.computed
176
+ && !node.optional
177
+ ) {
178
+ node = node.object;
179
+ }
180
+
181
+ return node.type === 'Identifier' ? node : undefined;
182
+ }
183
+
184
+ function getContextVariables(scopeStack) {
185
+ return scopeStack
186
+ .map(scope => scope.contextVariable)
187
+ .filter(Boolean);
188
+ }
189
+
190
+ function getContextCallKind(node, sourceCode, contextVariables) {
191
+ const receiver = getSubtestReceiver(node);
192
+ if (receiver) {
193
+ const receiverVariable = findVariable(sourceCode.getScope(receiver), receiver);
194
+ return receiverVariable && contextVariables.includes(receiverVariable) ? 'test' : undefined;
195
+ }
196
+
197
+ const {callee} = node;
198
+ if (
199
+ callee.type !== 'MemberExpression'
200
+ || callee.computed
201
+ || callee.optional
202
+ || callee.object.type !== 'Identifier'
203
+ || !HOOK_FUNCTIONS.has(getStaticPropertyName(callee.property))
204
+ ) {
205
+ return;
206
+ }
207
+
208
+ const receiverVariable = findVariable(sourceCode.getScope(callee.object), callee.object);
209
+ return receiverVariable && contextVariables.includes(receiverVariable) ? 'hook' : undefined;
210
+ }
211
+
212
+ function getScopeCallback(node, imports, sourceCode, contextVariables) {
213
+ const parsed = parseTestCall(node, imports);
214
+ const root = getCalleeRootIdentifier(node.callee);
215
+ if (
216
+ (parsed?.kind === 'test' || parsed?.kind === 'hook')
217
+ && parsed.modifiers.every(modifier => MODIFIERS.has(modifier.name))
218
+ && root
219
+ && isImportedIdentifier(root, sourceCode)
220
+ ) {
221
+ return getTestCallback(node);
222
+ }
223
+
224
+ const contextCallKind = getContextCallKind(node, sourceCode, contextVariables);
225
+ return contextCallKind === 'test' || contextCallKind === 'hook' ? getTestCallback(node) : undefined;
226
+ }
227
+
228
+ /** @param {import('eslint').Rule.RuleContext} context */
229
+ const create = context => {
230
+ const {sourceCode} = context;
231
+ const imports = resolveImports(context);
232
+ if (!imports.isTestFile) {
233
+ return;
234
+ }
235
+
236
+ const scopeStack = [];
237
+
238
+ context.on('CallExpression', node => {
239
+ const callback = getScopeCallback(node, imports, sourceCode, getContextVariables(scopeStack));
240
+ if (callback) {
241
+ const contextVariable = getContextVariable(callback, sourceCode);
242
+ scopeStack.push({
243
+ callNode: node,
244
+ callback,
245
+ contextVariable,
246
+ pending: [],
247
+ });
248
+ }
249
+
250
+ if (scopeStack.length === 0) {
251
+ return;
252
+ }
253
+
254
+ const currentScope = scopeStack.at(-1);
255
+ if (getEnclosingFunction(node) !== currentScope.callback) {
256
+ return;
257
+ }
258
+
259
+ const timersCall = getMockTimersCall(node, imports, sourceCode, getContextVariables(scopeStack));
260
+ if (timersCall && ADVANCE_METHODS.has(timersCall.method)) {
261
+ satisfyPending(currentScope, timersCall.receiverKey);
262
+ }
263
+ });
264
+
265
+ context.onExit('CallExpression', node => {
266
+ let problems;
267
+ const currentScope = scopeStack.at(-1);
268
+ if (currentScope?.callNode === node) {
269
+ scopeStack.pop();
270
+ problems = currentScope.pending
271
+ .filter(pending => !pending.satisfied)
272
+ .map(pending => ({
273
+ node: pending.node,
274
+ messageId: pending.messageId,
275
+ }));
276
+ }
277
+
278
+ const newCurrentScope = scopeStack.at(-1);
279
+ if (
280
+ newCurrentScope
281
+ && getEnclosingFunction(node) === newCurrentScope.callback
282
+ ) {
283
+ const timersCall = getMockTimersCall(node, imports, sourceCode, getContextVariables(scopeStack));
284
+ if (timersCall?.method === 'enable' && mayEnableTimerApis(node)) {
285
+ newCurrentScope.pending.push({
286
+ node,
287
+ messageId: MESSAGE_ID,
288
+ receiverKey: timersCall.receiverKey,
289
+ satisfied: false,
290
+ });
291
+ }
292
+ }
293
+
294
+ return problems;
295
+ });
296
+ };
297
+
298
+ /** @type {import('eslint').Rule.RuleModule} */
299
+ const config = {
300
+ create,
301
+ meta: {
302
+ type: 'problem',
303
+ docs: {
304
+ description: 'Require mock timers to be advanced after enabling timer APIs.',
305
+ recommended: true,
306
+ },
307
+ schema: [],
308
+ messages,
309
+ languages: ['js/js'],
310
+ },
311
+ };
312
+
313
+ export default config;
@@ -0,0 +1,383 @@
1
+ import {findVariable} from '@eslint-community/eslint-utils';
2
+ import {isFunction} from './ast/index.js';
3
+ import {
4
+ getSubtestReceiver,
5
+ getTestCallback,
6
+ MODIFIERS,
7
+ parseTestCall,
8
+ resolveImports,
9
+ } from './utils/node-test.js';
10
+ import unwrapTypeScriptExpression from './utils/unwrap-typescript-expression.js';
11
+
12
+ const MESSAGE_ID = 'require-mock-timers-apis';
13
+ const CONTEXT_HOOKS = new Set(['before', 'beforeEach', 'after', 'afterEach']);
14
+ const TEST_MODULES = new Set(['node:test', 'test']);
15
+ const STATIC_NON_OPTIONS_VALUE_TYPES = new Set(['ArrayExpression', 'Literal', 'TemplateLiteral']);
16
+
17
+ const messages = {
18
+ [MESSAGE_ID]: '`mock.timers.enable()` should explicitly specify the `apis` option to avoid unexpectedly mocking `Date`.',
19
+ };
20
+
21
+ function isMissingApisValue(node) {
22
+ const expression = unwrapTypeScriptExpression(node);
23
+ if (!expression) {
24
+ return true;
25
+ }
26
+
27
+ return (
28
+ (expression.type === 'Identifier' && expression.name === 'undefined')
29
+ || (expression.type === 'UnaryExpression' && expression.operator === 'void')
30
+ || (expression.type === 'Literal' && !expression.value)
31
+ );
32
+ }
33
+
34
+ function isStaticNonOptionsValue(node) {
35
+ return STATIC_NON_OPTIONS_VALUE_TYPES.has(node.type);
36
+ }
37
+
38
+ function isApisProperty(property) {
39
+ return property.type === 'Property'
40
+ && !property.computed
41
+ && (
42
+ (property.key.type === 'Identifier' && property.key.name === 'apis')
43
+ || (property.key.type === 'Literal' && property.key.value === 'apis')
44
+ );
45
+ }
46
+
47
+ function getLastVisibleApisProperty(optionsObject) {
48
+ for (let index = optionsObject.properties.length - 1; index >= 0; index -= 1) {
49
+ const property = optionsObject.properties[index];
50
+ if (property.type === 'SpreadElement') {
51
+ return undefined;
52
+ }
53
+
54
+ if (isApisProperty(property)) {
55
+ return property;
56
+ }
57
+ }
58
+
59
+ return undefined;
60
+ }
61
+
62
+ function isImportedReference(node, sourceCode) {
63
+ const variable = findVariable(sourceCode.getScope(node), node);
64
+ return variable?.defs.some(({type}) => type === 'ImportBinding') ?? false;
65
+ }
66
+
67
+ function isMissingApisOption(callExpression) {
68
+ const firstArgument = unwrapTypeScriptExpression(callExpression.arguments[0]);
69
+ if (!firstArgument) {
70
+ return true;
71
+ }
72
+
73
+ if (isMissingApisValue(firstArgument)) {
74
+ return true;
75
+ }
76
+
77
+ if (firstArgument.type !== 'ObjectExpression') {
78
+ return isStaticNonOptionsValue(firstArgument);
79
+ }
80
+
81
+ const apisProperty = getLastVisibleApisProperty(firstArgument);
82
+ return !apisProperty || isMissingApisValue(apisProperty.value);
83
+ }
84
+
85
+ function isContextProvidingCall(testCall) {
86
+ if (testCall.kind === 'hook') {
87
+ return testCall.modifiers.length === 0;
88
+ }
89
+
90
+ return testCall.kind === 'test'
91
+ && testCall.modifiers.every(modifier => MODIFIERS.has(modifier.name));
92
+ }
93
+
94
+ function getParameterIdentifier(parameter) {
95
+ if (parameter?.type === 'Identifier') {
96
+ return parameter;
97
+ }
98
+
99
+ if (parameter?.type === 'AssignmentPattern' && parameter.left.type === 'Identifier') {
100
+ return parameter.left;
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ function getContextHookReceiver(callExpression) {
107
+ const {callee} = callExpression;
108
+ if (
109
+ callee.type !== 'MemberExpression'
110
+ || callee.computed
111
+ || callee.property.type !== 'Identifier'
112
+ || !CONTEXT_HOOKS.has(callee.property.name)
113
+ ) {
114
+ return undefined;
115
+ }
116
+
117
+ const receiver = unwrapTypeScriptExpression(callee.object);
118
+ return receiver.type === 'Identifier' ? receiver : undefined;
119
+ }
120
+
121
+ function getTestContextImportLocals(sourceCode) {
122
+ const locals = new Set();
123
+
124
+ for (const node of sourceCode.ast.body) {
125
+ if (node.type !== 'ImportDeclaration' || !TEST_MODULES.has(node.source.value)) {
126
+ continue;
127
+ }
128
+
129
+ for (const specifier of node.specifiers) {
130
+ if (
131
+ specifier.type === 'ImportSpecifier'
132
+ && specifier.imported.type === 'Identifier'
133
+ && specifier.imported.name === 'getTestContext'
134
+ ) {
135
+ locals.add(specifier.local.name);
136
+ }
137
+ }
138
+ }
139
+
140
+ return locals;
141
+ }
142
+
143
+ function getHookCallback(callExpression) {
144
+ const firstArgument = unwrapTypeScriptExpression(callExpression.arguments[0]);
145
+ return isFunction(firstArgument) ? firstArgument : undefined;
146
+ }
147
+
148
+ function getLocalTestHookCall(callExpression, imports, sourceCode) {
149
+ const callee = unwrapTypeScriptExpression(callExpression.callee);
150
+ if (
151
+ callee.type !== 'MemberExpression'
152
+ || callee.computed
153
+ || callee.property.type !== 'Identifier'
154
+ || !CONTEXT_HOOKS.has(callee.property.name)
155
+ ) {
156
+ return undefined;
157
+ }
158
+
159
+ const object = unwrapTypeScriptExpression(callee.object);
160
+ if (
161
+ object.type !== 'Identifier'
162
+ || !['test', 'it'].includes(imports.locals.get(object.name))
163
+ || !isImportedReference(object, sourceCode)
164
+ ) {
165
+ return undefined;
166
+ }
167
+
168
+ return {
169
+ name: callee.property.name,
170
+ kind: 'hook',
171
+ modifiers: [],
172
+ };
173
+ }
174
+
175
+ /** @param {import('eslint').Rule.RuleContext} context */
176
+ const create = context => {
177
+ const {sourceCode} = context;
178
+ const imports = resolveImports(context);
179
+ const testContextLocals = getTestContextImportLocals(sourceCode);
180
+ if (!imports.isTestFile && testContextLocals.size === 0) {
181
+ return;
182
+ }
183
+
184
+ const contextVariables = [];
185
+ const pushedCalls = new Set();
186
+
187
+ const getImportedContextCall = node => {
188
+ const testCall = parseTestCall(node, imports);
189
+ if (testCall !== undefined && isContextProvidingCall(testCall)) {
190
+ return testCall;
191
+ }
192
+
193
+ return getLocalTestHookCall(node, imports, sourceCode);
194
+ };
195
+
196
+ const isSubtestCall = node => {
197
+ const receiver = getSubtestReceiver(node);
198
+ if (!receiver) {
199
+ return false;
200
+ }
201
+
202
+ const variable = findVariable(sourceCode.getScope(receiver), receiver);
203
+ return variable !== undefined && contextVariables.includes(variable);
204
+ };
205
+
206
+ const isContextHookCall = node => {
207
+ const receiver = getContextHookReceiver(node);
208
+ if (!receiver) {
209
+ return false;
210
+ }
211
+
212
+ const variable = findVariable(sourceCode.getScope(receiver), receiver);
213
+ return variable !== undefined && contextVariables.includes(variable);
214
+ };
215
+
216
+ const updateContext = node => {
217
+ const importedContextCall = getImportedContextCall(node);
218
+ const isContextHook = isContextHookCall(node);
219
+ if (!importedContextCall && !isSubtestCall(node) && !isContextHook) {
220
+ return;
221
+ }
222
+
223
+ const callback = importedContextCall?.kind === 'hook' || isContextHook
224
+ ? getHookCallback(node)
225
+ : getTestCallback(node);
226
+ if (!callback) {
227
+ return;
228
+ }
229
+
230
+ const parameter = getParameterIdentifier(callback.params[0]);
231
+ const variable = parameter
232
+ ? findVariable(sourceCode.getScope(parameter), parameter)
233
+ : undefined;
234
+
235
+ contextVariables.push(variable);
236
+ pushedCalls.add(node);
237
+ };
238
+
239
+ const leaveContext = node => {
240
+ if (!pushedCalls.has(node)) {
241
+ return;
242
+ }
243
+
244
+ pushedCalls.delete(node);
245
+ contextVariables.pop();
246
+ };
247
+
248
+ const isGlobalMock = node => {
249
+ const expression = unwrapTypeScriptExpression(node);
250
+ if (
251
+ expression.type === 'Identifier'
252
+ && imports.mockLocals.has(expression.name)
253
+ && isImportedReference(expression, sourceCode)
254
+ ) {
255
+ return true;
256
+ }
257
+
258
+ if (
259
+ expression.type !== 'MemberExpression'
260
+ || expression.computed
261
+ || expression.property.type !== 'Identifier'
262
+ || expression.property.name !== 'mock'
263
+ ) {
264
+ return false;
265
+ }
266
+
267
+ const object = unwrapTypeScriptExpression(expression.object);
268
+ return object.type === 'Identifier'
269
+ && (
270
+ object.name === imports.namespace
271
+ || imports.locals.get(object.name) === 'test'
272
+ || imports.locals.get(object.name) === 'it'
273
+ )
274
+ && isImportedReference(object, sourceCode);
275
+ };
276
+
277
+ const isCurrentContextReference = node => {
278
+ const variable = findVariable(sourceCode.getScope(node), node);
279
+ return variable !== undefined && contextVariables.includes(variable);
280
+ };
281
+
282
+ const isGetTestContextCall = node => {
283
+ if (node.type !== 'CallExpression') {
284
+ return false;
285
+ }
286
+
287
+ const callee = unwrapTypeScriptExpression(node.callee);
288
+ if (
289
+ callee.type === 'Identifier'
290
+ && testContextLocals.has(callee.name)
291
+ && isImportedReference(callee, sourceCode)
292
+ ) {
293
+ return true;
294
+ }
295
+
296
+ if (
297
+ callee.type !== 'MemberExpression'
298
+ || callee.computed
299
+ || callee.property.type !== 'Identifier'
300
+ || callee.property.name !== 'getTestContext'
301
+ ) {
302
+ return false;
303
+ }
304
+
305
+ const object = unwrapTypeScriptExpression(callee.object);
306
+ return object.type === 'Identifier'
307
+ && (
308
+ object.name === imports.namespace
309
+ || imports.locals.get(object.name) === 'test'
310
+ || imports.locals.get(object.name) === 'it'
311
+ )
312
+ && isImportedReference(object, sourceCode);
313
+ };
314
+
315
+ const isContextMock = node => {
316
+ const expression = unwrapTypeScriptExpression(node);
317
+ if (
318
+ expression.type !== 'MemberExpression'
319
+ || expression.computed
320
+ || expression.property.type !== 'Identifier'
321
+ || expression.property.name !== 'mock'
322
+ ) {
323
+ return false;
324
+ }
325
+
326
+ const object = unwrapTypeScriptExpression(expression.object);
327
+ return (
328
+ (object.type === 'Identifier' && isCurrentContextReference(object))
329
+ || isGetTestContextCall(object)
330
+ );
331
+ };
332
+
333
+ const isMockTimers = node => {
334
+ const expression = unwrapTypeScriptExpression(node);
335
+ return expression.type === 'MemberExpression'
336
+ && !expression.computed
337
+ && expression.property.type === 'Identifier'
338
+ && expression.property.name === 'timers'
339
+ && (isGlobalMock(expression.object) || isContextMock(expression.object));
340
+ };
341
+
342
+ context.on('CallExpression', node => {
343
+ updateContext(node);
344
+ });
345
+
346
+ context.onExit('CallExpression', node => {
347
+ leaveContext(node);
348
+ });
349
+
350
+ context.on('CallExpression', node => {
351
+ const callee = unwrapTypeScriptExpression(node.callee);
352
+ if (
353
+ callee.type === 'MemberExpression'
354
+ && !callee.computed
355
+ && callee.property.type === 'Identifier'
356
+ && callee.property.name === 'enable'
357
+ && isMockTimers(callee.object)
358
+ && isMissingApisOption(node)
359
+ ) {
360
+ return {
361
+ node,
362
+ messageId: MESSAGE_ID,
363
+ };
364
+ }
365
+ });
366
+ };
367
+
368
+ /** @type {import('eslint').Rule.RuleModule} */
369
+ const config = {
370
+ create,
371
+ meta: {
372
+ type: 'problem',
373
+ docs: {
374
+ description: 'Require an explicit `apis` option when enabling `mock.timers`.',
375
+ recommended: 'unopinionated',
376
+ },
377
+ schema: [],
378
+ messages,
379
+ languages: ['js/js'],
380
+ },
381
+ };
382
+
383
+ export default config;