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

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