@pobammer-ts/eslint-cease-nonsense-rules 0.10.1 → 1.0.0

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 (35) hide show
  1. package/dist/rules/ban-react-fc.js +6 -37
  2. package/dist/rules/ban-react-fc.js.map +1 -1
  3. package/dist/rules/enforce-ianitor-check-type.d.ts +7 -0
  4. package/dist/rules/enforce-ianitor-check-type.js +71 -159
  5. package/dist/rules/enforce-ianitor-check-type.js.map +1 -1
  6. package/dist/rules/no-color3-constructor.d.ts +0 -17
  7. package/dist/rules/no-color3-constructor.js +11 -32
  8. package/dist/rules/no-color3-constructor.js.map +1 -1
  9. package/dist/rules/no-instance-methods-without-this.js +2 -3
  10. package/dist/rules/no-instance-methods-without-this.js.map +1 -1
  11. package/dist/rules/no-print.d.ts +0 -15
  12. package/dist/rules/no-print.js +0 -21
  13. package/dist/rules/no-print.js.map +1 -1
  14. package/dist/rules/no-shorthand-names.d.ts +0 -24
  15. package/dist/rules/no-shorthand-names.js +32 -86
  16. package/dist/rules/no-shorthand-names.js.map +1 -1
  17. package/dist/rules/no-warn.d.ts +0 -15
  18. package/dist/rules/no-warn.js +0 -21
  19. package/dist/rules/no-warn.js.map +1 -1
  20. package/dist/rules/prefer-sequence-overloads.js +26 -23
  21. package/dist/rules/prefer-sequence-overloads.js.map +1 -1
  22. package/dist/rules/prefer-udim2-shorthand.d.ts +0 -14
  23. package/dist/rules/prefer-udim2-shorthand.js +138 -44
  24. package/dist/rules/prefer-udim2-shorthand.js.map +1 -1
  25. package/dist/rules/require-named-effect-functions.js +271 -59
  26. package/dist/rules/require-named-effect-functions.js.map +1 -1
  27. package/dist/rules/require-react-component-keys.d.ts +3 -39
  28. package/dist/rules/require-react-component-keys.js +58 -158
  29. package/dist/rules/require-react-component-keys.js.map +1 -1
  30. package/dist/rules/use-exhaustive-dependencies.d.ts +0 -59
  31. package/dist/rules/use-exhaustive-dependencies.js +120 -329
  32. package/dist/rules/use-exhaustive-dependencies.js.map +1 -1
  33. package/dist/rules/use-hook-at-top-level.js +17 -118
  34. package/dist/rules/use-hook-at-top-level.js.map +1 -1
  35. package/package.json +11 -8
@@ -11,9 +11,6 @@ const UNSTABLE_VALUES = new Set([
11
11
  TSESTree.AST_NODE_TYPES.ObjectExpression,
12
12
  TSESTree.AST_NODE_TYPES.ArrayExpression,
13
13
  ]);
