@pobammer-ts/eslint-cease-nonsense-rules 0.4.1 → 0.4.3

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.
@@ -1,9 +1,26 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
2
- /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
3
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
5
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
6
- /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
1
+ import { TSESTree } from "@typescript-eslint/types";
2
+ import * as S from "sury";
3
+ const FUNCTION_DECLARATIONS = new Set([
4
+ TSESTree.AST_NODE_TYPES.FunctionExpression,
5
+ TSESTree.AST_NODE_TYPES.ArrowFunctionExpression,
6
+ TSESTree.AST_NODE_TYPES.FunctionDeclaration,
7
+ ]);
8
+ const UNSTABLE_VALUES = new Set([
9
+ ...FUNCTION_DECLARATIONS,
10
+ TSESTree.AST_NODE_TYPES.ObjectExpression,
11
+ TSESTree.AST_NODE_TYPES.ArrayExpression,
12
+ ]);
13
+ /**
14
+ * Internal metrics used for testing to ensure specific branches execute.
15
+ */
16
+ const testingMetrics = {
17
+ moduleLevelStableConst: 0,
18
+ outerScopeSkip: 0,
19
+ };
20
+ function resetTestingMetrics() {
21
+ testingMetrics.moduleLevelStableConst = 0;
22
+ testingMetrics.outerScopeSkip = 0;
23
+ }
7
24
  /**
8
25
  * Default hooks to check for exhaustive dependencies.
9
26
  */
@@ -14,6 +31,12 @@ const DEFAULT_HOOKS = new Map([
14
31
  ["useCallback", { closureIndex: 0, dependenciesIndex: 1 }],
15
32
  ["useMemo", { closureIndex: 0, dependenciesIndex: 1 }],
16
33
  ["useImperativeHandle", { closureIndex: 1, dependenciesIndex: 2 }],
34
+ // React Spring hooks (function factory pattern)
35
+ // Note: These hooks support both function and object patterns.
36
+ // Only the function pattern is analyzed for dependencies.
37
+ ["useSpring", { closureIndex: 0, dependenciesIndex: 1 }],
38
+ ["useSprings", { closureIndex: 1, dependenciesIndex: 2 }],
39
+ ["useTrail", { closureIndex: 1, dependenciesIndex: 2 }],
17
40
  ]);
18
41
  /**
19
42
  * Hooks with stable results that don't need to be in dependencies.
@@ -127,7 +150,7 @@ function getMemberExpressionDepth(node) {
127
150
  let depth = 0;
128
151
  let current = node;
129
152
  while (current.type === "MemberExpression") {
130
- depth++;
153
+ depth += 1;
131
154
  current = current.object;
132
155
  }
133
156
  return depth;
@@ -140,9 +163,8 @@ function getMemberExpressionDepth(node) {
140
163
  */
141
164
  function getRootIdentifier(node) {
142
165
  let current = node;
143
- while (current.type === "MemberExpression") {
166
+ while (current.type === "MemberExpression")
144
167
  current = current.object;
145
- }
146
168
  return current.type === "Identifier" ? current : undefined;
147
169
  }
