@pobammer-ts/eslint-cease-nonsense-rules 0.10.1 → 0.11.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.
- package/dist/rules/ban-react-fc.js +2 -30
- package/dist/rules/ban-react-fc.js.map +1 -1
- package/dist/rules/enforce-ianitor-check-type.js +69 -157
- package/dist/rules/enforce-ianitor-check-type.js.map +1 -1
- package/dist/rules/no-color3-constructor.d.ts +0 -17
- package/dist/rules/no-color3-constructor.js +9 -31
- package/dist/rules/no-color3-constructor.js.map +1 -1
- package/dist/rules/no-instance-methods-without-this.js +2 -3
- package/dist/rules/no-instance-methods-without-this.js.map +1 -1
- package/dist/rules/no-print.d.ts +0 -15
- package/dist/rules/no-print.js +0 -21
- package/dist/rules/no-print.js.map +1 -1
- package/dist/rules/no-shorthand-names.d.ts +0 -24
- package/dist/rules/no-shorthand-names.js +18 -72
- package/dist/rules/no-shorthand-names.js.map +1 -1
- package/dist/rules/no-warn.d.ts +0 -15
- package/dist/rules/no-warn.js +0 -21
- package/dist/rules/no-warn.js.map +1 -1
- package/dist/rules/prefer-sequence-overloads.js +9 -16
- package/dist/rules/prefer-sequence-overloads.js.map +1 -1
- package/dist/rules/prefer-udim2-shorthand.d.ts +0 -14
- package/dist/rules/prefer-udim2-shorthand.js +17 -46
- package/dist/rules/prefer-udim2-shorthand.js.map +1 -1
- package/dist/rules/require-named-effect-functions.js +197 -59
- package/dist/rules/require-named-effect-functions.js.map +1 -1
- package/dist/rules/require-react-component-keys.d.ts +3 -39
- package/dist/rules/require-react-component-keys.js +40 -141
- package/dist/rules/require-react-component-keys.js.map +1 -1
- package/dist/rules/use-exhaustive-dependencies.d.ts +0 -42
- package/dist/rules/use-exhaustive-dependencies.js +100 -227
- package/dist/rules/use-exhaustive-dependencies.js.map +1 -1
- package/package.json +9 -7
|
@@ -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])],
|
|
47
|
-
["useReducer", new Set([1])],
|
|
48
|
-
["useTransition", new Set([1])],
|
|
49
|
-
["useRef", true],
|
|
50
|
-
["useBinding", true],
|
|
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
100
|
if (callee.type === "Identifier") {
|
|
136
101
|
return callee.name;
|
|
137
102
|
}
|
|
138
|
-
// Member expression: React.useEffect(...)
|
|
139
103
|
if (callee.type === "MemberExpression" && callee.property.type === "Identifier") {
|
|
140
104
|
return callee.property.name;
|
|
141
105
|
}
|
|
142
106
|
return undefined;
|
|
143
107
|
}
|
|
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
108
|
function getMemberExpressionDepth(node) {
|
|
151
109
|
let depth = 0;
|
|
152
110
|
let current = node;
|
|
153
|
-
while (current.type ===
|
|
111
|
+
while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
154
112
|
depth += 1;
|
|
155
113
|
current = current.object;
|
|
156
114
|
}
|
|
157
115
|
return depth;
|
|
158
116
|
}
|
|
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
117
|
function getRootIdentifier(node) {
|
|
166
118
|
let current = node;
|
|
167
|
-
while (current.type ===
|
|
119
|
+
while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression)
|
|
168
120
|
current = current.object;
|
|
169
|
-
return current.type ===
|
|
121
|
+
return current.type === TSESTree.AST_NODE_TYPES.Identifier ? current : undefined;
|
|
170
122
|
}
|
|
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
123
|
function nodeToDependencyString(node, sourceCode) {
|
|
179
124
|
return sourceCode.getText(node);
|
|
180
125
|
}
|
|
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
126
|
function isStableArrayIndex(stableResult, node, identifierName) {
|
|
190
127
|
if (!stableResult)
|
|
191
128
|
return false;
|
|
192
|
-
if (!(stableResult instanceof Set) ||
|
|
129
|
+
if (!(stableResult instanceof Set) ||
|
|
130
|
+
node.type !== TSESTree.AST_NODE_TYPES.VariableDeclarator ||
|
|
131
|
+
node.id.type !== TSESTree.AST_NODE_TYPES.ArrayPattern)
|
|
193
132
|
return false;
|
|
194
133
|
const elements = node.id.elements;
|
|
195
134
|
for (let index = 0; index < elements.length; index += 1) {
|
|
196
135
|
const element = elements[index];
|
|
197
|
-
if (element?.type ===
|
|
136
|
+
if (element?.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === identifierName)
|
|
198
137
|
return stableResult.has(index);
|
|
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
|
|
213
|
-
if (
|
|
142
|
+
const castInit = init;
|
|
143
|
+
if (castInit.type !== TSESTree.AST_NODE_TYPES.CallExpression)
|
|
214
144
|
return false;
|
|
215
|
-
const hookName = getHookName(
|
|
145
|
+
const hookName = getHookName(castInit);
|
|
216
146
|
if (!hookName)
|
|
217
147
|
return false;
|
|
218
148
|
const stableResult = stableHooks.get(hookName);
|
|
@@ -220,67 +150,52 @@ 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
153
|
/* eslint-disable jsdoc/require-param, jsdoc/require-returns */
|
|
232
154
|
function isStableValue(variable, identifierName, stableHooks) {
|
|
233
155
|
if (!variable)
|
|
234
156
|
return false;
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
157
|
+
const definitions = variable.defs;
|
|
158
|
+
if (definitions.length === 0)
|
|
237
159
|
return false;
|
|
238
|
-
for (const
|
|
239
|
-
const { node, type } =
|
|
240
|
-
// Imports, functions, classes are stable
|
|
160
|
+
for (const definition of definitions) {
|
|
161
|
+
const { node, type } = definition;
|
|
241
162
|
if (STABLE_VALUE_TYPES.has(type))
|
|
242
163
|
return true;
|
|
243
|
-
// Check for const declarations with constant initializers
|
|
244
164
|
if (type === "Variable" && node.type === "VariableDeclarator") {
|
|
245
165
|
const parent = node.parent;
|
|
246
|
-
if (!parent || parent.type !==
|
|
166
|
+
if (!parent || parent.type !== TSESTree.AST_NODE_TYPES.VariableDeclaration || parent.kind !== "const")
|
|
247
167
|
continue;
|
|
248
168
|
const { init } = node;
|
|
249
|
-
// Check if it's from a stable hook first
|
|
250
169
|
// @ts-expect-error - Type mismatch between ESLint and TypeScript AST types
|
|
251
170
|
if (init && isStableHookValue(init, node, identifierName, stableHooks))
|
|
252
171
|
return true;
|
|
253
|
-
|
|
254
|
-
if (init?.type === "CallExpression") {
|
|
172
|
+
if (init?.type === TSESTree.AST_NODE_TYPES.CallExpression) {
|
|
255
173
|
const { callee } = init;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
callee.object.type === "Identifier" &&
|
|
174
|
+
if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
175
|
+
callee.object.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
259
176
|
callee.object.name === "React" &&
|
|
260
|
-
callee.property.type ===
|
|
177
|
+
callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
261
178
|
callee.property.name === "joinBindings")
|
|
262
179
|
return true;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (callee.type === "MemberExpression" &&
|
|
266
|
-
callee.property.type === "Identifier" &&
|
|
180
|
+
if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
181
|
+
callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
267
182
|
callee.property.name === "map")
|
|
268
183
|
return true;
|
|
269
184
|
}
|
|
270
|
-
// Check for literal values FIRST (stable regardless of scope)
|
|
271
185
|
if (init) {
|
|
272
|
-
if (init.type ===
|
|
186
|
+
if (init.type === TSESTree.AST_NODE_TYPES.Literal ||
|
|
187
|
+
init.type === TSESTree.AST_NODE_TYPES.TemplateLiteral)
|
|
273
188
|
return true;
|
|
274
|
-
if (init.type ===
|
|
189
|
+
if (init.type === TSESTree.AST_NODE_TYPES.UnaryExpression &&
|
|
190
|
+
init.argument.type === TSESTree.AST_NODE_TYPES.Literal)
|
|
275
191
|
return true;
|
|
276
192
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (declParent && (declParent.type === "Program" || declParent.type === "ExportNamedDeclaration")) {
|
|
193
|
+
const variableDefinition = variable.defs.find((definition) => definition.node === node);
|
|
194
|
+
if (variableDefinition && variableDefinition.node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator) {
|
|
195
|
+
const declarationParent = variableDefinition.node.parent?.parent;
|
|
196
|
+
if (declarationParent &&
|
|
197
|
+
(declarationParent.type === TSESTree.AST_NODE_TYPES.Program ||
|
|
198
|
+
declarationParent.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration)) {
|
|
284
199
|
testingMetrics.moduleLevelStableConst += 1;
|
|
285
200
|
return true;
|
|
286
201
|
}
|
|
@@ -297,9 +212,9 @@ function isStableValue(variable, identifierName, stableHooks) {
|
|
|
297
212
|
*/
|
|
298
213
|
function findTopmostMemberExpression(node) {
|
|
299
214
|
let current = node;
|
|
300
|
-
let parent = node
|
|
215
|
+
let { parent } = node;
|
|
301
216
|
// Walk up the tree while we're the object of a member expression
|
|
302
|
-
while (parent?.type ===
|
|
217
|
+
while (parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression && parent.object === current) {
|
|
303
218
|
current = parent;
|
|
304
219
|
parent = parent.parent;
|
|
305
220
|
}
|
|
@@ -350,18 +265,18 @@ function isDeclaredInComponentBody(variable, closureNode) {
|
|
|
350
265
|
// Capture parent in a const so TypeScript understands it's stable in closures
|
|
351
266
|
const functionParent = parent;
|
|
352
267
|
// Check if variable is a parameter of this function (props)
|
|
353
|
-
const isParameter = variable.defs.some((
|
|
354
|
-
if (
|
|
268
|
+
const isParameter = variable.defs.some((definition) => {
|
|
269
|
+
if (definition.type !== "Parameter")
|
|
355
270
|
return false;
|
|
356
271
|
// For parameters, the def.node is the function itself
|
|
357
272
|
// Just check if the definition's node is the current function parent
|
|
358
|
-
return
|
|
273
|
+
return definition.node === functionParent;
|
|
359
274
|
});
|
|
360
275
|
if (isParameter)
|
|
361
276
|
return true; // Props are reactive
|
|
362
277
|
// Check if variable is defined inside this function
|
|
363
|
-
return variable.defs.some((
|
|
364
|
-
let node =
|
|
278
|
+
return variable.defs.some((definition) => {
|
|
279
|
+
let node = definition.node.parent;
|
|
365
280
|
while (node && node !== functionParent)
|
|
366
281
|
node = node.parent;
|
|
367
282
|
return node === functionParent;
|
|
@@ -391,48 +306,28 @@ function resolveFunctionReference(identifier, scope) {
|
|
|
391
306
|
if (!variable || variable.defs.length === 0)
|
|
392
307
|
return undefined;
|
|
393
308
|
// Check all definitions for a function
|
|
394
|
-
for (const
|
|
395
|
-
const { node } =
|
|
309
|
+
for (const definition of variable.defs) {
|
|
310
|
+
const { node } = definition;
|
|
396
311
|
// Direct function declaration
|
|
397
|
-
if (node.type ===
|
|
312
|
+
if (node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration)
|
|
398
313
|
return node;
|
|
399
314
|
// Variable declarator with function initializer
|
|
400
|
-
if (node.type ===
|
|
315
|
+
if (node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator &&
|
|
401
316
|
node.init &&
|
|
402
|
-
(node.init.type ===
|
|
317
|
+
(node.init.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
318
|
+
node.init.type === TSESTree.AST_NODE_TYPES.FunctionExpression))
|
|
403
319
|
return node.init;
|
|
404
320
|
}
|
|
405
321
|
return undefined;
|
|
406
322
|
}
|
|
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
323
|
function collectCaptures(node, scope, sourceCode) {
|
|
416
324
|
const captures = new Array();
|
|
417
325
|
const captureSet = new Set();
|
|
418
|
-
/**
|
|
419
|
-
* Recursively visits nodes to find identifier references.
|
|
420
|
-
*
|
|
421
|
-
* @param current - The current node.
|
|
422
|
-
*/
|
|
423
326
|
function visit(current) {
|
|
424
|
-
if (current.type ===
|
|
327
|
+
if (current.type === TSESTree.AST_NODE_TYPES.Identifier) {
|
|
425
328
|
const { name } = current;
|
|
426
|
-
|
|
427
|
-
if (captureSet.has(name))
|
|
428
|
-
return;
|
|
429
|
-
// Skip global built-ins (always stable, never need to be in dependencies)
|
|
430
|
-
if (GLOBAL_BUILTINS.has(name))
|
|
329
|
+
if (captureSet.has(name) || GLOBAL_BUILTINS.has(name) || isInTypePosition(current))
|
|
431
330
|
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
331
|
let variable;
|
|
437
332
|
let currentScope = scope;
|
|
438
333
|
while (currentScope) {
|
|
@@ -441,24 +336,20 @@ function collectCaptures(node, scope, sourceCode) {
|
|
|
441
336
|
break;
|
|
442
337
|
currentScope = currentScope.upper;
|
|
443
338
|
}
|
|
444
|
-
// Only capture if variable is defined outside the closure
|
|
445
339
|
if (variable) {
|
|
446
|
-
const isDefinedInClosure = variable.defs.some((
|
|
447
|
-
let
|
|
448
|
-
while (
|
|
449
|
-
if (
|
|
340
|
+
const isDefinedInClosure = variable.defs.some((definition) => {
|
|
341
|
+
let definitionNode = definition.node;
|
|
342
|
+
while (definitionNode) {
|
|
343
|
+
if (definitionNode === node)
|
|
450
344
|
return true;
|
|
451
|
-
|
|
345
|
+
definitionNode = definitionNode.parent;
|
|
452
346
|
}
|
|
453
347
|
return false;
|
|
454
348
|
});
|
|
455
349
|
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
350
|
if (!isDeclaredInComponentBody(variable, node)) {
|
|
460
351
|
testingMetrics.outerScopeSkip += 1;
|
|
461
|
-
return;
|
|
352
|
+
return;
|
|
462
353
|
}
|
|
463
354
|
captureSet.add(name);
|
|
464
355
|
const depthNode = findTopmostMemberExpression(current);
|
|
@@ -473,23 +364,20 @@ function collectCaptures(node, scope, sourceCode) {
|
|
|
473
364
|
}
|
|
474
365
|
}
|
|
475
366
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
current.type ===
|
|
479
|
-
current.type ===
|
|
480
|
-
current.type === "TSNonNullExpression") {
|
|
367
|
+
if (current.type === TSESTree.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
368
|
+
current.type === TSESTree.AST_NODE_TYPES.TSAsExpression ||
|
|
369
|
+
current.type === TSESTree.AST_NODE_TYPES.TSTypeAssertion ||
|
|
370
|
+
current.type === TSESTree.AST_NODE_TYPES.TSNonNullExpression) {
|
|
481
371
|
visit(current.expression);
|
|
482
372
|
return;
|
|
483
373
|
}
|
|
484
|
-
|
|
485
|
-
if (current.type === "MemberExpression") {
|
|
374
|
+
if (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
486
375
|
visit(current.object);
|
|
487
376
|
if (current.computed)
|
|
488
377
|
visit(current.property);
|
|
489
378
|
return;
|
|
490
379
|
}
|
|
491
|
-
|
|
492
|
-
const keys = sourceCode.visitorKeys?.[current.type] || [];
|
|
380
|
+
const keys = sourceCode.visitorKeys?.[current.type] ?? [];
|
|
493
381
|
for (const key of keys) {
|
|
494
382
|
const value = current[key];
|
|
495
383
|
if (Array.isArray(value)) {
|
|
@@ -504,17 +392,10 @@ function collectCaptures(node, scope, sourceCode) {
|
|
|
504
392
|
visit(node);
|
|
505
393
|
return captures;
|
|
506
394
|
}
|
|
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
395
|
function parseDependencies(node, sourceCode) {
|
|
515
396
|
const dependencies = new Array();
|
|
516
397
|
for (const element of node.elements) {
|
|
517
|
-
if (!element || element.type ===
|
|
398
|
+
if (!element || element.type === TSESTree.AST_NODE_TYPES.SpreadElement)
|
|
518
399
|
continue;
|
|
519
400
|
const name = nodeToDependencyString(element, sourceCode);
|
|
520
401
|
const depth = getMemberExpressionDepth(element);
|
|
@@ -526,23 +407,11 @@ function parseDependencies(node, sourceCode) {
|
|
|
526
407
|
}
|
|
527
408
|
return dependencies;
|
|
528
409
|
}
|
|
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
|
-
*/
|
|
535
410
|
function isUnstableValue(node) {
|
|
536
411
|
return node ? UNSTABLE_VALUES.has(node.type) : false;
|
|
537
412
|
}
|
|
538
413
|
const isNumberArray = Compile(Type.Array(Type.Number(), { minItems: 1, readOnly: true }));
|
|
539
414
|
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
415
|
function convertStableResult(stableResult) {
|
|
547
416
|
if (typeof stableResult === "boolean")
|
|
548
417
|
return stableResult;
|
|
@@ -565,12 +434,12 @@ const useExhaustiveDependencies = {
|
|
|
565
434
|
// Build hook configuration map
|
|
566
435
|
const hookConfigs = new Map(DEFAULT_HOOKS);
|
|
567
436
|
for (const customHook of options.hooks) {
|
|
568
|
-
if (customHook.closureIndex
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
437
|
+
if (customHook.closureIndex === undefined || customHook.dependenciesIndex === undefined)
|
|
438
|
+
continue;
|
|
439
|
+
hookConfigs.set(customHook.name, {
|
|
440
|
+
closureIndex: customHook.closureIndex,
|
|
441
|
+
dependenciesIndex: customHook.dependenciesIndex,
|
|
442
|
+
});
|
|
574
443
|
}
|
|
575
444
|
// Build stable hooks map
|
|
576
445
|
const stableHooks = new Map(STABLE_HOOKS);
|
|
@@ -600,33 +469,33 @@ const useExhaustiveDependencies = {
|
|
|
600
469
|
const callNode = node;
|
|
601
470
|
// Early exit: get hook name
|
|
602
471
|
const hookName = getHookName(callNode);
|
|
603
|
-
if (
|
|
472
|
+
if (hookName === undefined || hookName === "")
|
|
604
473
|
return;
|
|
605
474
|
// Early exit: check if this hook needs dependency checking
|
|
606
475
|
const hookConfig = hookConfigs.get(hookName);
|
|
607
476
|
if (!hookConfig)
|
|
608
477
|
return;
|
|
609
478
|
const { closureIndex, dependenciesIndex } = hookConfig;
|
|
610
|
-
const
|
|
479
|
+
const parameters = callNode.arguments;
|
|
611
480
|
// Early exit: check if closure argument exists
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
481
|
+
const closureArgument = parameters[closureIndex];
|
|
482
|
+
if (closureArgument === undefined)
|
|
614
483
|
return;
|
|
615
484
|
// Resolve the actual closure function (handles both inline and reference cases)
|
|
616
485
|
let closureFunction;
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
else if (
|
|
486
|
+
if (closureArgument.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
487
|
+
closureArgument.type === TSESTree.AST_NODE_TYPES.FunctionExpression)
|
|
488
|
+
closureFunction = closureArgument;
|
|
489
|
+
else if (closureArgument.type === TSESTree.AST_NODE_TYPES.Identifier) {
|
|
621
490
|
// Function reference - try to resolve it
|
|
622
491
|
const scope = getScope(callNode);
|
|
623
|
-
closureFunction = resolveFunctionReference(
|
|
492
|
+
closureFunction = resolveFunctionReference(closureArgument, scope);
|
|
624
493
|
}
|
|
625
494
|
// Early exit: check if we have a valid closure function
|
|
626
495
|
if (!closureFunction)
|
|
627
496
|
return;
|
|
628
497
|
// Get dependencies argument
|
|
629
|
-
const dependenciesArgument =
|
|
498
|
+
const dependenciesArgument = parameters[dependenciesIndex];
|
|
630
499
|
// Report missing dependencies array if configured
|
|
631
500
|
if (!dependenciesArgument && options.reportMissingDependenciesArray) {
|
|
632
501
|
// Collect captures to see if any are needed
|
|
@@ -649,8 +518,8 @@ const useExhaustiveDependencies = {
|
|
|
649
518
|
desc: `Add dependencies array: ${depsArrayString}`,
|
|
650
519
|
fix(fixer) {
|
|
651
520
|
// Insert the dependencies array after the closure argument
|
|
652
|
-
const
|
|
653
|
-
return fixer.insertTextAfter(
|
|
521
|
+
const closureArgumentNode = parameters[closureIndex];
|
|
522
|
+
return fixer.insertTextAfter(closureArgumentNode, `, ${depsArrayString}`);
|
|
654
523
|
},
|
|
655
524
|
},
|
|
656
525
|
],
|
|
@@ -662,14 +531,14 @@ const useExhaustiveDependencies = {
|
|
|
662
531
|
if (!dependenciesArgument)
|
|
663
532
|
return;
|
|
664
533
|
// Dependencies must be an array
|
|
665
|
-
if (dependenciesArgument.type !==
|
|
534
|
+
if (dependenciesArgument.type !== TSESTree.AST_NODE_TYPES.ArrayExpression)
|
|
666
535
|
return;
|
|
667
|
-
const
|
|
536
|
+
const dependenciesArray = dependenciesArgument;
|
|
668
537
|
// Collect captures from closure
|
|
669
538
|
const scope = getScope(closureFunction);
|
|
670
539
|
const captures = collectCaptures(closureFunction, scope, context.sourceCode);
|
|
671
540
|
// Parse dependencies array
|
|
672
|
-
const dependencies = parseDependencies(
|
|
541
|
+
const dependencies = parseDependencies(dependenciesArray, context.sourceCode);
|
|
673
542
|
// Check for unnecessary dependencies first (for consistent error ordering)
|
|
674
543
|
for (const dependency of dependencies) {
|
|
675
544
|
const dependencyRootIdentifier = getRootIdentifier(dependency.node);
|
|
@@ -682,8 +551,10 @@ const useExhaustiveDependencies = {
|
|
|
682
551
|
if (matchingCaptures.length === 0) {
|
|
683
552
|
if (options.reportUnnecessaryDependencies) {
|
|
684
553
|
// Generate fix suggestion
|
|
685
|
-
const
|
|
686
|
-
|
|
554
|
+
const newDependencies = dependencies
|
|
555
|
+
.filter((value) => value.name !== dependency.name)
|
|
556
|
+
.map(({ name }) => name);
|
|
557
|
+
const dependenciesString = `[${newDependencies.join(", ")}]`;
|
|
687
558
|
context.report({
|
|
688
559
|
data: { name: dependency.name },
|
|
689
560
|
messageId: "unnecessaryDependency",
|
|
@@ -692,7 +563,7 @@ const useExhaustiveDependencies = {
|
|
|
692
563
|
{
|
|
693
564
|
desc: `Remove '${dependency.name}' from dependencies array`,
|
|
694
565
|
fix(fixer) {
|
|
695
|
-
return fixer.replaceText(
|
|
566
|
+
return fixer.replaceText(dependenciesArray, dependenciesString);
|
|
696
567
|
},
|
|
697
568
|
},
|
|
698
569
|
],
|
|
@@ -702,11 +573,13 @@ const useExhaustiveDependencies = {
|
|
|
702
573
|
}
|
|
703
574
|
// Check if dependency is more specific than any usage
|
|
704
575
|
// dep.depth > all capture depths means the dep is too specific
|
|
705
|
-
const maxCaptureDepth = Math.max(...matchingCaptures.map((
|
|
576
|
+
const maxCaptureDepth = Math.max(...matchingCaptures.map(({ depth }) => depth));
|
|
706
577
|
if (dependency.depth > maxCaptureDepth && options.reportUnnecessaryDependencies) {
|
|
707
578
|
// Generate fix suggestion
|
|
708
|
-
const
|
|
709
|
-
|
|
579
|
+
const newDependencies = dependencies
|
|
580
|
+
.filter(({ name }) => name !== dependency.name)
|
|
581
|
+
.map(({ name }) => name);
|
|
582
|
+
const newDependenciesString = `[${newDependencies.join(", ")}]`;
|
|
710
583
|
context.report({
|
|
711
584
|
data: { name: dependency.name },
|
|
712
585
|
messageId: "unnecessaryDependency",
|
|
@@ -715,7 +588,7 @@ const useExhaustiveDependencies = {
|
|
|
715
588
|
{
|
|
716
589
|
desc: `Remove '${dependency.name}' from dependencies array`,
|
|
717
590
|
fix(fixer) {
|
|
718
|
-
return fixer.replaceText(
|
|
591
|
+
return fixer.replaceText(dependenciesArray, newDependenciesString);
|
|
719
592
|
},
|
|
720
593
|
},
|
|
721
594
|
],
|
|
@@ -759,12 +632,12 @@ const useExhaustiveDependencies = {
|
|
|
759
632
|
context.report({
|
|
760
633
|
data: { name: firstMissing.usagePath },
|
|
761
634
|
messageId: "missingDependency",
|
|
762
|
-
node: lastDependency?.node ||
|
|
635
|
+
node: lastDependency?.node || dependenciesArray,
|
|
763
636
|
suggest: [
|
|
764
637
|
{
|
|
765
638
|
desc: `Add '${firstMissing.usagePath}' to dependencies array`,
|
|
766
639
|
fix(fixer) {
|
|
767
|
-
return fixer.replaceText(
|
|
640
|
+
return fixer.replaceText(dependenciesArray, newDependenciesString);
|
|
768
641
|
},
|
|
769
642
|
},
|
|
770
643
|
],
|
|
@@ -776,12 +649,12 @@ const useExhaustiveDependencies = {
|
|
|
776
649
|
context.report({
|
|
777
650
|
data: { names: missingNames },
|
|
778
651
|
messageId: "missingDependencies",
|
|
779
|
-
node: lastDependency?.node ||
|
|
652
|
+
node: lastDependency?.node || dependenciesArray,
|
|
780
653
|
suggest: [
|
|
781
654
|
{
|
|
782
655
|
desc: "Add missing dependencies to array",
|
|
783
656
|
fix(fixer) {
|
|
784
|
-
return fixer.replaceText(
|
|
657
|
+
return fixer.replaceText(dependenciesArray, newDependenciesString);
|
|
785
658
|
},
|
|
786
659
|
},
|
|
787
660
|
],
|