14
- /**
15
- * Internal metrics used for testing to ensure specific branches execute.
16
- */
17
14
  const testingMetrics = {
18
15
  moduleLevelStableConst: 0,
19
16
  outerScopeSkip: 0,
@@ -22,9 +19,6 @@ function resetTestingMetrics() {
22
19
  testingMetrics.moduleLevelStableConst = 0;
23
20
  testingMetrics.outerScopeSkip = 0;
24
21
  }
25
- /**
26
- * Default hooks to check for exhaustive dependencies.
27
- */
28
22
  const DEFAULT_HOOKS = new Map([
29
23
  ["useEffect", { closureIndex: 0, dependenciesIndex: 1 }],
30
24
  ["useLayoutEffect", { closureIndex: 0, dependenciesIndex: 1 }],
@@ -32,38 +26,23 @@ const DEFAULT_HOOKS = new Map([
32
26
  ["useCallback", { closureIndex: 0, dependenciesIndex: 1 }],
33
27
  ["useMemo", { closureIndex: 0, dependenciesIndex: 1 }],
34
28
  ["useImperativeHandle", { closureIndex: 1, dependenciesIndex: 2 }],
35
- // React Spring hooks (function factory pattern)
36
- // Note: These hooks support both function and object patterns.
37
- // Only the function pattern is analyzed for dependencies.
38
29
  ["useSpring", { closureIndex: 0, dependenciesIndex: 1 }],
39
30
  ["useSprings", { closureIndex: 1, dependenciesIndex: 2 }],
40
31
  ["useTrail", { closureIndex: 1, dependenciesIndex: 2 }],
41
32
  ]);
42
- /**
43
- * Hooks with stable results that don't need to be in dependencies.
44
- */
45
33
  const STABLE_HOOKS = new Map([
46
- ["useState", new Set([1])], // setter at index 1
47
- ["useReducer", new Set([1])], // dispatch at index 1
48
- ["useTransition", new Set([1])], // startTransition at index 1
49
- ["useRef", true], // entire result is stable
50
- ["useBinding", true], // React Lua: both binding and setter are stable
34
+ ["useState", new Set([1])],
35
+ ["useReducer", new Set([1])],
36
+ ["useTransition", new Set([1])],
37
+ ["useRef", true],
38
+ ["useBinding", true],
51
39
  ]);
52
- /**
53
- * Values that don't need to be in dependencies (imported, constants, etc.).
54
- */
55
40
  const STABLE_VALUE_TYPES = new Set(["ImportBinding", "FunctionDeclaration", "ClassDeclaration", "FunctionName"]);
56
- /**
57
- * Global built-in identifiers that are always stable and should never be dependencies.
58
- * Includes JavaScript/TypeScript globals, constructors, and type-only names.
59
- */
60
41
  const GLOBAL_BUILTINS = new Set([
61
- // Primitive values
62
42
  "undefined",
63
43
  "null",
64
44
  "Infinity",
65
45
  "NaN",
66
- // Constructors
67
46
  "Array",
68
47
  "Object",
69
48
  "String",
@@ -72,20 +51,16 @@ const GLOBAL_BUILTINS = new Set([
72
51
  "Symbol",
73
52
  "BigInt",
74
53
  "Function",
75
- // Collections
76
54
  "Map",
77
55
  "Set",
78
56
  "WeakMap",
79
57
  "WeakSet",
80
- // Promises and async
81
58
  "Promise",
82
- // Utility
83
59
  "Date",
84
60
  "RegExp",
85
61
  "Error",
86
62
  "Math",
87
63
  "JSON",
88
- // Global functions
89
64
  "parseInt",
90
65
  "parseFloat",
91
66
  "isNaN",
@@ -94,7 +69,6 @@ const GLOBAL_BUILTINS = new Set([
94
69
  "encodeURIComponent",
95
70
  "decodeURI",
96
71
  "decodeURIComponent",
97
- // TypeScript utility types (appear in type annotations but shouldn't be dependencies)
98
72
  "ReadonlyArray",
99
73
  "ReadonlyMap",
100
74
  "ReadonlySet",
@@ -110,109 +84,65 @@ const GLOBAL_BUILTINS = new Set([
110
84
  "ReturnType",
111
85
  "InstanceType",
112
86
  "Parameters",
113
- // Web/Node globals commonly seen
114
87
  "console",
115
88
  "setTimeout",
116
89
  "setInterval",
117
90
  "clearTimeout",
118
91
  "clearInterval",
119
- // Common DOM/Web types
120
92
  "Element",
121
93
  "Node",
122
94
  "Document",
123
95
  "Window",
124
96
  "Event",
125
97
  ]);
126
- /**
127
- * Gets the hook name from a call expression.
128
- *
129
- * @param node - The call expression node.
130
- * @returns The hook name or undefined.
131
- */
132
98
  function getHookName(node) {
133
99
  const { callee } = node;
134
- // Direct call: useEffect(...)
135
- if (callee.type === "Identifier") {
100
+ if (callee.type === TSESTree.AST_NODE_TYPES.Identifier)
136
101
  return callee.name;
137
- }
138
- // Member expression: React.useEffect(...)
139
- if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
102
+ if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
103
+ callee.property.type === TSESTree.AST_NODE_TYPES.Identifier)
140
104
  return callee.property.name;
141
- }
142
105
  return undefined;
143
106
  }
144
- /**
145
- * Gets the member expression depth (number of property accesses).
146
- *
147
- * @param node - The node to analyze.
148
- * @returns The depth count.
149
- */
150
107
  function getMemberExpressionDepth(node) {
151
108
  let depth = 0;
152
109
  let current = node;
153
- while (current.type === "MemberExpression") {
110
+ while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
154
111
  depth += 1;
155
112
  current = current.object;
156
113
  }
157
114
  return depth;
158
115
  }
159
- /**
160
- * Gets the root identifier from a member expression.
161
- *
162
- * @param node - The node to analyze.
163
- * @returns The root identifier or undefined.
164
- */
165
116
  function getRootIdentifier(node) {
166
117
  let current = node;
167
- while (current.type === "MemberExpression")
118
+ while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression)
168
119
  current = current.object;
169
- return current.type === "Identifier" ? current : undefined;
120
+ return current.type === TSESTree.AST_NODE_TYPES.Identifier ? current : undefined;
170
121
  }
171
- /**
172
- * Converts a node to a dependency string representation.
173
- *
174
- * @param node - The node to convert.
175
- * @param sourceCode - The source code instance.
176
- * @returns The dependency string.
177
- */
178
122
  function nodeToDependencyString(node, sourceCode) {
179
123
  return sourceCode.getText(node);
180
124
  }
181
- /**
182
- * Checks if a stable array index is being accessed.
183
- *
184
- * @param stableResult - The stable result set.
185
- * @param node - The variable declarator node.
186
- * @param identifierName - The identifier name being accessed.
187
- * @returns True if accessing a stable array index.
188
- */
189
125
  function isStableArrayIndex(stableResult, node, identifierName) {
190
126
  if (!stableResult)
191
127
  return false;
192
- if (!(stableResult instanceof Set) || node.type !== "VariableDeclarator" || node.id.type !== "ArrayPattern")
128
+ if (!(stableResult instanceof Set) ||
129
+ node.type !== TSESTree.AST_NODE_TYPES.VariableDeclarator ||
130
+ node.id.type !== TSESTree.AST_NODE_TYPES.ArrayPattern)
193
131
  return false;
194
132
  const elements = node.id.elements;
195
- for (let index = 0; index < elements.length; index += 1) {
196
- const element = elements[index];
197
- if (element?.type === "Identifier" && element.name === identifierName)
133
+ let index = 0;
134
+ for (const element of elements) {
135
+ if (element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === identifierName)
198
136
  return stableResult.has(index);
137
+ index += 1;
199
138
  }
200
139
  return false;
201
140
  }
202
- /**
203
- * Checks if a value is from a stable hook.
204
- *
205
- * @param init - The initializer expression.
206
- * @param node - The variable declarator node.
207
- * @param identifierName - The identifier name being accessed.
208
- * @param stableHooks - Map of stable hooks.
209
- * @returns True if the value is from a stable hook.
210
- */
211
141
  function isStableHookValue(init, node, identifierName, stableHooks) {
212
- const initNode = init;
213
- if (initNode.type !== "CallExpression")
142
+ const castInit = init;
143
+ if (castInit.type !== TSESTree.AST_NODE_TYPES.CallExpression)
214
144
  return false;
215
- const hookName = getHookName(initNode);
145
+ const hookName = getHookName(castInit);
216
146
  if (!hookName)
217
147
  return false;
218
148
  const stableResult = stableHooks.get(hookName);
@@ -220,67 +150,50 @@ function isStableHookValue(init, node, identifierName, stableHooks) {
220
150
  return true;
221
151
  return isStableArrayIndex(stableResult, node, identifierName);
222
152
  }
223
- /**
224
- * Checks if a value is stable (doesn't need to be in dependencies).
225
- *
226
- * @param variable - The variable to check.
227
- * @param identifierName - The identifier name being accessed.
228
- * @param stableHooks - Map of stable hooks.
229
- * @returns True if the value is stable.
230
- */
231
- /* eslint-disable jsdoc/require-param, jsdoc/require-returns */
232
153
  function isStableValue(variable, identifierName, stableHooks) {
233
154
  if (!variable)
234
155
  return false;
235
- const { defs } = variable;
236
- if (defs.length === 0)
156
+ const definitions = variable.defs;
157
+ if (definitions.length === 0)
237
158
  return false;
238
- for (const def of defs) {
239
- const { node, type } = def;
240
- // Imports, functions, classes are stable
159
+ for (const definition of definitions) {
160
+ const { node, type } = definition;
241
161
  if (STABLE_VALUE_TYPES.has(type))
242
162
  return true;
243
- // Check for const declarations with constant initializers
244
- if (type === "Variable" && node.type === "VariableDeclarator") {
163
+ if (type === "Variable" && node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator) {
245
164
  const parent = node.parent;
246
- if (!parent || parent.type !== "VariableDeclaration" || parent.kind !== "const")
165
+ if (!parent || parent.type !== TSESTree.AST_NODE_TYPES.VariableDeclaration || parent.kind !== "const")
247
166
  continue;
248
- const { init } = node;
249
- // Check if it's from a stable hook first
250
- // @ts-expect-error - Type mismatch between ESLint and TypeScript AST types
167
+ const init = node.init;
251
168
  if (init && isStableHookValue(init, node, identifierName, stableHooks))
252
169
  return true;
253
- // Check for React Lua bindings - bindings are always stable
254
- if (init?.type === "CallExpression") {
170
+ if (init?.type === TSESTree.AST_NODE_TYPES.CallExpression) {
255
171
  const { callee } = init;
256
- // React.joinBindings() returns a stable binding
257
- if (callee.type === "MemberExpression" &&
258
- callee.object.type === "Identifier" &&
172
+ if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
173
+ callee.object.type === TSESTree.AST_NODE_TYPES.Identifier &&
259
174
  callee.object.name === "React" &&
260
- callee.property.type === "Identifier" &&
175
+ callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
261
176
  callee.property.name === "joinBindings")
262
177
  return true;
263
- // .map() on bindings returns a stable binding
264
- // This covers: binding.map(...), React.joinBindings(...).map(...), etc.
265
- if (callee.type === "MemberExpression" &&
266
- callee.property.type === "Identifier" &&
178
+ if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
179
+ callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
267
180
  callee.property.name === "map")
268
181
  return true;
269
182
  }
270
- // Check for literal values FIRST (stable regardless of scope)
271
183
  if (init) {
272
- if (init.type === "Literal" || init.type === "TemplateLiteral")
184
+ if (init.type === TSESTree.AST_NODE_TYPES.Literal ||
185
+ init.type === TSESTree.AST_NODE_TYPES.TemplateLiteral)
273
186
  return true;
274
- if (init.type === "UnaryExpression" && init.argument.type === "Literal")
187
+ if (init.type === TSESTree.AST_NODE_TYPES.UnaryExpression &&
188
+ init.argument.type === TSESTree.AST_NODE_TYPES.Literal)
275
189
  return true;
276
190
  }
277
- // For non-literal constants, only module-level is stable
278
- // Component-scoped non-literal constants are recreated on every render
279
- const varDef = variable.defs.find((d) => d.node === node);
280
- if (varDef && varDef.node.type === "VariableDeclarator") {
281
- const declParent = varDef.node.parent?.parent;
282
- // Module-level (Program or ExportNamedDeclaration)
283
- if (declParent && (declParent.type === "Program" || declParent.type === "ExportNamedDeclaration")) {
191
+ const variableDefinition = variable.defs.find((definition) => definition.node === node);
192
+ if (variableDefinition && variableDefinition.node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator) {
193
+ const declarationParent = variableDefinition.node.parent?.parent;
194
+ if (declarationParent &&
195
+ (declarationParent.type === TSESTree.AST_NODE_TYPES.Program ||
196
+ declarationParent.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration)) {
284
197
  testingMetrics.moduleLevelStableConst += 1;
285
198
  return true;
286
199
  }
@@ -289,17 +202,10 @@ function isStableValue(variable, identifierName, stableHooks) {
289
202
  }
290
203
  return false;
291
204
  }
292
- /**
293
- * Finds the topmost member expression in a chain when an identifier is the object.
294
- *
295
- * @param node - The identifier node.
296
- * @returns The topmost member expression or the node itself.
297
- */
298
205
  function findTopmostMemberExpression(node) {
299
206
  let current = node;
300
- let parent = node.parent;
301
- // Walk up the tree while we're the object of a member expression
302
- while (parent?.type === "MemberExpression" && parent.object === current) {
207
+ let { parent } = node;
208
+ while (parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression && parent.object === current) {
303
209
  current = parent;
304
210
  parent = parent.parent;
305
211
  }
@@ -311,57 +217,32 @@ const IS_CEASE_BOUNDARY = new Set([
311
217
  TSESTree.AST_NODE_TYPES.ArrowFunctionExpression,
312
218
  TSESTree.AST_NODE_TYPES.VariableDeclarator,
313
219
  ]);
314
- /**
315
- * Checks if an identifier is in a TypeScript type-only position.
316
- * Type parameters and type annotations are compile-time only and should not be dependencies.
317
- *
318
- * @param identifier - The identifier node to check.
319
- * @returns True if the identifier is in a type-only position.
320
- */
321
220
  function isInTypePosition(identifier) {
322
221
  let parent = identifier.parent;
323
222
  while (parent) {
324
- // Any TypeScript-specific node indicates a type-only position
325
223
  if (parent.type.startsWith("TS"))
326
224
  return true;
327
- // Stop searching at certain boundaries
328
225
  if (IS_CEASE_BOUNDARY.has(parent.type))
329
226
  return false;
330
227
  parent = parent.parent;
331
228
  }
332
229
  return false;
333
230
  }
334
- /**
335
- * Checks if a variable is declared directly in the component/hook body OR is a prop (parameter).
336
- * Per React rules, only variables "declared directly inside the component body" are reactive.
337
- * This includes props (function parameters).
338
- * Variables from outer scopes (module-level, parent functions) are non-reactive.
339
- *
340
- * @param variable - The variable to check.
341
- * @param closureNode - The closure node (useEffect callback, etc.).
342
- * @returns True if the variable is declared in the component body or is a prop.
343
- */
344
231
  function isDeclaredInComponentBody(variable, closureNode) {
345
- // Find the parent component/hook function
346
232
  let parent = closureNode.parent;
347
233
  while (parent) {
348
234
  const isFunction = FUNCTION_DECLARATIONS.has(parent.type);
349
235
  if (isFunction) {
350
- // Capture parent in a const so TypeScript understands it's stable in closures
351
236
  const functionParent = parent;
352
- // Check if variable is a parameter of this function (props)
353
- const isParameter = variable.defs.some((def) => {
354
- if (def.type !== "Parameter")
237
+ const isParameter = variable.defs.some((definition) => {
238
+ if (definition.type !== "Parameter")
355
239
  return false;
356
- // For parameters, the def.node is the function itself
357
- // Just check if the definition's node is the current function parent
358
- return def.node === functionParent;
240
+ return definition.node === functionParent;
359
241
  });
360
242
  if (isParameter)
361
- return true; // Props are reactive
362
- // Check if variable is defined inside this function
363
- return variable.defs.some((def) => {
364
- let node = def.node.parent;
243
+ return true;
244
+ return variable.defs.some((definition) => {
245
+ let node = definition.node.parent;
365
246
  while (node && node !== functionParent)
366
247
  node = node.parent;
367
248
  return node === functionParent;
@@ -371,15 +252,7 @@ function isDeclaredInComponentBody(variable, closureNode) {
371
252
  }
372
253
  return false;
373
254
  }
374
- /**
375
- * Resolves an identifier to its function definition if it references a function.
376
- *
377
- * @param identifier - The identifier node.
378
- * @param scope - The scope to search in.
379
- * @returns The function node if found, undefined otherwise.
380
- */
381
255
  function resolveFunctionReference(identifier, scope) {
382
- // Look up the variable in the scope chain
383
256
  let variable;
384
257
  let currentScope = scope;
385
258
  while (currentScope) {
@@ -390,49 +263,26 @@ function resolveFunctionReference(identifier, scope) {
390
263
  }
391
264
  if (!variable || variable.defs.length === 0)
392
265
  return undefined;
393
- // Check all definitions for a function
394
- for (const def of variable.defs) {
395
- const { node } = def;
396
- // Direct function declaration
397
- if (node.type === "FunctionDeclaration")
266
+ for (const definition of variable.defs) {
267
+ const { node } = definition;
268
+ if (node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration)
398
269
  return node;
399
- // Variable declarator with function initializer
400
- if (node.type === "VariableDeclarator" &&
270
+ if (node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator &&
401
271
  node.init &&
402
- (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression"))
272
+ (node.init.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression ||
273
+ node.init.type === TSESTree.AST_NODE_TYPES.FunctionExpression))
403
274
  return node.init;
404
275
  }
405
276
  return undefined;
406
277
  }
407
- /**
408
- * Collects all captured identifiers from a closure.
409
- *
410
- * @param node - The closure node (function/arrow function).
411
- * @param scope - The scope of the closure.
412
- * @param sourceCode - The source code instance.
413
- * @returns Array of captured identifiers.
414
- */
415
278
  function collectCaptures(node, scope, sourceCode) {
416
279
  const captures = new Array();
417
280
  const captureSet = new Set();
418
- /**
419
- * Recursively visits nodes to find identifier references.
420
- *
421
- * @param current - The current node.
422
- */
423
281
  function visit(current) {
424
- if (current.type === "Identifier") {
282
+ if (current.type === TSESTree.AST_NODE_TYPES.Identifier) {
425
283
  const { name } = current;
426
- // Skip if already captured
427
- if (captureSet.has(name))
284
+ if (captureSet.has(name) || GLOBAL_BUILTINS.has(name) || isInTypePosition(current))
428
285
  return;
429
- // Skip global built-ins (always stable, never need to be in dependencies)
430
- if (GLOBAL_BUILTINS.has(name))
431
- return;
432
- // Skip TypeScript type-only positions (type parameters, annotations, etc.)
433
- if (isInTypePosition(current))
434
- return;
435
- // Look up the variable in the scope chain
436
286
  let variable;
437
287
  let currentScope = scope;
438
288
  while (currentScope) {
@@ -441,24 +291,20 @@ function collectCaptures(node, scope, sourceCode) {
441
291
  break;
442
292
  currentScope = currentScope.upper;
443
293
  }
444
- // Only capture if variable is defined outside the closure
445
294
  if (variable) {
446
- const isDefinedInClosure = variable.defs.some((def) => {
447
- let defNode = def.node;
448
- while (defNode) {
449
- if (defNode === node)
295
+ const isDefinedInClosure = variable.defs.some((definition) => {
296
+ let definitionNode = definition.node;
297
+ while (definitionNode) {
298
+ if (definitionNode === node)
450
299
  return true;
451
- defNode = defNode.parent;
300
+ definitionNode = definitionNode.parent;
452
301
  }
453
302
  return false;
454
303
  });
455
304
  if (!isDefinedInClosure) {
456
- // Only capture variables declared in the component body
457
- // Per React rules, only "variables declared directly inside the component body" are reactive
458
- // Variables from outer scopes (module-level, parent functions) are non-reactive and stable
459
305
  if (!isDeclaredInComponentBody(variable, node)) {
460
306
  testingMetrics.outerScopeSkip += 1;
461
- return; // From outer scope - skip
307
+ return;
462
308
  }
463
309
  captureSet.add(name);
464
310
  const depthNode = findTopmostMemberExpression(current);
@@ -473,23 +319,20 @@ function collectCaptures(node, scope, sourceCode) {
473
319
  }
474
320
  }
475
321
  }
476
- // Unwrap TypeScript type expressions to visit the actual expression
477
- if (current.type === "TSSatisfiesExpression" ||
478
- current.type === "TSAsExpression" ||
479
- current.type === "TSTypeAssertion" ||
480
- current.type === "TSNonNullExpression") {
322
+ if (current.type === TSESTree.AST_NODE_TYPES.TSSatisfiesExpression ||
323
+ current.type === TSESTree.AST_NODE_TYPES.TSAsExpression ||
324
+ current.type === TSESTree.AST_NODE_TYPES.TSTypeAssertion ||
325
+ current.type === TSESTree.AST_NODE_TYPES.TSNonNullExpression) {
481
326
  visit(current.expression);
482
327
  return;
483
328
  }
484
- // Traverse member expressions
485
- if (current.type === "MemberExpression") {
329
+ if (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
486
330
  visit(current.object);
487
331
  if (current.computed)
488
332
  visit(current.property);
489
333
  return;
490
334
  }
491
- // Visit children
492
- const keys = sourceCode.visitorKeys?.[current.type] || [];
335
+ const keys = sourceCode.visitorKeys?.[current.type] ?? [];
493
336
  for (const key of keys) {
494
337
  const value = current[key];
495
338
  if (Array.isArray(value)) {
@@ -504,17 +347,10 @@ function collectCaptures(node, scope, sourceCode) {
504
347
  visit(node);
505
348
  return captures;
506
349
  }
507
- /**
508
- * Parses dependencies from a dependency array expression.
509
- *
510
- * @param node - The dependency array node.
511
- * @param sourceCode - The source code instance.
512
- * @returns Array of dependency information.
513
- */
514
350
  function parseDependencies(node, sourceCode) {
515
351
  const dependencies = new Array();
516
352
  for (const element of node.elements) {
517
- if (!element || element.type === "SpreadElement")
353
+ if (!element || element.type === TSESTree.AST_NODE_TYPES.SpreadElement)
518
354
  continue;
519
355
  const name = nodeToDependencyString(element, sourceCode);
520
356
  const depth = getMemberExpressionDepth(element);
@@ -526,23 +362,14 @@ function parseDependencies(node, sourceCode) {
526
362
  }
527
363
  return dependencies;
528
364
  }
529
- /**
530
- * Checks if a dependency or capture is an inline function or object (unstable).
531
- *
532
- * @param node - The node to check.
533
- * @returns True if the node is an unstable value.
534
- */
365
+ function returnName({ name }) {
366
+ return name;
367
+ }
535
368
  function isUnstableValue(node) {
536
369
  return node ? UNSTABLE_VALUES.has(node.type) : false;
537
370
  }
538
371
  const isNumberArray = Compile(Type.Array(Type.Number(), { minItems: 1, readOnly: true }));
539
372
  const isStringArray = Compile(Type.Array(Type.String(), { minItems: 1, readOnly: true }));
540
- /**
541
- * Converts stableResult configuration to internal format.
542
- *
543
- * @param stableResult - The stable result configuration.
544
- * @returns The internal stable result format.
545
- */
546
373
  function convertStableResult(stableResult) {
547
374
  if (typeof stableResult === "boolean")
548
375
  return stableResult;
@@ -562,31 +389,22 @@ const useExhaustiveDependencies = {
562
389
  reportUnnecessaryDependencies: true,
563
390
  ...context.options[0],
564
391
  };
565
- // Build hook configuration map
566
392
  const hookConfigs = new Map(DEFAULT_HOOKS);
567
393
  for (const customHook of options.hooks) {
568
- if (customHook.closureIndex !== undefined && customHook.dependenciesIndex !== undefined) {
569
- hookConfigs.set(customHook.name, {
570
- closureIndex: customHook.closureIndex,
571
- dependenciesIndex: customHook.dependenciesIndex,
572
- });
573
- }
394
+ if (customHook.closureIndex === undefined || customHook.dependenciesIndex === undefined)
395
+ continue;
396
+ hookConfigs.set(customHook.name, {
397
+ closureIndex: customHook.closureIndex,
398
+ dependenciesIndex: customHook.dependenciesIndex,
399
+ });
574
400
  }
575
- // Build stable hooks map
576
401
  const stableHooks = new Map(STABLE_HOOKS);
577
402
  for (const customHook of options.hooks) {
578
403
  if (customHook.stableResult === undefined)
579
404
  continue;
580
405
  stableHooks.set(customHook.name, convertStableResult(customHook.stableResult));
581
406
  }
582
- // Performance: cache scope lookups
583
407
  const scopeCache = new WeakMap();
584
- /**
585
- * Gets the scope for a node with caching.
586
- *
587
- * @param node - The node to get scope for.
588
- * @returns The scope.
589
- */
590
408
  function getScope(node) {
591
409
  const cached = scopeCache.get(node);
592
410
  if (cached)
@@ -598,59 +416,49 @@ const useExhaustiveDependencies = {
598
416
  return {
599
417
  CallExpression(node) {
600
418
  const callNode = node;
601
- // Early exit: get hook name
602
419
  const hookName = getHookName(callNode);
603
- if (!hookName)
420
+ if (hookName === undefined || hookName === "")
604
421
  return;
605
- // Early exit: check if this hook needs dependency checking
606
422
  const hookConfig = hookConfigs.get(hookName);
607
423
  if (!hookConfig)
608
424
  return;
609
425
  const { closureIndex, dependenciesIndex } = hookConfig;
610
- const { arguments: args } = callNode;
611
- // Early exit: check if closure argument exists
612
- const closureArg = args[closureIndex];
613
- if (!closureArg)
426
+ const parameters = callNode.arguments;
427
+ const closureArgument = parameters[closureIndex];
428
+ if (closureArgument === undefined)
614
429
  return;
615
- // Resolve the actual closure function (handles both inline and reference cases)
616
430
  let closureFunction;
617
- if (closureArg.type === "ArrowFunctionExpression" || closureArg.type === "FunctionExpression") {
618
- closureFunction = closureArg;
619
- }
620
- else if (closureArg.type === "Identifier") {
621
- // Function reference - try to resolve it
431
+ if (closureArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression ||
432
+ closureArgument.type === TSESTree.AST_NODE_TYPES.FunctionExpression)
433
+ closureFunction = closureArgument;
434
+ else if (closureArgument.type === TSESTree.AST_NODE_TYPES.Identifier) {
622
435
  const scope = getScope(callNode);
623
- closureFunction = resolveFunctionReference(closureArg, scope);
436
+ closureFunction = resolveFunctionReference(closureArgument, scope);
624
437
  }
625
- // Early exit: check if we have a valid closure function
626
438
  if (!closureFunction)
627
439
  return;
628
- // Get dependencies argument
629
- const dependenciesArgument = args[dependenciesIndex];
630
- // Report missing dependencies array if configured
440
+ const dependenciesArgument = parameters[dependenciesIndex];
631
441
  if (!dependenciesArgument && options.reportMissingDependenciesArray) {
632
- // Collect captures to see if any are needed
633
442
  const scope = getScope(closureFunction);
634
443
  const captures = collectCaptures(closureFunction, scope, context.sourceCode);
635
- // Filter out stable values
636
444
  const requiredCaptures = captures.filter((capture) => !isStableValue(capture.variable, capture.name, stableHooks));
637
445
  if (requiredCaptures.length > 0) {
638
- const missingNames = Array.from(new Set(requiredCaptures.map(({ name }) => name))).join(", ");
639
- // Generate fix suggestion - add dependencies array
446
+ // oxlint-disable-next-line no-array-callback-reference
447
+ const missingNames = Array.from(new Set(requiredCaptures.map(returnName))).join(", ");
640
448
  const usagePaths = requiredCaptures.map(({ usagePath }) => usagePath);
641
- const uniqueDeps = Array.from(new Set(usagePaths)).toSorted();
642
- const depsArrayString = `[${uniqueDeps.join(", ")}]`;
449
+ const uniqueDependencies = Array.from(new Set(usagePaths)).toSorted();
450
+ const dependenciesString = `[${uniqueDependencies.join(", ")}]`;
643
451
  context.report({
644
452
  data: { deps: missingNames },
645
453
  messageId: "missingDependenciesArray",
646
454
  node: callNode,
647
455
  suggest: [
648
456
  {
649
- desc: `Add dependencies array: ${depsArrayString}`,
457
+ desc: `Add dependencies array: ${dependenciesString}`,
650
458
  fix(fixer) {
651
- // Insert the dependencies array after the closure argument
652
- const closureArgNode = args[closureIndex];
653
- return fixer.insertTextAfter(closureArgNode, `, ${depsArrayString}`);
459
+ //
460
+ const closureArgumentNode = parameters[closureIndex];
461
+ return fixer.insertTextAfter(closureArgumentNode, `, ${dependenciesString}`);
654
462
  },
655
463
  },
656
464
  ],
@@ -658,32 +466,27 @@ const useExhaustiveDependencies = {
658
466
  }
659
467
  return;
660
468
  }
661
- // Early exit: no dependencies array
662
469
  if (!dependenciesArgument)
663
470
  return;
664
- // Dependencies must be an array
665
- if (dependenciesArgument.type !== "ArrayExpression")
471
+ if (dependenciesArgument.type !== TSESTree.AST_NODE_TYPES.ArrayExpression)
666
472
  return;
667
- const depsArray = dependenciesArgument;
668
- // Collect captures from closure
473
+ const dependenciesArray = dependenciesArgument;
669
474
  const scope = getScope(closureFunction);
670
475
  const captures = collectCaptures(closureFunction, scope, context.sourceCode);
671
- // Parse dependencies array
672
- const dependencies = parseDependencies(depsArray, context.sourceCode);
673
- // Check for unnecessary dependencies first (for consistent error ordering)
476
+ const dependencies = parseDependencies(dependenciesArray, context.sourceCode);
674
477
  for (const dependency of dependencies) {
675
478
  const dependencyRootIdentifier = getRootIdentifier(dependency.node);
676
479
  if (!dependencyRootIdentifier)
677
480
  continue;
678
481
  const dependencyName = dependencyRootIdentifier.name;
679
- // Find all captures with the same root identifier
680
482
  const matchingCaptures = captures.filter(({ node }) => getRootIdentifier(node)?.name === dependencyName);
681
- // If no captures use this identifier at all, it's unnecessary
682
483
  if (matchingCaptures.length === 0) {
683
484
  if (options.reportUnnecessaryDependencies) {
684
- // Generate fix suggestion
685
- const newDeps = dependencies.filter((d) => d.name !== dependency.name).map((d) => d.name);
686
- const newDepsString = `[${newDeps.join(", ")}]`;
485
+ const newDependencies = dependencies
486
+ .filter((value) => value.name !== dependency.name)
487
+ // oxlint-disable-next-line no-array-callback-reference
488
+ .map(returnName);
489
+ const dependenciesString = `[${newDependencies.join(", ")}]`;
687
490
  context.report({
688
491
  data: { name: dependency.name },
689
492
  messageId: "unnecessaryDependency",
@@ -692,7 +495,7 @@ const useExhaustiveDependencies = {
692
495
  {
693
496
  desc: `Remove '${dependency.name}' from dependencies array`,
694
497
  fix(fixer) {
695
- return fixer.replaceText(depsArray, newDepsString);
498
+ return fixer.replaceText(dependenciesArray, dependenciesString);
696
499
  },
697
500
  },
698
501
  ],
@@ -700,13 +503,13 @@ const useExhaustiveDependencies = {
700
503
  }
701
504
  continue;
702
505
  }
703
- // Check if dependency is more specific than any usage
704
- // dep.depth > all capture depths means the dep is too specific
705
- const maxCaptureDepth = Math.max(...matchingCaptures.map((c) => c.depth));
506
+ const maxCaptureDepth = Math.max(...matchingCaptures.map(({ depth }) => depth));
706
507
  if (dependency.depth > maxCaptureDepth && options.reportUnnecessaryDependencies) {
707
- // Generate fix suggestion
708
- const newDeps = dependencies.filter((d) => d.name !== dependency.name).map((d) => d.name);
709
- const newDepsString = `[${newDeps.join(", ")}]`;
508
+ const newDependencies = dependencies
509
+ .filter(({ name }) => name !== dependency.name)
510
+ // oxlint-disable-next-line no-array-callback-reference
511
+ .map(returnName);
512
+ const dependencyString = `[${newDependencies.join(", ")}]`;
710
513
  context.report({
711
514
  data: { name: dependency.name },
712
515
  messageId: "unnecessaryDependency",
@@ -715,29 +518,24 @@ const useExhaustiveDependencies = {
715
518
  {
716
519
  desc: `Remove '${dependency.name}' from dependencies array`,
717
520
  fix(fixer) {
718
- return fixer.replaceText(depsArray, newDepsString);
521
+ return fixer.replaceText(dependenciesArray, dependencyString);
719
522
  },
720
523
  },
721
524
  ],
722
525
  });
723
526
  }
724
527
  }
725
- // Check for missing dependencies
726
528
  const missingCaptures = new Array();
727
529
  for (const capture of captures) {
728
- // Skip stable values
729
530
  if (isStableValue(capture.variable, capture.name, stableHooks))
730
531
  continue;
731
- // Check if the capture is in the dependencies
732
532
  const rootIdentifier = getRootIdentifier(capture.node);
733
533
  if (!rootIdentifier)
734
534
  continue;
735
535
  const captureName = rootIdentifier.name;
736
536
  let isInDependencies = false;
737
- // Check if capture is covered by dependencies
738
537
  for (const dependency of dependencies) {
739
538
  const dependencyRootIdentifier = getRootIdentifier(dependency.node);
740
- // Check name match and depth: dependency should not be more specific than capture
741
539
  if (dependencyRootIdentifier?.name === captureName && dependency.depth <= capture.depth) {
742
540
  isInDependencies = true;
743
541
  break;
@@ -746,7 +544,6 @@ const useExhaustiveDependencies = {
746
544
  if (!isInDependencies)
747
545
  missingCaptures.push(capture);
748
546
  }
749
- // Report all missing dependencies at once
750
547
  if (missingCaptures.length > 0) {
751
548
  const dependencyNames = dependencies.map(({ name }) => name);
752
549
  const missingPaths = missingCaptures.map(({ usagePath }) => usagePath);
@@ -754,51 +551,45 @@ const useExhaustiveDependencies = {
754
551
  const newDependenciesString = `[${newDependencies.join(", ")}]`;
755
552
  const lastDependency = dependencies.at(-1);
756
553
  const firstMissing = missingCaptures.at(0);
757
- // For single missing dependency, use singular message for backward compat
758
554
  if (missingCaptures.length === 1 && firstMissing) {
759
555
  context.report({
760
556
  data: { name: firstMissing.usagePath },
761
557
  messageId: "missingDependency",
762
- node: lastDependency?.node || depsArray,
558
+ node: lastDependency?.node || dependenciesArray,
763
559
  suggest: [
764
560
  {
765
561
  desc: `Add '${firstMissing.usagePath}' to dependencies array`,
766
562
  fix(fixer) {
767
- return fixer.replaceText(depsArray, newDependenciesString);
563
+ return fixer.replaceText(dependenciesArray, newDependenciesString);
768
564
  },
769
565
  },
770
566
  ],
771
567
  });
772
568
  }
773
569
  else {
774
- // For multiple missing dependencies, use plural message
775
570
  const missingNames = missingPaths.join(", ");
776
571
  context.report({
777
572
  data: { names: missingNames },
778
573
  messageId: "missingDependencies",
779
- node: lastDependency?.node || depsArray,
574
+ node: lastDependency?.node || dependenciesArray,
780
575
  suggest: [
781
576
  {
782
577
  desc: "Add missing dependencies to array",
783
578
  fix(fixer) {
784
- return fixer.replaceText(depsArray, newDependenciesString);
579
+ return fixer.replaceText(dependenciesArray, newDependenciesString);
785
580
  },
786
581
  },
787
582
  ],
788
583
  });
789
584
  }
790
585
  }
791
- // Check for unstable dependencies in the array
792
586
  for (const capture of captures) {
793
- // Skip stable values
794
587
  if (isStableValue(capture.variable, capture.name, stableHooks))
795
588
  continue;
796
- // Check if this capture has a corresponding dependency
797
589
  const rootIdentifier = getRootIdentifier(capture.node);
798
590
  if (!rootIdentifier)
799
591
  continue;
800
592
  const captureName = rootIdentifier.name;
801
- // Find if there's a matching dependency
802
593
  for (const dependency of dependencies) {
803
594
  const dependencyRootIdentifier = getRootIdentifier(dependency.node);
804
595
  const isMatch = dependencyRootIdentifier?.name === captureName && dependency.depth === capture.depth;