148
170
  /**
@@ -166,15 +188,13 @@ function nodeToDependencyString(node, sourceCode) {
166
188
  function isStableArrayIndex(stableResult, node, identifierName) {
167
189
  if (!stableResult)
168
190
  return false;
169
- if (!(stableResult instanceof Set) || node.type !== "VariableDeclarator" || node.id.type !== "ArrayPattern") {
191
+ if (!(stableResult instanceof Set) || node.type !== "VariableDeclarator" || node.id.type !== "ArrayPattern")
170
192
  return false;
171
- }
172
193
  const elements = node.id.elements;
173
- for (let i = 0; i < elements.length; i++) {
174
- const element = elements[i];
175
- if (element && element.type === "Identifier" && element.name === identifierName) {
176
- return stableResult.has(i);
177
- }
194
+ for (let index = 0; index < elements.length; index += 1) {
195
+ const element = elements[index];
196
+ if (element?.type === "Identifier" && element.name === identifierName)
197
+ return stableResult.has(index);
178
198
  }
179
199
  return false;
180
200
  }
@@ -222,15 +242,13 @@ function isStableValue(variable, identifierName, stableHooks) {
222
242
  // Check for const declarations with constant initializers
223
243
  if (type === "Variable" && node.type === "VariableDeclarator") {
224
244
  const parent = node.parent;
225
- if (!parent || parent.type !== "VariableDeclaration" || parent.kind !== "const") {
245
+ if (!parent || parent.type !== "VariableDeclaration" || parent.kind !== "const")
226
246
  continue;
227
- }
228
247
  const { init } = node;
229
248
  // Check if it's from a stable hook first
230
249
  // @ts-expect-error - Type mismatch between ESLint and TypeScript AST types
231
- if (init && isStableHookValue(init, node, identifierName, stableHooks)) {
250
+ if (init && isStableHookValue(init, node, identifierName, stableHooks))
232
251
  return true;
233
- }
234
252
  // Check for React Lua bindings - bindings are always stable
235
253
  if (init?.type === "CallExpression") {
236
254
  const { callee } = init;
@@ -239,25 +257,21 @@ function isStableValue(variable, identifierName, stableHooks) {
239
257
  callee.object.type === "Identifier" &&
240
258
  callee.object.name === "React" &&
241
259
  callee.property.type === "Identifier" &&
242
- callee.property.name === "joinBindings") {
260
+ callee.property.name === "joinBindings")
243
261
  return true;
244
- }
245
262
  // .map() on bindings returns a stable binding
246
263
  // This covers: binding.map(...), React.joinBindings(...).map(...), etc.
247
264
  if (callee.type === "MemberExpression" &&
248
265
  callee.property.type === "Identifier" &&
249
- callee.property.name === "map") {
266
+ callee.property.name === "map")
250
267
  return true;
251
- }
252
268
  }
253
269
  // Check for literal values FIRST (stable regardless of scope)
254
270
  if (init) {
255
- if (init.type === "Literal" || init.type === "TemplateLiteral") {
271
+ if (init.type === "Literal" || init.type === "TemplateLiteral")
256
272
  return true;
257
- }
258
- if (init.type === "UnaryExpression" && init.argument.type === "Literal") {
273
+ if (init.type === "UnaryExpression" && init.argument.type === "Literal")
259
274
  return true;
260
- }
261
275
  }
262
276
  // For non-literal constants, only module-level is stable
263
277
  // Component-scoped non-literal constants are recreated on every render
@@ -266,6 +280,7 @@ function isStableValue(variable, identifierName, stableHooks) {
266
280
  const declParent = varDef.node.parent?.parent;
267
281
  // Module-level (Program or ExportNamedDeclaration)
268
282
  if (declParent && (declParent.type === "Program" || declParent.type === "ExportNamedDeclaration")) {
283
+ testingMetrics.moduleLevelStableConst += 1;
269
284
  return true;
270
285
  }
271
286
  }
@@ -289,6 +304,12 @@ function findTopmostMemberExpression(node) {
289
304
  }
290
305
  return current;
291
306
  }
307
+ const IS_CEASE_BOUNDARY = new Set([
308
+ TSESTree.AST_NODE_TYPES.FunctionDeclaration,
309
+ TSESTree.AST_NODE_TYPES.FunctionExpression,
310
+ TSESTree.AST_NODE_TYPES.ArrowFunctionExpression,
311
+ TSESTree.AST_NODE_TYPES.VariableDeclarator,
312
+ ]);
292
313
  /**
293
314
  * Checks if an identifier is in a TypeScript type-only position.
294
315
  * Type parameters and type annotations are compile-time only and should not be dependencies.
@@ -300,16 +321,11 @@ function isInTypePosition(identifier) {
300
321
  let parent = identifier.parent;
301
322
  while (parent) {
302
323
  // Any TypeScript-specific node indicates a type-only position
303
- if (parent.type.startsWith("TS")) {
324
+ if (parent.type.startsWith("TS"))
304
325
  return true;
305
- }
306
326
  // Stop searching at certain boundaries
307
- if (parent.type === "FunctionDeclaration" ||
308
- parent.type === "FunctionExpression" ||
309
- parent.type === "ArrowFunctionExpression" ||
310
- parent.type === "VariableDeclarator") {
327
+ if (IS_CEASE_BOUNDARY.has(parent.type))
311
328
  return false;
312
- }
313
329
  parent = parent.parent;
314
330
  }
315
331
  return false;
@@ -328,9 +344,7 @@ function isDeclaredInComponentBody(variable, closureNode) {
328
344
  // Find the parent component/hook function
329
345
  let parent = closureNode.parent;
330
346
  while (parent) {
331
- const isFunction = parent.type === "FunctionDeclaration" ||
332
- parent.type === "FunctionExpression" ||
333
- parent.type === "ArrowFunctionExpression";
347
+ const isFunction = FUNCTION_DECLARATIONS.has(parent.type);
334
348
  if (isFunction) {
335
349
  // Capture parent in a const so TypeScript understands it's stable in closures
336
350
  const functionParent = parent;
@@ -342,15 +356,13 @@ function isDeclaredInComponentBody(variable, closureNode) {
342
356
  // Just check if the definition's node is the current function parent
343
357
  return def.node === functionParent;
344
358
  });
345
- if (isParameter) {
359
+ if (isParameter)
346
360
  return true; // Props are reactive
347
- }
348
361
  // Check if variable is defined inside this function
349
362
  return variable.defs.some((def) => {
350
363
  let node = def.node.parent;
351
- while (node && node !== functionParent) {
364
+ while (node && node !== functionParent)
352
365
  node = node.parent;
353
- }
354
366
  return node === functionParent;
355
367
  });
356
368
  }
@@ -358,6 +370,39 @@ function isDeclaredInComponentBody(variable, closureNode) {
358
370
  }
359
371
  return false;
360
372
  }
373
+ /**
374
+ * Resolves an identifier to its function definition if it references a function.
375
+ *
376
+ * @param identifier - The identifier node.
377
+ * @param scope - The scope to search in.
378
+ * @returns The function node if found, undefined otherwise.
379
+ */
380
+ function resolveFunctionReference(identifier, scope) {
381
+ // Look up the variable in the scope chain
382
+ let variable;
383
+ let currentScope = scope;
384
+ while (currentScope) {
385
+ variable = currentScope.set.get(identifier.name);
386
+ if (variable)
387
+ break;
388
+ currentScope = currentScope.upper;
389
+ }
390
+ if (!variable || variable.defs.length === 0)
391
+ return undefined;
392
+ // Check all definitions for a function
393
+ for (const def of variable.defs) {
394
+ const { node } = def;
395
+ // Direct function declaration
396
+ if (node.type === "FunctionDeclaration")
397
+ return node;
398
+ // Variable declarator with function initializer
399
+ if (node.type === "VariableDeclarator" &&
400
+ node.init &&
401
+ (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression"))
402
+ return node.init;
403
+ }
404
+ return undefined;
405
+ }
361
406
  /**
362
407
  * Collects all captured identifiers from a closure.
363
408
  *
@@ -367,7 +412,7 @@ function isDeclaredInComponentBody(variable, closureNode) {
367
412
  * @returns Array of captured identifiers.
368
413
  */
369
414
  function collectCaptures(node, scope, sourceCode) {
370
- const captures = [];
415
+ const captures = new Array();
371
416
  const captureSet = new Set();
372
417
  /**
373
418
  * Recursively visits nodes to find identifier references.
@@ -411,6 +456,7 @@ function collectCaptures(node, scope, sourceCode) {
411
456
  // Per React rules, only "variables declared directly inside the component body" are reactive
412
457
  // Variables from outer scopes (module-level, parent functions) are non-reactive and stable
413
458
  if (!isDeclaredInComponentBody(variable, node)) {
459
+ testingMetrics.outerScopeSkip += 1;
414
460
  return; // From outer scope - skip
415
461
  }
416
462
  captureSet.add(name);
@@ -421,17 +467,24 @@ function collectCaptures(node, scope, sourceCode) {
421
467
  name,
422
468
  node: depthNode,
423
469
  usagePath,
424
- variable,
470
+ variable: variable,
425
471
  });
426
472
  }
427
473
  }
428
474
  }
475
+ // Unwrap TypeScript type expressions to visit the actual expression
476
+ if (current.type === "TSSatisfiesExpression" ||
477
+ current.type === "TSAsExpression" ||
478
+ current.type === "TSTypeAssertion" ||
479
+ current.type === "TSNonNullExpression") {
480
+ visit(current.expression);
481
+ return;
482
+ }
429
483
  // Traverse member expressions
430
484
  if (current.type === "MemberExpression") {
431
485
  visit(current.object);
432
- if (current.computed) {
486
+ if (current.computed)
433
487
  visit(current.property);
434
- }
435
488
  return;
436
489
  }
437
490
  // Visit children
@@ -439,15 +492,12 @@ function collectCaptures(node, scope, sourceCode) {
439
492
  for (const key of keys) {
440
493
  const value = current[key];
441
494
  if (Array.isArray(value)) {
442
- for (const item of value) {
443
- if (item && typeof item === "object" && "type" in item) {
495
+ for (const item of value)
496
+ if (item && typeof item === "object" && "type" in item)
444
497
  visit(item);
445
- }
446
- }
447
498
  }
448
- else if (value && typeof value === "object" && "type" in value) {
499
+ else if (value && typeof value === "object" && "type" in value)
449
500
  visit(value);
450
- }
451
501
  }
452
502
  }
453
503
  visit(node);
@@ -461,7 +511,7 @@ function collectCaptures(node, scope, sourceCode) {
461
511
  * @returns Array of dependency information.
462
512
  */
463
513
  function parseDependencies(node, sourceCode) {
464
- const dependencies = [];
514
+ const dependencies = new Array();
465
515
  for (const element of node.elements) {
466
516
  if (!element || element.type === "SpreadElement")
467
517
  continue;
@@ -482,23 +532,12 @@ function parseDependencies(node, sourceCode) {
482
532
  * @returns True if the node is an unstable value.
483
533
  */
484
534
  function isUnstableValue(node) {
485
- if (!node)
486
- return false;
487
- // Inline functions
488
- if (node.type === "FunctionExpression" ||
489
- node.type === "ArrowFunctionExpression" ||
490
- node.type === "FunctionDeclaration") {
491
- return true;
492
- }
493
- // Object literals
494
- if (node.type === "ObjectExpression") {
495
- return true;
496
- }
497
- // Array literals
498
- if (node.type === "ArrayExpression") {
499
- return true;
500
- }
501
- return false;
535
+ return node ? UNSTABLE_VALUES.has(node.type) : false;
536
+ }
537
+ const isNumberArray = S.array(S.number);
538
+ const isStringArray = S.array(S.string);
539
+ function validate(validator, value) {
540
+ return S.safe(() => S.parseOrThrow(value, validator)).success;
502
541
  }
503
542
  /**
504
543
  * Converts stableResult configuration to internal format.
@@ -511,12 +550,10 @@ function convertStableResult(stableResult) {
511
550
  return stableResult;
512
551
  if (typeof stableResult === "number")
513
552
  return new Set([stableResult]);
514
- if (Array.isArray(stableResult) && stableResult.length > 0) {
515
- if (typeof stableResult[0] === "number") {
516
- return new Set(stableResult);
517
- }
553
+ if (validate(isNumberArray, stableResult) && stableResult.length > 0)
554
+ return new Set(stableResult);
555
+ if (validate(isStringArray, stableResult) && stableResult.length > 0)
518
556
  return new Set(stableResult);
519
- }
520
557
  return false;
521
558
  }
522
559
  const useExhaustiveDependencies = {
@@ -577,56 +614,90 @@ const useExhaustiveDependencies = {
577
614
  const closureArg = args[closureIndex];
578
615
  if (!closureArg)
579
616
  return;
580
- // Early exit: check if closure is a function
581
- if (closureArg.type !== "ArrowFunctionExpression" && closureArg.type !== "FunctionExpression") {
582
- return;
617
+ // Resolve the actual closure function (handles both inline and reference cases)
618
+ let closureFunction;
619
+ if (closureArg.type === "ArrowFunctionExpression" || closureArg.type === "FunctionExpression") {
620
+ closureFunction = closureArg;
583
621
  }
622
+ else if (closureArg.type === "Identifier") {
623
+ // Function reference - try to resolve it
624
+ const scope = getScope(callNode);
625
+ closureFunction = resolveFunctionReference(closureArg, scope);
626
+ }
627
+ // Early exit: check if we have a valid closure function
628
+ if (!closureFunction)
629
+ return;
584
630
  // Get dependencies argument
585
- const depsArg = args[dependenciesIndex];
631
+ const dependenciesArgument = args[dependenciesIndex];
586
632
  // Report missing dependencies array if configured
587
- if (!depsArg && options.reportMissingDependenciesArray) {
633
+ if (!dependenciesArgument && options.reportMissingDependenciesArray) {
588
634
  // Collect captures to see if any are needed
589
- const scope = getScope(closureArg);
590
- const captures = collectCaptures(closureArg, scope, context.sourceCode);
635
+ const scope = getScope(closureFunction);
636
+ const captures = collectCaptures(closureFunction, scope, context.sourceCode);
591
637
  // Filter out stable values
592
638
  const requiredCaptures = captures.filter((capture) => !isStableValue(capture.variable, capture.name, stableHooks));
593
639
  if (requiredCaptures.length > 0) {
594
- const missingNames = Array.from(new Set(requiredCaptures.map((c) => c.name))).join(", ");
640
+ const missingNames = Array.from(new Set(requiredCaptures.map(({ name }) => name))).join(", ");
641
+ // Generate fix suggestion - add dependencies array
642
+ const usagePaths = requiredCaptures.map(({ usagePath }) => usagePath);
643
+ const uniqueDeps = Array.from(new Set(usagePaths)).toSorted();
644
+ const depsArrayString = `[${uniqueDeps.join(", ")}]`;
595
645
  context.report({
596
646
  data: { deps: missingNames },
597
647
  messageId: "missingDependenciesArray",
598
648
  node: callNode,
649
+ suggest: [
650
+ {
651
+ desc: `Add dependencies array: ${depsArrayString}`,
652
+ fix(fixer) {
653
+ // Insert the dependencies array after the closure argument
654
+ const closureArgNode = args[closureIndex];
655
+ return fixer.insertTextAfter(closureArgNode, `, ${depsArrayString}`);
656
+ },
657
+ },
658
+ ],
599
659
  });
600
660
  }
601
661
  return;
602
662
  }
603
663
  // Early exit: no dependencies array
604
- if (!depsArg)
664
+ if (!dependenciesArgument)
605
665
  return;
606
666
  // Dependencies must be an array
607
- if (depsArg.type !== "ArrayExpression")
667
+ if (dependenciesArgument.type !== "ArrayExpression")
608
668
  return;
609
- const depsArray = depsArg;
669
+ const depsArray = dependenciesArgument;
610
670
  // Collect captures from closure
611
- const scope = getScope(closureArg);
612
- const captures = collectCaptures(closureArg, scope, context.sourceCode);
671
+ const scope = getScope(closureFunction);
672
+ const captures = collectCaptures(closureFunction, scope, context.sourceCode);
613
673
  // Parse dependencies array
614
674
  const dependencies = parseDependencies(depsArray, context.sourceCode);
615
675
  // Check for unnecessary dependencies first (for consistent error ordering)
616
- for (const dep of dependencies) {
617
- const depRootIdent = getRootIdentifier(dep.node);
618
- if (!depRootIdent)
676
+ for (const dependency of dependencies) {
677
+ const dependencyRootIdentifier = getRootIdentifier(dependency.node);
678
+ if (!dependencyRootIdentifier)
619
679
  continue;
620
- const depName = depRootIdent.name;
680
+ const dependencyName = dependencyRootIdentifier.name;
621
681
  // Find all captures with the same root identifier
622
- const matchingCaptures = captures.filter((c) => getRootIdentifier(c.node)?.name === depName);
682
+ const matchingCaptures = captures.filter(({ node }) => getRootIdentifier(node)?.name === dependencyName);
623
683
  // If no captures use this identifier at all, it's unnecessary
624
684
  if (matchingCaptures.length === 0) {
625
685
  if (options.reportUnnecessaryDependencies) {
686
+ // Generate fix suggestion
687
+ const newDeps = dependencies.filter((d) => d.name !== dependency.name).map((d) => d.name);
688
+ const newDepsString = `[${newDeps.join(", ")}]`;
626
689
  context.report({
627
- data: { name: dep.name },
690
+ data: { name: dependency.name },
628
691
  messageId: "unnecessaryDependency",
629
- node: dep.node,
692
+ node: dependency.node,
693
+ suggest: [
694
+ {
695
+ desc: `Remove '${dependency.name}' from dependencies array`,
696
+ fix(fixer) {
697
+ return fixer.replaceText(depsArray, newDepsString);
698
+ },
699
+ },
700
+ ],
630
701
  });
631
702
  }
632
703
  continue;
@@ -634,11 +705,22 @@ const useExhaustiveDependencies = {
634
705
  // Check if dependency is more specific than any usage
635
706
  // dep.depth > all capture depths means the dep is too specific
636
707
  const maxCaptureDepth = Math.max(...matchingCaptures.map((c) => c.depth));
637
- if (dep.depth > maxCaptureDepth && options.reportUnnecessaryDependencies) {
708
+ if (dependency.depth > maxCaptureDepth && options.reportUnnecessaryDependencies) {
709
+ // Generate fix suggestion
710
+ const newDeps = dependencies.filter((d) => d.name !== dependency.name).map((d) => d.name);
711
+ const newDepsString = `[${newDeps.join(", ")}]`;
638
712
  context.report({
639
- data: { name: dep.name },
713
+ data: { name: dependency.name },
640
714
  messageId: "unnecessaryDependency",
641
- node: dep.node,
715
+ node: dependency.node,
716
+ suggest: [
717
+ {
718
+ desc: `Remove '${dependency.name}' from dependencies array`,
719
+ fix(fixer) {
720
+ return fixer.replaceText(depsArray, newDepsString);
721
+ },
722
+ },
723
+ ],
642
724
  });
643
725
  }
644
726
  }
@@ -648,27 +730,39 @@ const useExhaustiveDependencies = {
648
730
  if (isStableValue(capture.variable, capture.name, stableHooks))
649
731
  continue;
650
732
  // Check if the capture is in the dependencies
651
- const rootIdent = getRootIdentifier(capture.node);
652
- if (!rootIdent)
733
+ const rootIdentifier = getRootIdentifier(capture.node);
734
+ if (!rootIdentifier)
653
735
  continue;
654
- const captureName = rootIdent.name;
655
- let isInDeps = false;
736
+ const captureName = rootIdentifier.name;
737
+ let isInDependencies = false;
656
738
  // Check if capture is covered by dependencies
657
- for (const dep of dependencies) {
658
- const depRootIdent = getRootIdentifier(dep.node);
739
+ for (const dependency of dependencies) {
740
+ const dependencyRootIdentifier = getRootIdentifier(dependency.node);
659
741
  // Check name match and depth: dependency should not be more specific than capture
660
- if (depRootIdent?.name === captureName && dep.depth <= capture.depth) {
661
- isInDeps = true;
742
+ if (dependencyRootIdentifier?.name === captureName && dependency.depth <= capture.depth) {
743
+ isInDependencies = true;
662
744
  break;
663
745
  }
664
746
  }
665
- if (!isInDeps) {
747
+ if (!isInDependencies) {
666
748
  // Report on the last dependency in the array for better error positioning
667
- const lastDep = dependencies[dependencies.length - 1];
749
+ const lastDependency = dependencies.at(-1);
750
+ // Generate fix suggestion
751
+ const dependencyNames = dependencies.map(({ name }) => name);
752
+ const newDependencies = [...dependencyNames, capture.usagePath].toSorted();
753
+ const newDependenciesString = `[${newDependencies.join(", ")}]`;
668
754
  context.report({
669
755
  data: { name: capture.usagePath },
670
756
  messageId: "missingDependency",
671
- node: lastDep?.node || depsArray,
757
+ node: lastDependency?.node || depsArray,
758
+ suggest: [
759
+ {
760
+ desc: `Add '${capture.usagePath}' to dependencies array`,
761
+ fix(fixer) {
762
+ return fixer.replaceText(depsArray, newDependenciesString);
763
+ },
764
+ },
765
+ ],
672
766
  });
673
767
  }
674
768
  }
@@ -678,23 +772,25 @@ const useExhaustiveDependencies = {
678
772
  if (isStableValue(capture.variable, capture.name, stableHooks))
679
773
  continue;
680
774
  // Check if this capture has a corresponding dependency
681
- const rootIdent = getRootIdentifier(capture.node);
682
- if (!rootIdent)
775
+ const rootIdentifier = getRootIdentifier(capture.node);
776
+ if (!rootIdentifier)
683
777
  continue;
684
- const captureName = rootIdent.name;
778
+ const captureName = rootIdentifier.name;
685
779
  // Find if there's a matching dependency
686
- for (const dep of dependencies) {
687
- const depRootIdent = getRootIdentifier(dep.node);
688
- const isMatch = depRootIdent?.name === captureName && dep.depth === capture.depth;
689
- const isDirectIdentifier = dep.depth === 0;
780
+ for (const dependency of dependencies) {
781
+ const dependencyRootIdentifier = getRootIdentifier(dependency.node);
782
+ const isMatch = dependencyRootIdentifier?.name === captureName && dependency.depth === capture.depth;
783
+ const isDirectIdentifier = dependency.depth === 0;
690
784
  if (isMatch && isDirectIdentifier) {
691
- const def = capture.variable?.defs[0];
692
- const initNode = def?.node.type === "VariableDeclarator" ? def.node.init : undefined;
693
- if (isUnstableValue(initNode)) {
785
+ const variableDefinition = capture.variable?.defs[0];
786
+ const initialNode = variableDefinition?.node.type === "VariableDeclarator"
787
+ ? (variableDefinition.node.init ?? undefined)
788
+ : undefined;
789
+ if (isUnstableValue(initialNode)) {
694
790
  context.report({
695
791
  data: { name: capture.usagePath },
696
792
  messageId: "unstableDependency",
697
- node: dep.node,
793
+ node: dependency.node,
698
794
  });
699
795
  }
700
796
  break;
@@ -713,6 +809,7 @@ const useExhaustiveDependencies = {
713
809
  url: "https://biomejs.dev/linter/rules/use-exhaustive-dependencies/",
714
810
  },
715
811
  fixable: "code",
812
+ hasSuggestions: true,
716
813
  messages: {
717
814
  missingDependenciesArray: "This hook does not specify its dependencies array. Missing: {{deps}}",
718
815
  missingDependency: "This hook does not specify its dependency on {{name}}.",
@@ -772,5 +869,15 @@ const useExhaustiveDependencies = {
772
869
  type: "problem",
773
870
  },
774
871
  };
872
+ export const __testing = {
873
+ collectCaptures,
874
+ convertStableResult,
875
+ isDeclaredInComponentBody,
876
+ isInTypePosition,
877
+ isStableArrayIndex,
878
+ isStableValue,
879
+ metrics: testingMetrics,
880
+ resetMetrics: resetTestingMetrics,
881
+ };
775
882
  export default useExhaustiveDependencies;
776
883
  //# sourceMappingURL=use-exhaustive-dependencies.js.map