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