@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.
- package/dist/index.d.ts +39 -3
- package/dist/index.js.map +1 -1
- package/dist/rules/enforce-ianitor-check-type.js +26 -26
- package/dist/rules/enforce-ianitor-check-type.js.map +1 -1
- package/dist/rules/no-shorthand-names.js +1 -1
- package/dist/rules/no-shorthand-names.js.map +1 -1
- package/dist/rules/require-react-component-keys.d.ts +14 -2
- package/dist/rules/require-react-component-keys.js +219 -15
- package/dist/rules/require-react-component-keys.js.map +1 -1
- package/dist/rules/use-exhaustive-dependencies.d.ts +96 -1
- package/dist/rules/use-exhaustive-dependencies.js +227 -120
- package/dist/rules/use-exhaustive-dependencies.js.map +1 -1
- package/dist/rules/use-hook-at-top-level.js +88 -90
- package/dist/rules/use-hook-at-top-level.js.map +1 -1
- package/dist/stricty +3303 -0
- package/package.json +5 -4
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
174
|
-
const element = elements[
|
|
175
|
-
if (element
|
|
176
|
-
return stableResult.has(
|
|
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
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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 (
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
631
|
+
const dependenciesArgument = args[dependenciesIndex];
|
|
586
632
|
// Report missing dependencies array if configured
|
|
587
|
-
if (!
|
|
633
|
+
if (!dependenciesArgument && options.reportMissingDependenciesArray) {
|
|
588
634
|
// Collect captures to see if any are needed
|
|
589
|
-
const scope = getScope(
|
|
590
|
-
const captures = collectCaptures(
|
|
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((
|
|
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 (!
|
|
664
|
+
if (!dependenciesArgument)
|
|
605
665
|
return;
|
|
606
666
|
// Dependencies must be an array
|
|
607
|
-
if (
|
|
667
|
+
if (dependenciesArgument.type !== "ArrayExpression")
|
|
608
668
|
return;
|
|
609
|
-
const depsArray =
|
|
669
|
+
const depsArray = dependenciesArgument;
|
|
610
670
|
// Collect captures from closure
|
|
611
|
-
const scope = getScope(
|
|
612
|
-
const captures = collectCaptures(
|
|
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
|
|
617
|
-
const
|
|
618
|
-
if (!
|
|
676
|
+
for (const dependency of dependencies) {
|
|
677
|
+
const dependencyRootIdentifier = getRootIdentifier(dependency.node);
|
|
678
|
+
if (!dependencyRootIdentifier)
|
|
619
679
|
continue;
|
|
620
|
-
const
|
|
680
|
+
const dependencyName = dependencyRootIdentifier.name;
|
|
621
681
|
// Find all captures with the same root identifier
|
|
622
|
-
const matchingCaptures = captures.filter((
|
|
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:
|
|
690
|
+
data: { name: dependency.name },
|
|
628
691
|
messageId: "unnecessaryDependency",
|
|
629
|
-
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 (
|
|
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:
|
|
713
|
+
data: { name: dependency.name },
|
|
640
714
|
messageId: "unnecessaryDependency",
|
|
641
|
-
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
|
|
652
|
-
if (!
|
|
733
|
+
const rootIdentifier = getRootIdentifier(capture.node);
|
|
734
|
+
if (!rootIdentifier)
|
|
653
735
|
continue;
|
|
654
|
-
const captureName =
|
|
655
|
-
let
|
|
736
|
+
const captureName = rootIdentifier.name;
|
|
737
|
+
let isInDependencies = false;
|
|
656
738
|
// Check if capture is covered by dependencies
|
|
657
|
-
for (const
|
|
658
|
-
const
|
|
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 (
|
|
661
|
-
|
|
742
|
+
if (dependencyRootIdentifier?.name === captureName && dependency.depth <= capture.depth) {
|
|
743
|
+
isInDependencies = true;
|
|
662
744
|
break;
|
|
663
745
|
}
|
|
664
746
|
}
|
|
665
|
-
if (!
|
|
747
|
+
if (!isInDependencies) {
|
|
666
748
|
// Report on the last dependency in the array for better error positioning
|
|
667
|
-
const
|
|
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:
|
|
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
|
|
682
|
-
if (!
|
|
775
|
+
const rootIdentifier = getRootIdentifier(capture.node);
|
|
776
|
+
if (!rootIdentifier)
|
|
683
777
|
continue;
|
|
684
|
-
const captureName =
|
|
778
|
+
const captureName = rootIdentifier.name;
|
|
685
779
|
// Find if there's a matching dependency
|
|
686
|
-
for (const
|
|
687
|
-
const
|
|
688
|
-
const isMatch =
|
|
689
|
-
const isDirectIdentifier =
|
|
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
|
|
692
|
-
const
|
|
693
|
-
|
|
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:
|
|
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
|