@pobammer-ts/eslint-cease-nonsense-rules 1.2.2 → 1.2.4
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/build-metadata.json +5 -0
- package/dist/index.js +11637 -80
- package/dist/index.js.map +515 -1
- package/dist/rules/require-paired-calls.d.ts +1 -1
- package/package.json +10 -10
- package/dist/configure-utilities.js +0 -136
- package/dist/configure-utilities.js.map +0 -1
- package/dist/rules/ban-instances.js +0 -123
- package/dist/rules/ban-instances.js.map +0 -1
- package/dist/rules/ban-react-fc.js +0 -42
- package/dist/rules/ban-react-fc.js.map +0 -1
- package/dist/rules/enforce-ianitor-check-type.js +0 -340
- package/dist/rules/enforce-ianitor-check-type.js.map +0 -1
- package/dist/rules/no-color3-constructor.js +0 -76
- package/dist/rules/no-color3-constructor.js.map +0 -1
- package/dist/rules/no-instance-methods-without-this.js +0 -122
- package/dist/rules/no-instance-methods-without-this.js.map +0 -1
- package/dist/rules/no-print.js +0 -25
- package/dist/rules/no-print.js.map +0 -1
- package/dist/rules/no-shorthand-names.js +0 -116
- package/dist/rules/no-shorthand-names.js.map +0 -1
- package/dist/rules/no-warn.js +0 -25
- package/dist/rules/no-warn.js.map +0 -1
- package/dist/rules/prefer-sequence-overloads.js +0 -109
- package/dist/rules/prefer-sequence-overloads.js.map +0 -1
- package/dist/rules/prefer-udim2-shorthand.js +0 -161
- package/dist/rules/prefer-udim2-shorthand.js.map +0 -1
- package/dist/rules/require-named-effect-functions.js +0 -350
- package/dist/rules/require-named-effect-functions.js.map +0 -1
- package/dist/rules/require-paired-calls.js +0 -848
- package/dist/rules/require-paired-calls.js.map +0 -1
- package/dist/rules/require-react-component-keys.js +0 -357
- package/dist/rules/require-react-component-keys.js.map +0 -1
- package/dist/rules/use-exhaustive-dependencies.js +0 -697
- package/dist/rules/use-exhaustive-dependencies.js.map +0 -1
- package/dist/rules/use-hook-at-top-level.js +0 -351
- package/dist/rules/use-hook-at-top-level.js.map +0 -1
|
@@ -1,697 +0,0 @@
|
|
|
1
|
-
import { TSESTree } from "@typescript-eslint/types";
|
|
2
|
-
import Type from "typebox";
|
|
3
|
-
import { Compile } from "typebox/compile";
|
|
4
|
-
const FUNCTION_DECLARATIONS = new Set([
|
|
5
|
-
TSESTree.AST_NODE_TYPES.FunctionExpression,
|
|
6
|
-
TSESTree.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
7
|
-
TSESTree.AST_NODE_TYPES.FunctionDeclaration,
|
|
8
|
-
]);
|
|
9
|
-
const UNSTABLE_VALUES = new Set([
|
|
10
|
-
...FUNCTION_DECLARATIONS,
|
|
11
|
-
TSESTree.AST_NODE_TYPES.ObjectExpression,
|
|
12
|
-
TSESTree.AST_NODE_TYPES.ArrayExpression,
|
|
13
|
-
]);
|
|
14
|
-
const testingMetrics = {
|
|
15
|
-
moduleLevelStableConst: 0,
|
|
16
|
-
outerScopeSkip: 0,
|
|
17
|
-
};
|
|
18
|
-
function resetTestingMetrics() {
|
|
19
|
-
testingMetrics.moduleLevelStableConst = 0;
|
|
20
|
-
testingMetrics.outerScopeSkip = 0;
|
|
21
|
-
}
|
|
22
|
-
const DEFAULT_HOOKS = new Map([
|
|
23
|
-
["useEffect", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
24
|
-
["useLayoutEffect", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
25
|
-
["useInsertionEffect", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
26
|
-
["useCallback", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
27
|
-
["useMemo", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
28
|
-
["useImperativeHandle", { closureIndex: 1, dependenciesIndex: 2 }],
|
|
29
|
-
["useSpring", { closureIndex: 0, dependenciesIndex: 1 }],
|
|
30
|
-
["useSprings", { closureIndex: 1, dependenciesIndex: 2 }],
|
|
31
|
-
["useTrail", { closureIndex: 1, dependenciesIndex: 2 }],
|
|
32
|
-
]);
|
|
33
|
-
const STABLE_HOOKS = new Map([
|
|
34
|
-
["useState", new Set([1])],
|
|
35
|
-
["useReducer", new Set([1])],
|
|
36
|
-
["useTransition", new Set([1])],
|
|
37
|
-
["useRef", true],
|
|
38
|
-
["useBinding", true],
|
|
39
|
-
]);
|
|
40
|
-
const STABLE_VALUE_TYPES = new Set(["ImportBinding", "FunctionDeclaration", "ClassDeclaration", "FunctionName"]);
|
|
41
|
-
const GLOBAL_BUILTINS = new Set([
|
|
42
|
-
"undefined",
|
|
43
|
-
"null",
|
|
44
|
-
"Infinity",
|
|
45
|
-
"NaN",
|
|
46
|
-
"Array",
|
|
47
|
-
"Object",
|
|
48
|
-
"String",
|
|
49
|
-
"Number",
|
|
50
|
-
"Boolean",
|
|
51
|
-
"Symbol",
|
|
52
|
-
"BigInt",
|
|
53
|
-
"Function",
|
|
54
|
-
"Map",
|
|
55
|
-
"Set",
|
|
56
|
-
"WeakMap",
|
|
57
|
-
"WeakSet",
|
|
58
|
-
"Promise",
|
|
59
|
-
"Date",
|
|
60
|
-
"RegExp",
|
|
61
|
-
"Error",
|
|
62
|
-
"Math",
|
|
63
|
-
"JSON",
|
|
64
|
-
"parseInt",
|
|
65
|
-
"parseFloat",
|
|
66
|
-
"isNaN",
|
|
67
|
-
"isFinite",
|
|
68
|
-
"encodeURI",
|
|
69
|
-
"encodeURIComponent",
|
|
70
|
-
"decodeURI",
|
|
71
|
-
"decodeURIComponent",
|
|
72
|
-
"ReadonlyArray",
|
|
73
|
-
"ReadonlyMap",
|
|
74
|
-
"ReadonlySet",
|
|
75
|
-
"Partial",
|
|
76
|
-
"Required",
|
|
77
|
-
"Readonly",
|
|
78
|
-
"Pick",
|
|
79
|
-
"Omit",
|
|
80
|
-
"Exclude",
|
|
81
|
-
"Extract",
|
|
82
|
-
"Record",
|
|
83
|
-
"NonNullable",
|
|
84
|
-
"ReturnType",
|
|
85
|
-
"InstanceType",
|
|
86
|
-
"Parameters",
|
|
87
|
-
"console",
|
|
88
|
-
"setTimeout",
|
|
89
|
-
"setInterval",
|
|
90
|
-
"clearTimeout",
|
|
91
|
-
"clearInterval",
|
|
92
|
-
"Element",
|
|
93
|
-
"Node",
|
|
94
|
-
"Document",
|
|
95
|
-
"Window",
|
|
96
|
-
"Event",
|
|
97
|
-
]);
|
|
98
|
-
function getHookName(node) {
|
|
99
|
-
const { callee } = node;
|
|
100
|
-
if (callee.type === TSESTree.AST_NODE_TYPES.Identifier)
|
|
101
|
-
return callee.name;
|
|
102
|
-
if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
103
|
-
callee.property.type === TSESTree.AST_NODE_TYPES.Identifier)
|
|
104
|
-
return callee.property.name;
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
function getMemberExpressionDepth(node) {
|
|
108
|
-
let depth = 0;
|
|
109
|
-
let current = node;
|
|
110
|
-
while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
111
|
-
depth += 1;
|
|
112
|
-
current = current.object;
|
|
113
|
-
}
|
|
114
|
-
return depth;
|
|
115
|
-
}
|
|
116
|
-
function getRootIdentifier(node) {
|
|
117
|
-
let current = node;
|
|
118
|
-
while (current.type === TSESTree.AST_NODE_TYPES.MemberExpression)
|
|
119
|
-
current = current.object;
|
|
120
|
-
return current.type === TSESTree.AST_NODE_TYPES.Identifier ? current : undefined;
|
|
121
|
-
}
|
|
122
|
-
function nodeToDependencyString(node, sourceCode) {
|
|
123
|
-
return sourceCode.getText(node);
|
|
124
|
-
}
|
|
125
|
-
function isStableArrayIndex(stableResult, node, identifierName) {
|
|
126
|
-
if (!stableResult)
|
|
127
|
-
return false;
|
|
128
|
-
if (!(stableResult instanceof Set) ||
|
|
129
|
-
node.type !== TSESTree.AST_NODE_TYPES.VariableDeclarator ||
|
|
130
|
-
node.id.type !== TSESTree.AST_NODE_TYPES.ArrayPattern)
|
|
131
|
-
return false;
|
|
132
|
-
const elements = node.id.elements;
|
|
133
|
-
let index = 0;
|
|
134
|
-
for (const element of elements) {
|
|
135
|
-
if (element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === identifierName)
|
|
136
|
-
return stableResult.has(index);
|
|
137
|
-
index += 1;
|
|
138
|
-
}
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
function isStableHookValue(init, node, identifierName, stableHooks) {
|
|
142
|
-
const castInit = init;
|
|
143
|
-
if (castInit.type !== TSESTree.AST_NODE_TYPES.CallExpression)
|
|
144
|
-
return false;
|
|
145
|
-
const hookName = getHookName(castInit);
|
|
146
|
-
if (!hookName)
|
|
147
|
-
return false;
|
|
148
|
-
const stableResult = stableHooks.get(hookName);
|
|
149
|
-
if (stableResult === true)
|
|
150
|
-
return true;
|
|
151
|
-
return isStableArrayIndex(stableResult, node, identifierName);
|
|
152
|
-
}
|
|
153
|
-
function isStableValue(variable, identifierName, stableHooks) {
|
|
154
|
-
if (!variable)
|
|
155
|
-
return false;
|
|
156
|
-
const definitions = variable.defs;
|
|
157
|
-
if (definitions.length === 0)
|
|
158
|
-
return false;
|
|
159
|
-
for (const definition of definitions) {
|
|
160
|
-
const { node, type } = definition;
|
|
161
|
-
if (STABLE_VALUE_TYPES.has(type))
|
|
162
|
-
return true;
|
|
163
|
-
if (type === "Variable" && node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator) {
|
|
164
|
-
const parent = node.parent;
|
|
165
|
-
if (!parent || parent.type !== TSESTree.AST_NODE_TYPES.VariableDeclaration || parent.kind !== "const")
|
|
166
|
-
continue;
|
|
167
|
-
const init = node.init;
|
|
168
|
-
if (init && isStableHookValue(init, node, identifierName, stableHooks))
|
|
169
|
-
return true;
|
|
170
|
-
if (init?.type === TSESTree.AST_NODE_TYPES.CallExpression) {
|
|
171
|
-
const { callee } = init;
|
|
172
|
-
if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
173
|
-
callee.object.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
174
|
-
callee.object.name === "React" &&
|
|
175
|
-
callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
176
|
-
callee.property.name === "joinBindings")
|
|
177
|
-
return true;
|
|
178
|
-
if (callee.type === TSESTree.AST_NODE_TYPES.MemberExpression &&
|
|
179
|
-
callee.property.type === TSESTree.AST_NODE_TYPES.Identifier &&
|
|
180
|
-
callee.property.name === "map")
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
if (init) {
|
|
184
|
-
if (init.type === TSESTree.AST_NODE_TYPES.Literal ||
|
|
185
|
-
init.type === TSESTree.AST_NODE_TYPES.TemplateLiteral)
|
|
186
|
-
return true;
|
|
187
|
-
if (init.type === TSESTree.AST_NODE_TYPES.UnaryExpression &&
|
|
188
|
-
init.argument.type === TSESTree.AST_NODE_TYPES.Literal)
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
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)) {
|
|
197
|
-
testingMetrics.moduleLevelStableConst += 1;
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
function findTopmostMemberExpression(node) {
|
|
206
|
-
let current = node;
|
|
207
|
-
let { parent } = node;
|
|
208
|
-
while (parent?.type === TSESTree.AST_NODE_TYPES.MemberExpression && parent.object === current) {
|
|
209
|
-
current = parent;
|
|
210
|
-
parent = parent.parent;
|
|
211
|
-
}
|
|
212
|
-
return current;
|
|
213
|
-
}
|
|
214
|
-
const IS_CEASE_BOUNDARY = new Set([
|
|
215
|
-
TSESTree.AST_NODE_TYPES.FunctionDeclaration,
|
|
216
|
-
TSESTree.AST_NODE_TYPES.FunctionExpression,
|
|
217
|
-
TSESTree.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
218
|
-
TSESTree.AST_NODE_TYPES.VariableDeclarator,
|
|
219
|
-
]);
|
|
220
|
-
function isInTypePosition(identifier) {
|
|
221
|
-
let parent = identifier.parent;
|
|
222
|
-
while (parent) {
|
|
223
|
-
if (parent.type.startsWith("TS"))
|
|
224
|
-
return true;
|
|
225
|
-
if (IS_CEASE_BOUNDARY.has(parent.type))
|
|
226
|
-
return false;
|
|
227
|
-
parent = parent.parent;
|
|
228
|
-
}
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
function isDeclaredInComponentBody(variable, closureNode) {
|
|
232
|
-
let parent = closureNode.parent;
|
|
233
|
-
while (parent) {
|
|
234
|
-
const isFunction = FUNCTION_DECLARATIONS.has(parent.type);
|
|
235
|
-
if (isFunction) {
|
|
236
|
-
const functionParent = parent;
|
|
237
|
-
const isParameter = variable.defs.some((definition) => {
|
|
238
|
-
if (definition.type !== "Parameter")
|
|
239
|
-
return false;
|
|
240
|
-
return definition.node === functionParent;
|
|
241
|
-
});
|
|
242
|
-
if (isParameter)
|
|
243
|
-
return true;
|
|
244
|
-
return variable.defs.some((definition) => {
|
|
245
|
-
let node = definition.node.parent;
|
|
246
|
-
while (node && node !== functionParent)
|
|
247
|
-
node = node.parent;
|
|
248
|
-
return node === functionParent;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
parent = parent.parent;
|
|
252
|
-
}
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
function resolveFunctionReference(identifier, scope) {
|
|
256
|
-
let variable;
|
|
257
|
-
let currentScope = scope;
|
|
258
|
-
while (currentScope) {
|
|
259
|
-
variable = currentScope.set.get(identifier.name);
|
|
260
|
-
if (variable)
|
|
261
|
-
break;
|
|
262
|
-
currentScope = currentScope.upper;
|
|
263
|
-
}
|
|
264
|
-
if (!variable || variable.defs.length === 0)
|
|
265
|
-
return undefined;
|
|
266
|
-
for (const definition of variable.defs) {
|
|
267
|
-
const { node } = definition;
|
|
268
|
-
if (node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration)
|
|
269
|
-
return node;
|
|
270
|
-
if (node.type === TSESTree.AST_NODE_TYPES.VariableDeclarator &&
|
|
271
|
-
node.init &&
|
|
272
|
-
(node.init.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
273
|
-
node.init.type === TSESTree.AST_NODE_TYPES.FunctionExpression))
|
|
274
|
-
return node.init;
|
|
275
|
-
}
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
function collectCaptures(node, sourceCode) {
|
|
279
|
-
const captures = new Array();
|
|
280
|
-
const captureSet = new Set();
|
|
281
|
-
function visit(current) {
|
|
282
|
-
if (current.type === TSESTree.AST_NODE_TYPES.Identifier) {
|
|
283
|
-
const { name } = current;
|
|
284
|
-
if (captureSet.has(name) || GLOBAL_BUILTINS.has(name) || isInTypePosition(current))
|
|
285
|
-
return;
|
|
286
|
-
let variable;
|
|
287
|
-
let currentScope = sourceCode.getScope(current);
|
|
288
|
-
while (currentScope) {
|
|
289
|
-
variable = currentScope.set.get(name);
|
|
290
|
-
if (variable)
|
|
291
|
-
break;
|
|
292
|
-
currentScope = currentScope.upper;
|
|
293
|
-
}
|
|
294
|
-
if (variable) {
|
|
295
|
-
const isDefinedInClosure = variable.defs.some((definition) => {
|
|
296
|
-
let definitionNode = definition.node;
|
|
297
|
-
while (definitionNode) {
|
|
298
|
-
if (definitionNode === node)
|
|
299
|
-
return true;
|
|
300
|
-
definitionNode = definitionNode.parent;
|
|
301
|
-
}
|
|
302
|
-
return false;
|
|
303
|
-
});
|
|
304
|
-
if (!isDefinedInClosure) {
|
|
305
|
-
if (!isDeclaredInComponentBody(variable, node)) {
|
|
306
|
-
testingMetrics.outerScopeSkip += 1;
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
captureSet.add(name);
|
|
310
|
-
const depthNode = findTopmostMemberExpression(current);
|
|
311
|
-
const usagePath = sourceCode.getText(depthNode);
|
|
312
|
-
captures.push({
|
|
313
|
-
depth: getMemberExpressionDepth(depthNode),
|
|
314
|
-
name,
|
|
315
|
-
node: depthNode,
|
|
316
|
-
usagePath,
|
|
317
|
-
variable: variable,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
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) {
|
|
326
|
-
visit(current.expression);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (current.type === TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
330
|
-
visit(current.object);
|
|
331
|
-
if (current.computed)
|
|
332
|
-
visit(current.property);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const keys = sourceCode.visitorKeys?.[current.type] ?? [];
|
|
336
|
-
for (const key of keys) {
|
|
337
|
-
const value = current[key];
|
|
338
|
-
if (Array.isArray(value)) {
|
|
339
|
-
for (const item of value)
|
|
340
|
-
if (item && typeof item === "object" && "type" in item)
|
|
341
|
-
visit(item);
|
|
342
|
-
}
|
|
343
|
-
else if (value && typeof value === "object" && "type" in value)
|
|
344
|
-
visit(value);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
visit(node);
|
|
348
|
-
return captures;
|
|
349
|
-
}
|
|
350
|
-
function parseDependencies(node, sourceCode) {
|
|
351
|
-
const dependencies = new Array();
|
|
352
|
-
for (const element of node.elements) {
|
|
353
|
-
if (!element || element.type === TSESTree.AST_NODE_TYPES.SpreadElement)
|
|
354
|
-
continue;
|
|
355
|
-
const name = nodeToDependencyString(element, sourceCode);
|
|
356
|
-
const depth = getMemberExpressionDepth(element);
|
|
357
|
-
dependencies.push({
|
|
358
|
-
depth,
|
|
359
|
-
name,
|
|
360
|
-
node: element,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return dependencies;
|
|
364
|
-
}
|
|
365
|
-
function returnName({ name }) {
|
|
366
|
-
return name;
|
|
367
|
-
}
|
|
368
|
-
function isUnstableValue(node) {
|
|
369
|
-
return node ? UNSTABLE_VALUES.has(node.type) : false;
|
|
370
|
-
}
|
|
371
|
-
const isNumberArray = Compile(Type.Array(Type.Number(), { minItems: 1, readOnly: true }));
|
|
372
|
-
const isStringArray = Compile(Type.Array(Type.String(), { minItems: 1, readOnly: true }));
|
|
373
|
-
function convertStableResult(stableResult) {
|
|
374
|
-
if (typeof stableResult === "boolean")
|
|
375
|
-
return stableResult;
|
|
376
|
-
if (typeof stableResult === "number")
|
|
377
|
-
return new Set([stableResult]);
|
|
378
|
-
if (isNumberArray.Check(stableResult))
|
|
379
|
-
return new Set(stableResult);
|
|
380
|
-
if (isStringArray.Check(stableResult))
|
|
381
|
-
return new Set(stableResult);
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
const useExhaustiveDependencies = {
|
|
385
|
-
create(context) {
|
|
386
|
-
const options = {
|
|
387
|
-
hooks: [],
|
|
388
|
-
reportMissingDependenciesArray: true,
|
|
389
|
-
reportUnnecessaryDependencies: true,
|
|
390
|
-
...context.options[0],
|
|
391
|
-
};
|
|
392
|
-
const hookConfigs = new Map(DEFAULT_HOOKS);
|
|
393
|
-
for (const customHook of options.hooks) {
|
|
394
|
-
if (customHook.closureIndex === undefined || customHook.dependenciesIndex === undefined)
|
|
395
|
-
continue;
|
|
396
|
-
hookConfigs.set(customHook.name, {
|
|
397
|
-
closureIndex: customHook.closureIndex,
|
|
398
|
-
dependenciesIndex: customHook.dependenciesIndex,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
const stableHooks = new Map(STABLE_HOOKS);
|
|
402
|
-
for (const customHook of options.hooks) {
|
|
403
|
-
if (customHook.stableResult === undefined)
|
|
404
|
-
continue;
|
|
405
|
-
stableHooks.set(customHook.name, convertStableResult(customHook.stableResult));
|
|
406
|
-
}
|
|
407
|
-
const scopeCache = new WeakMap();
|
|
408
|
-
function getScope(node) {
|
|
409
|
-
const cached = scopeCache.get(node);
|
|
410
|
-
if (cached)
|
|
411
|
-
return cached;
|
|
412
|
-
const scope = context.sourceCode.getScope(node);
|
|
413
|
-
scopeCache.set(node, scope);
|
|
414
|
-
return scope;
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
CallExpression(node) {
|
|
418
|
-
const callNode = node;
|
|
419
|
-
const hookName = getHookName(callNode);
|
|
420
|
-
if (hookName === undefined || hookName === "")
|
|
421
|
-
return;
|
|
422
|
-
const hookConfig = hookConfigs.get(hookName);
|
|
423
|
-
if (!hookConfig)
|
|
424
|
-
return;
|
|
425
|
-
const { closureIndex, dependenciesIndex } = hookConfig;
|
|
426
|
-
const parameters = callNode.arguments;
|
|
427
|
-
const closureArgument = parameters[closureIndex];
|
|
428
|
-
if (closureArgument === undefined)
|
|
429
|
-
return;
|
|
430
|
-
let closureFunction;
|
|
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) {
|
|
435
|
-
const scope = getScope(callNode);
|
|
436
|
-
closureFunction = resolveFunctionReference(closureArgument, scope);
|
|
437
|
-
}
|
|
438
|
-
if (!closureFunction)
|
|
439
|
-
return;
|
|
440
|
-
const dependenciesArgument = parameters[dependenciesIndex];
|
|
441
|
-
if (!dependenciesArgument && options.reportMissingDependenciesArray) {
|
|
442
|
-
// const _scope = getScope(closureFunction);
|
|
443
|
-
const captures = collectCaptures(closureFunction, context.sourceCode);
|
|
444
|
-
const requiredCaptures = captures.filter((capture) => !isStableValue(capture.variable, capture.name, stableHooks));
|
|
445
|
-
if (requiredCaptures.length > 0) {
|
|
446
|
-
// oxlint-disable-next-line no-array-callback-reference
|
|
447
|
-
const missingNames = [...new Set(requiredCaptures.map(returnName))].join(", ");
|
|
448
|
-
const usagePaths = requiredCaptures.map(({ usagePath }) => usagePath);
|
|
449
|
-
const uniqueDependencies = [...new Set(usagePaths)].toSorted();
|
|
450
|
-
const dependenciesString = `[${uniqueDependencies.join(", ")}]`;
|
|
451
|
-
context.report({
|
|
452
|
-
data: { deps: missingNames },
|
|
453
|
-
messageId: "missingDependenciesArray",
|
|
454
|
-
node: callNode,
|
|
455
|
-
suggest: [
|
|
456
|
-
{
|
|
457
|
-
desc: `Add dependencies array: ${dependenciesString}`,
|
|
458
|
-
fix(fixer) {
|
|
459
|
-
//
|
|
460
|
-
const closureArgumentNode = parameters[closureIndex];
|
|
461
|
-
return fixer.insertTextAfter(closureArgumentNode, `, ${dependenciesString}`);
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (!dependenciesArgument)
|
|
470
|
-
return;
|
|
471
|
-
if (dependenciesArgument.type !== TSESTree.AST_NODE_TYPES.ArrayExpression)
|
|
472
|
-
return;
|
|
473
|
-
const dependenciesArray = dependenciesArgument;
|
|
474
|
-
// const _scope = getScope(closureFunction);
|
|
475
|
-
const captures = collectCaptures(closureFunction, context.sourceCode);
|
|
476
|
-
const dependencies = parseDependencies(dependenciesArray, context.sourceCode);
|
|
477
|
-
for (const dependency of dependencies) {
|
|
478
|
-
const dependencyRootIdentifier = getRootIdentifier(dependency.node);
|
|
479
|
-
if (!dependencyRootIdentifier)
|
|
480
|
-
continue;
|
|
481
|
-
const dependencyName = dependencyRootIdentifier.name;
|
|
482
|
-
const matchingCaptures = captures.filter(({ node }) => getRootIdentifier(node)?.name === dependencyName);
|
|
483
|
-
if (matchingCaptures.length === 0) {
|
|
484
|
-
if (options.reportUnnecessaryDependencies) {
|
|
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(", ")}]`;
|
|
490
|
-
context.report({
|
|
491
|
-
data: { name: dependency.name },
|
|
492
|
-
messageId: "unnecessaryDependency",
|
|
493
|
-
node: dependency.node,
|
|
494
|
-
suggest: [
|
|
495
|
-
{
|
|
496
|
-
desc: `Remove '${dependency.name}' from dependencies array`,
|
|
497
|
-
fix(fixer) {
|
|
498
|
-
return fixer.replaceText(dependenciesArray, dependenciesString);
|
|
499
|
-
},
|
|
500
|
-
},
|
|
501
|
-
],
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
const maxCaptureDepth = Math.max(...matchingCaptures.map(({ depth }) => depth));
|
|
507
|
-
if (dependency.depth > maxCaptureDepth && options.reportUnnecessaryDependencies) {
|
|
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(", ")}]`;
|
|
513
|
-
context.report({
|
|
514
|
-
data: { name: dependency.name },
|
|
515
|
-
messageId: "unnecessaryDependency",
|
|
516
|
-
node: dependency.node,
|
|
517
|
-
suggest: [
|
|
518
|
-
{
|
|
519
|
-
desc: `Remove '${dependency.name}' from dependencies array`,
|
|
520
|
-
fix(fixer) {
|
|
521
|
-
return fixer.replaceText(dependenciesArray, dependencyString);
|
|
522
|
-
},
|
|
523
|
-
},
|
|
524
|
-
],
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
const missingCaptures = new Array();
|
|
529
|
-
for (const capture of captures) {
|
|
530
|
-
if (isStableValue(capture.variable, capture.name, stableHooks))
|
|
531
|
-
continue;
|
|
532
|
-
const rootIdentifier = getRootIdentifier(capture.node);
|
|
533
|
-
if (!rootIdentifier)
|
|
534
|
-
continue;
|
|
535
|
-
const captureName = rootIdentifier.name;
|
|
536
|
-
let isInDependencies = false;
|
|
537
|
-
for (const dependency of dependencies) {
|
|
538
|
-
const dependencyRootIdentifier = getRootIdentifier(dependency.node);
|
|
539
|
-
if (dependencyRootIdentifier?.name === captureName && dependency.depth <= capture.depth) {
|
|
540
|
-
isInDependencies = true;
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (!isInDependencies)
|
|
545
|
-
missingCaptures.push(capture);
|
|
546
|
-
}
|
|
547
|
-
if (missingCaptures.length > 0) {
|
|
548
|
-
const dependencyNames = dependencies.map(({ name }) => name);
|
|
549
|
-
const missingPaths = missingCaptures.map(({ usagePath }) => usagePath);
|
|
550
|
-
const newDependencies = [...dependencyNames, ...missingPaths].toSorted();
|
|
551
|
-
const newDependenciesString = `[${newDependencies.join(", ")}]`;
|
|
552
|
-
const lastDependency = dependencies.at(-1);
|
|
553
|
-
const firstMissing = missingCaptures.at(0);
|
|
554
|
-
if (missingCaptures.length === 1 && firstMissing) {
|
|
555
|
-
context.report({
|
|
556
|
-
data: { name: firstMissing.usagePath },
|
|
557
|
-
messageId: "missingDependency",
|
|
558
|
-
node: lastDependency?.node || dependenciesArray,
|
|
559
|
-
suggest: [
|
|
560
|
-
{
|
|
561
|
-
desc: `Add '${firstMissing.usagePath}' to dependencies array`,
|
|
562
|
-
fix(fixer) {
|
|
563
|
-
return fixer.replaceText(dependenciesArray, newDependenciesString);
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
const missingNames = missingPaths.join(", ");
|
|
571
|
-
context.report({
|
|
572
|
-
data: { names: missingNames },
|
|
573
|
-
messageId: "missingDependencies",
|
|
574
|
-
node: lastDependency?.node || dependenciesArray,
|
|
575
|
-
suggest: [
|
|
576
|
-
{
|
|
577
|
-
desc: "Add missing dependencies to array",
|
|
578
|
-
fix(fixer) {
|
|
579
|
-
return fixer.replaceText(dependenciesArray, newDependenciesString);
|
|
580
|
-
},
|
|
581
|
-
},
|
|
582
|
-
],
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
for (const capture of captures) {
|
|
587
|
-
if (isStableValue(capture.variable, capture.name, stableHooks))
|
|
588
|
-
continue;
|
|
589
|
-
const rootIdentifier = getRootIdentifier(capture.node);
|
|
590
|
-
if (!rootIdentifier)
|
|
591
|
-
continue;
|
|
592
|
-
const captureName = rootIdentifier.name;
|
|
593
|
-
for (const dependency of dependencies) {
|
|
594
|
-
const dependencyRootIdentifier = getRootIdentifier(dependency.node);
|
|
595
|
-
const isMatch = dependencyRootIdentifier?.name === captureName && dependency.depth === capture.depth;
|
|
596
|
-
const isDirectIdentifier = dependency.depth === 0;
|
|
597
|
-
if (isMatch && isDirectIdentifier) {
|
|
598
|
-
const variableDefinition = capture.variable?.defs[0];
|
|
599
|
-
const initialNode = variableDefinition?.node.type === "VariableDeclarator"
|
|
600
|
-
? (variableDefinition.node.init ?? undefined)
|
|
601
|
-
: undefined;
|
|
602
|
-
if (isUnstableValue(initialNode)) {
|
|
603
|
-
context.report({
|
|
604
|
-
data: { name: capture.usagePath },
|
|
605
|
-
messageId: "unstableDependency",
|
|
606
|
-
node: dependency.node,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
if (isMatch)
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
},
|
|
616
|
-
};
|
|
617
|
-
},
|
|
618
|
-
meta: {
|
|
619
|
-
docs: {
|
|
620
|
-
description: "Enforce exhaustive and correct dependency specification in React hooks to prevent stale closures and unnecessary re-renders",
|
|
621
|
-
recommended: true,
|
|
622
|
-
url: "https://biomejs.dev/linter/rules/use-exhaustive-dependencies/",
|
|
623
|
-
},
|
|
624
|
-
fixable: "code",
|
|
625
|
-
hasSuggestions: true,
|
|
626
|
-
messages: {
|
|
627
|
-
missingDependencies: "This hook does not specify all its dependencies. Missing: {{names}}",
|
|
628
|
-
missingDependenciesArray: "This hook does not specify its dependencies array. Missing: {{deps}}",
|
|
629
|
-
missingDependency: "This hook does not specify its dependency on {{name}}.",
|
|
630
|
-
unnecessaryDependency: "This dependency {{name}} can be removed from the list.",
|
|
631
|
-
unstableDependency: "{{name}} changes on every re-render. Wrap the definition in useCallback() or useMemo() to stabilize it.",
|
|
632
|
-
},
|
|
633
|
-
schema: [
|
|
634
|
-
{
|
|
635
|
-
additionalProperties: false,
|
|
636
|
-
properties: {
|
|
637
|
-
hooks: {
|
|
638
|
-
description: "Array of custom hook entries to check for exhaustive dependencies",
|
|
639
|
-
items: {
|
|
640
|
-
additionalProperties: false,
|
|
641
|
-
properties: {
|
|
642
|
-
closureIndex: {
|
|
643
|
-
description: "Index of the closure argument for dependency validation",
|
|
644
|
-
type: "number",
|
|
645
|
-
},
|
|
646
|
-
dependenciesIndex: {
|
|
647
|
-
description: "Index of the dependencies array for validation",
|
|
648
|
-
type: "number",
|
|
649
|
-
},
|
|
650
|
-
name: {
|
|
651
|
-
description: "The name of the hook",
|
|
652
|
-
type: "string",
|
|
653
|
-
},
|
|
654
|
-
stableResult: {
|
|
655
|
-
description: "Specify stable results: true (whole result), number (array index), number[] (multiple indices), or string[] (object properties)",
|
|
656
|
-
oneOf: [
|
|
657
|
-
{ type: "boolean" },
|
|
658
|
-
{ type: "number" },
|
|
659
|
-
{ items: { type: "number" }, type: "array" },
|
|
660
|
-
{ items: { type: "string" }, type: "array" },
|
|
661
|
-
],
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
|
-
required: ["name"],
|
|
665
|
-
type: "object",
|
|
666
|
-
},
|
|
667
|
-
type: "array",
|
|
668
|
-
},
|
|
669
|
-
reportMissingDependenciesArray: {
|
|
670
|
-
default: true,
|
|
671
|
-
description: "Report when the dependencies array is completely missing",
|
|
672
|
-
type: "boolean",
|
|
673
|
-
},
|
|
674
|
-
reportUnnecessaryDependencies: {
|
|
675
|
-
default: true,
|
|
676
|
-
description: "Report when unnecessary dependencies are specified",
|
|
677
|
-
type: "boolean",
|
|
678
|
-
},
|
|
679
|
-
},
|
|
680
|
-
type: "object",
|
|
681
|
-
},
|
|
682
|
-
],
|
|
683
|
-
type: "problem",
|
|
684
|
-
},
|
|
685
|
-
};
|
|
686
|
-
export const __testing = {
|
|
687
|
-
collectCaptures,
|
|
688
|
-
convertStableResult,
|
|
689
|
-
isDeclaredInComponentBody,
|
|
690
|
-
isInTypePosition,
|
|
691
|
-
isStableArrayIndex,
|
|
692
|
-
isStableValue,
|
|
693
|
-
metrics: testingMetrics,
|
|
694
|
-
resetMetrics: resetTestingMetrics,
|
|
695
|
-
};
|
|
696
|
-
export default useExhaustiveDependencies;
|
|
697
|
-
//# sourceMappingURL=use-exhaustive-dependencies.js.map
|