eslint-plugin-react-hooks-extra 2.0.0-beta.172 → 2.0.0-beta.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +70 -444
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getConfigAdapters, getDocsUrl
|
|
1
|
+
import { getConfigAdapters, getDocsUrl } from "@eslint-react/shared";
|
|
2
2
|
import * as AST from "@eslint-react/ast";
|
|
3
3
|
import * as ER from "@eslint-react/core";
|
|
4
|
-
import { constVoid, getOrElseUpdate,
|
|
4
|
+
import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff";
|
|
5
5
|
import * as VAR from "@eslint-react/var";
|
|
6
6
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
7
7
|
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
@@ -30,19 +30,48 @@ const rules = { "react-hooks-extra/no-direct-set-state-in-use-effect": "warn" };
|
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region package.json
|
|
32
32
|
var name = "eslint-plugin-react-hooks-extra";
|
|
33
|
-
var version = "2.0.0-beta.
|
|
33
|
+
var version = "2.0.0-beta.173";
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
|
-
//#region src/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
//#region src/utils/create-rule.ts
|
|
37
|
+
const createRule = ESLintUtils.RuleCreator(getDocsUrl("hooks-extra"));
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/utils/is-variable-declarator-from-hook-call.ts
|
|
41
|
+
function isInitFromHookCall(init) {
|
|
42
|
+
if (init?.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
43
|
+
switch (init.callee.type) {
|
|
44
|
+
case AST_NODE_TYPES.Identifier: return ER.isReactHookName(init.callee.name);
|
|
45
|
+
case AST_NODE_TYPES.MemberExpression: return init.callee.property.type === AST_NODE_TYPES.Identifier && ER.isReactHookName(init.callee.property.name);
|
|
46
|
+
default: return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isVariableDeclaratorFromHookCall(node) {
|
|
50
|
+
if (node.type !== AST_NODE_TYPES.VariableDeclarator) return false;
|
|
51
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier) return false;
|
|
52
|
+
return isInitFromHookCall(node.init);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/rules/no-direct-set-state-in-use-effect.ts
|
|
57
|
+
const RULE_NAME = "no-direct-set-state-in-use-effect";
|
|
58
|
+
const RULE_FEATURES = ["EXP"];
|
|
59
|
+
var no_direct_set_state_in_use_effect_default = createRule({
|
|
60
|
+
meta: {
|
|
61
|
+
type: "problem",
|
|
62
|
+
docs: {
|
|
63
|
+
description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
|
|
64
|
+
[Symbol.for("rule_features")]: RULE_FEATURES
|
|
65
|
+
},
|
|
66
|
+
messages: { noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'." },
|
|
67
|
+
schema: []
|
|
68
|
+
},
|
|
69
|
+
name: RULE_NAME,
|
|
70
|
+
create,
|
|
71
|
+
defaultOptions: []
|
|
72
|
+
});
|
|
73
|
+
function create(context) {
|
|
74
|
+
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
46
75
|
const functionEntries = [];
|
|
47
76
|
const setupFnRef = { current: null };
|
|
48
77
|
const setupFnIds = [];
|
|
@@ -51,6 +80,7 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
51
80
|
const setStateInEffectArg = /* @__PURE__ */ new WeakMap();
|
|
52
81
|
const setStateInEffectSetup = /* @__PURE__ */ new Map();
|
|
53
82
|
const setStateInHookCallbacks = /* @__PURE__ */ new WeakMap();
|
|
83
|
+
const getText = (n) => context.sourceCode.getText(n);
|
|
54
84
|
const onSetupFunctionEnter = (node) => {
|
|
55
85
|
setupFnRef.current = node;
|
|
56
86
|
};
|
|
@@ -58,14 +88,14 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
58
88
|
if (setupFnRef.current === node) setupFnRef.current = null;
|
|
59
89
|
};
|
|
60
90
|
function isFunctionOfUseEffectSetup(node) {
|
|
61
|
-
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node &&
|
|
91
|
+
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && ER.isUseEffectCall(node.parent);
|
|
62
92
|
}
|
|
63
93
|
function getCallName(node) {
|
|
64
94
|
if (node.type === AST_NODE_TYPES.CallExpression) return AST.toStringFormat(node.callee, getText);
|
|
65
95
|
return AST.toStringFormat(node, getText);
|
|
66
96
|
}
|
|
67
97
|
function getCallKind(node) {
|
|
68
|
-
return match(node).when(isUseStateCall, () => "useState").when(isUseEffectLikeCall, () =>
|
|
98
|
+
return match(node).when(ER.isUseStateCall, () => "useState").when(ER.isUseEffectLikeCall, () => "useEffect").when(isSetStateCall, () => "setState").when(AST.isThenCall, () => "then").otherwise(() => "other");
|
|
69
99
|
}
|
|
70
100
|
function getFunctionKind(node) {
|
|
71
101
|
const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
|
|
@@ -82,7 +112,7 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
82
112
|
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
83
113
|
if (variableNode == null) return false;
|
|
84
114
|
if (variableNode.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
85
|
-
if (!ER.
|
|
115
|
+
if (!ER.isUseStateCall(variableNode)) return false;
|
|
86
116
|
const variableNodeParent = variableNode.parent;
|
|
87
117
|
if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
|
|
88
118
|
return variableNodeParent.id.elements.findIndex((e) => e?.type === AST_NODE_TYPES.Identifier && e.name === topLevelId.name) === at;
|
|
@@ -135,7 +165,11 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
135
165
|
case pEntry.node.async: break;
|
|
136
166
|
case pEntry.node === setupFunction:
|
|
137
167
|
case pEntry.kind === "immediate" && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction:
|
|
138
|
-
|
|
168
|
+
context.report({
|
|
169
|
+
messageId: "noDirectSetStateInUseEffect",
|
|
170
|
+
node,
|
|
171
|
+
data: { name: context.sourceCode.getText(node.callee) }
|
|
172
|
+
});
|
|
139
173
|
return;
|
|
140
174
|
default: {
|
|
141
175
|
const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall);
|
|
@@ -143,7 +177,7 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
143
177
|
else getOrElseUpdate(setStateInHookCallbacks, vd.init, () => []).push(node);
|
|
144
178
|
}
|
|
145
179
|
}
|
|
146
|
-
}).with(
|
|
180
|
+
}).with("useEffect", () => {
|
|
147
181
|
if (AST.isFunction(node.arguments.at(0))) return;
|
|
148
182
|
setupFnIds.push(...AST.getNestedIdentifiers(node));
|
|
149
183
|
}).with("other", () => {
|
|
@@ -158,19 +192,19 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
158
192
|
case AST_NODE_TYPES.ArrowFunctionExpression: {
|
|
159
193
|
const parent = node.parent.parent;
|
|
160
194
|
if (parent.type !== AST_NODE_TYPES.CallExpression) break;
|
|
161
|
-
if (!isUseMemoCall(parent)) break;
|
|
195
|
+
if (!ER.isUseMemoCall(parent)) break;
|
|
162
196
|
const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall);
|
|
163
197
|
if (vd != null) getOrElseUpdate(setStateInEffectArg, vd.init, () => []).push(node);
|
|
164
198
|
break;
|
|
165
199
|
}
|
|
166
200
|
case AST_NODE_TYPES.CallExpression:
|
|
167
201
|
if (node !== node.parent.arguments.at(0)) break;
|
|
168
|
-
if (isUseCallbackCall(node.parent)) {
|
|
202
|
+
if (ER.isUseCallbackCall(node.parent)) {
|
|
169
203
|
const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
|
|
170
204
|
if (vd != null) getOrElseUpdate(setStateInEffectArg, vd.init, () => []).push(node);
|
|
171
205
|
break;
|
|
172
206
|
}
|
|
173
|
-
if (
|
|
207
|
+
if (ER.isUseEffectCall(node.parent)) getOrElseUpdate(setStateInEffectSetup, node.parent, () => []).push(node);
|
|
174
208
|
}
|
|
175
209
|
},
|
|
176
210
|
"Program:exit"() {
|
|
@@ -184,433 +218,32 @@ function useNoDirectSetStateInUseEffect(context, options) {
|
|
|
184
218
|
}
|
|
185
219
|
return [];
|
|
186
220
|
};
|
|
187
|
-
for (const [, calls] of setStateInEffectSetup) for (const call of calls)
|
|
221
|
+
for (const [, calls] of setStateInEffectSetup) for (const call of calls) context.report({
|
|
222
|
+
messageId: "noDirectSetStateInUseEffect",
|
|
223
|
+
node: call,
|
|
224
|
+
data: { name: call.name }
|
|
225
|
+
});
|
|
188
226
|
for (const { callee } of trackedFnCalls) {
|
|
189
227
|
if (!("name" in callee)) continue;
|
|
190
228
|
const { name: name$2 } = callee;
|
|
191
229
|
const setStateCalls = getSetStateCalls(name$2, context.sourceCode.getScope(callee));
|
|
192
|
-
for (const setStateCall of setStateCalls)
|
|
230
|
+
for (const setStateCall of setStateCalls) context.report({
|
|
231
|
+
messageId: "noDirectSetStateInUseEffect",
|
|
232
|
+
node: setStateCall,
|
|
233
|
+
data: { name: getCallName(setStateCall) }
|
|
234
|
+
});
|
|
193
235
|
}
|
|
194
236
|
for (const id of setupFnIds) {
|
|
195
237
|
const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
|
|
196
|
-
for (const setStateCall of setStateCalls)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
function isInitFromHookCall(init) {
|
|
202
|
-
if (init?.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
203
|
-
switch (init.callee.type) {
|
|
204
|
-
case AST_NODE_TYPES.Identifier: return ER.isReactHookName(init.callee.name);
|
|
205
|
-
case AST_NODE_TYPES.MemberExpression: return init.callee.property.type === AST_NODE_TYPES.Identifier && ER.isReactHookName(init.callee.property.name);
|
|
206
|
-
default: return false;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function isVariableDeclaratorFromHookCall(node) {
|
|
210
|
-
if (node.type !== AST_NODE_TYPES.VariableDeclarator) return false;
|
|
211
|
-
if (node.id.type !== AST_NODE_TYPES.Identifier) return false;
|
|
212
|
-
return isInitFromHookCall(node.init);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
//#endregion
|
|
216
|
-
//#region src/utils/create-rule.ts
|
|
217
|
-
const createRule = ESLintUtils.RuleCreator(getDocsUrl("hooks-extra"));
|
|
218
|
-
|
|
219
|
-
//#endregion
|
|
220
|
-
//#region src/rules/no-direct-set-state-in-use-effect.ts
|
|
221
|
-
const RULE_NAME$5 = "no-direct-set-state-in-use-effect";
|
|
222
|
-
const RULE_FEATURES$5 = ["EXP"];
|
|
223
|
-
var no_direct_set_state_in_use_effect_default = createRule({
|
|
224
|
-
meta: {
|
|
225
|
-
type: "problem",
|
|
226
|
-
docs: {
|
|
227
|
-
description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
|
|
228
|
-
[Symbol.for("rule_features")]: RULE_FEATURES$5
|
|
229
|
-
},
|
|
230
|
-
messages: { noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'." },
|
|
231
|
-
schema: []
|
|
232
|
-
},
|
|
233
|
-
name: RULE_NAME$5,
|
|
234
|
-
create: create$5,
|
|
235
|
-
defaultOptions: []
|
|
236
|
-
});
|
|
237
|
-
function create$5(context) {
|
|
238
|
-
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
239
|
-
return useNoDirectSetStateInUseEffect(context, {
|
|
240
|
-
onViolation(ctx, node, data) {
|
|
241
|
-
ctx.report({
|
|
242
|
-
messageId: "noDirectSetStateInUseEffect",
|
|
243
|
-
node,
|
|
244
|
-
data
|
|
245
|
-
});
|
|
246
|
-
},
|
|
247
|
-
useEffectKind: "useEffect"
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/rules/no-direct-set-state-in-use-layout-effect.ts
|
|
253
|
-
const RULE_NAME$4 = "no-direct-set-state-in-use-layout-effect";
|
|
254
|
-
const RULE_FEATURES$4 = ["EXP"];
|
|
255
|
-
var no_direct_set_state_in_use_layout_effect_default = createRule({
|
|
256
|
-
meta: {
|
|
257
|
-
type: "problem",
|
|
258
|
-
docs: {
|
|
259
|
-
description: "Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`.",
|
|
260
|
-
[Symbol.for("rule_features")]: RULE_FEATURES$4
|
|
261
|
-
},
|
|
262
|
-
messages: { noDirectSetStateInUseLayoutEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useLayoutEffect'." },
|
|
263
|
-
schema: []
|
|
264
|
-
},
|
|
265
|
-
name: RULE_NAME$4,
|
|
266
|
-
create: create$4,
|
|
267
|
-
defaultOptions: []
|
|
268
|
-
});
|
|
269
|
-
function create$4(context) {
|
|
270
|
-
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
271
|
-
return useNoDirectSetStateInUseEffect(context, {
|
|
272
|
-
onViolation(ctx, node, data) {
|
|
273
|
-
ctx.report({
|
|
274
|
-
messageId: "noDirectSetStateInUseLayoutEffect",
|
|
275
|
-
node,
|
|
276
|
-
data
|
|
277
|
-
});
|
|
278
|
-
},
|
|
279
|
-
useEffectKind: "useLayoutEffect"
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
//#endregion
|
|
284
|
-
//#region src/rules-removed/no-unnecessary-use-callback.ts
|
|
285
|
-
const RULE_NAME$3 = "no-unnecessary-use-callback";
|
|
286
|
-
const RULE_FEATURES$3 = ["EXP"];
|
|
287
|
-
var no_unnecessary_use_callback_default = createRule({
|
|
288
|
-
meta: {
|
|
289
|
-
type: "problem",
|
|
290
|
-
deprecated: {
|
|
291
|
-
deprecatedSince: "2.0.0",
|
|
292
|
-
replacedBy: [{
|
|
293
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
294
|
-
plugin: {
|
|
295
|
-
name: "eslint-plugin-react-x",
|
|
296
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
|
|
297
|
-
},
|
|
298
|
-
rule: {
|
|
299
|
-
name: "no-unnecessary-use-callback",
|
|
300
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
|
|
301
|
-
}
|
|
302
|
-
}, {
|
|
303
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
304
|
-
plugin: {
|
|
305
|
-
name: "@eslint-react/eslint-plugin",
|
|
306
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
|
|
307
|
-
},
|
|
308
|
-
rule: {
|
|
309
|
-
name: "no-unnecessary-use-callback",
|
|
310
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
|
|
311
|
-
}
|
|
312
|
-
}]
|
|
313
|
-
},
|
|
314
|
-
docs: {
|
|
315
|
-
description: "Disallow unnecessary usage of `useCallback`.",
|
|
316
|
-
[Symbol.for("rule_features")]: RULE_FEATURES$3
|
|
317
|
-
},
|
|
318
|
-
messages: { noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary." },
|
|
319
|
-
schema: []
|
|
320
|
-
},
|
|
321
|
-
name: RULE_NAME$3,
|
|
322
|
-
create: create$3,
|
|
323
|
-
defaultOptions: []
|
|
324
|
-
});
|
|
325
|
-
function create$3(context) {
|
|
326
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
327
|
-
const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? [];
|
|
328
|
-
const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", alias);
|
|
329
|
-
return { CallExpression(node) {
|
|
330
|
-
if (!ER.isReactHookCall(node)) return;
|
|
331
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
332
|
-
if (!isUseCallbackCall(node)) return;
|
|
333
|
-
const scope = context.sourceCode.getScope(node);
|
|
334
|
-
const component = scope.block;
|
|
335
|
-
if (!AST.isFunction(component)) return;
|
|
336
|
-
const [arg0, arg1] = node.arguments;
|
|
337
|
-
if (arg0 == null || arg1 == null) return;
|
|
338
|
-
const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
339
|
-
const variable = VAR.findVariable(n.name, initialScope);
|
|
340
|
-
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
341
|
-
if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) return false;
|
|
342
|
-
return variableNode.elements.length === 0;
|
|
343
|
-
}).otherwise(() => false);
|
|
344
|
-
if (!hasEmptyDeps) return;
|
|
345
|
-
const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
346
|
-
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) return n.body;
|
|
347
|
-
return n;
|
|
348
|
-
}).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
349
|
-
const variable = VAR.findVariable(n.name, initialScope);
|
|
350
|
-
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
351
|
-
if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) return null;
|
|
352
|
-
return variableNode;
|
|
353
|
-
}).otherwise(() => null);
|
|
354
|
-
if (arg0Node == null) return;
|
|
355
|
-
const arg0NodeScope = context.sourceCode.getScope(arg0Node);
|
|
356
|
-
const arg0NodeReferences = VAR.getChildScopes(arg0NodeScope).flatMap((x) => x.references);
|
|
357
|
-
const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
|
|
358
|
-
if (!isReferencedToComponentScope) context.report({
|
|
359
|
-
messageId: "noUnnecessaryUseCallback",
|
|
360
|
-
node
|
|
361
|
-
});
|
|
362
|
-
} };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/rules-removed/no-unnecessary-use-memo.ts
|
|
367
|
-
const RULE_NAME$2 = "no-unnecessary-use-memo";
|
|
368
|
-
const RULE_FEATURES$2 = ["EXP"];
|
|
369
|
-
var no_unnecessary_use_memo_default = createRule({
|
|
370
|
-
meta: {
|
|
371
|
-
type: "problem",
|
|
372
|
-
deprecated: {
|
|
373
|
-
deprecatedSince: "2.0.0",
|
|
374
|
-
replacedBy: [{
|
|
375
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
376
|
-
plugin: {
|
|
377
|
-
name: "eslint-plugin-react-x",
|
|
378
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
|
|
379
|
-
},
|
|
380
|
-
rule: {
|
|
381
|
-
name: "no-unnecessary-use-memo",
|
|
382
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
|
|
383
|
-
}
|
|
384
|
-
}, {
|
|
385
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
386
|
-
plugin: {
|
|
387
|
-
name: "@eslint-react/eslint-plugin",
|
|
388
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
|
|
389
|
-
},
|
|
390
|
-
rule: {
|
|
391
|
-
name: "no-unnecessary-use-memo",
|
|
392
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
|
|
393
|
-
}
|
|
394
|
-
}]
|
|
395
|
-
},
|
|
396
|
-
docs: {
|
|
397
|
-
description: "Disallow unnecessary usage of `useMemo`.",
|
|
398
|
-
[Symbol.for("rule_features")]: RULE_FEATURES$2
|
|
399
|
-
},
|
|
400
|
-
messages: { noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary." },
|
|
401
|
-
schema: []
|
|
402
|
-
},
|
|
403
|
-
name: RULE_NAME$2,
|
|
404
|
-
create: create$2,
|
|
405
|
-
defaultOptions: []
|
|
406
|
-
});
|
|
407
|
-
function create$2(context) {
|
|
408
|
-
if (!context.sourceCode.text.includes("use")) return {};
|
|
409
|
-
const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? [];
|
|
410
|
-
const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", alias);
|
|
411
|
-
return { CallExpression(node) {
|
|
412
|
-
if (!ER.isReactHookCall(node)) return;
|
|
413
|
-
const initialScope = context.sourceCode.getScope(node);
|
|
414
|
-
if (!isUseMemoCall(node)) return;
|
|
415
|
-
const scope = context.sourceCode.getScope(node);
|
|
416
|
-
const component = scope.block;
|
|
417
|
-
if (!AST.isFunction(component)) return;
|
|
418
|
-
const [arg0, arg1] = node.arguments;
|
|
419
|
-
if (arg0 == null || arg1 == null) return;
|
|
420
|
-
const hasCallInArg0 = AST.isFunction(arg0) && [...AST.getNestedCallExpressions(arg0.body), ...AST.getNestedNewExpressions(arg0.body)].length > 0;
|
|
421
|
-
if (hasCallInArg0) return;
|
|
422
|
-
const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
423
|
-
const variable = VAR.findVariable(n.name, initialScope);
|
|
424
|
-
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
425
|
-
if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) return false;
|
|
426
|
-
return variableNode.elements.length === 0;
|
|
427
|
-
}).otherwise(() => false);
|
|
428
|
-
if (!hasEmptyDeps) return;
|
|
429
|
-
const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
|
|
430
|
-
if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) return n.body;
|
|
431
|
-
return n;
|
|
432
|
-
}).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
|
|
433
|
-
const variable = VAR.findVariable(n.name, initialScope);
|
|
434
|
-
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
435
|
-
if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) return null;
|
|
436
|
-
return variableNode;
|
|
437
|
-
}).otherwise(() => null);
|
|
438
|
-
if (arg0Node == null) return;
|
|
439
|
-
const arg0NodeScope = context.sourceCode.getScope(arg0Node);
|
|
440
|
-
const arg0NodeReferences = VAR.getChildScopes(arg0NodeScope).flatMap((x) => x.references);
|
|
441
|
-
const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
|
|
442
|
-
if (!isReferencedToComponentScope) context.report({
|
|
443
|
-
messageId: "noUnnecessaryUseMemo",
|
|
444
|
-
node
|
|
445
|
-
});
|
|
446
|
-
} };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/rules-removed/no-unnecessary-use-prefix.ts
|
|
451
|
-
const RULE_NAME$1 = "no-unnecessary-use-prefix";
|
|
452
|
-
const RULE_FEATURES$1 = [];
|
|
453
|
-
const WELL_KNOWN_HOOKS = ["useMDXComponents"];
|
|
454
|
-
function containsUseComments(context, node) {
|
|
455
|
-
return context.sourceCode.getCommentsInside(node).some(({ value }) => /use\([\s\S]*?\)/u.test(value) || /use[A-Z0-9]\w*\([\s\S]*?\)/u.test(value));
|
|
456
|
-
}
|
|
457
|
-
var no_unnecessary_use_prefix_default = createRule({
|
|
458
|
-
meta: {
|
|
459
|
-
type: "problem",
|
|
460
|
-
deprecated: {
|
|
461
|
-
deprecatedSince: "2.0.0",
|
|
462
|
-
replacedBy: [{
|
|
463
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
464
|
-
plugin: {
|
|
465
|
-
name: "eslint-plugin-react-x",
|
|
466
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
|
|
467
|
-
},
|
|
468
|
-
rule: {
|
|
469
|
-
name: "no-unnecessary-use-prefix",
|
|
470
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
|
|
471
|
-
}
|
|
472
|
-
}, {
|
|
473
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
474
|
-
plugin: {
|
|
475
|
-
name: "@eslint-react/eslint-plugin",
|
|
476
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
|
|
477
|
-
},
|
|
478
|
-
rule: {
|
|
479
|
-
name: "no-unnecessary-use-prefix",
|
|
480
|
-
url: "https://eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
|
|
481
|
-
}
|
|
482
|
-
}]
|
|
483
|
-
},
|
|
484
|
-
docs: {
|
|
485
|
-
description: "Enforces that a function with the `use` prefix should use at least one Hook inside of it.",
|
|
486
|
-
[Symbol.for("rule_features")]: RULE_FEATURES$1
|
|
487
|
-
},
|
|
488
|
-
messages: { noUnnecessaryUsePrefix: "If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix." },
|
|
489
|
-
schema: []
|
|
490
|
-
},
|
|
491
|
-
name: RULE_NAME$1,
|
|
492
|
-
create: create$1,
|
|
493
|
-
defaultOptions: []
|
|
494
|
-
});
|
|
495
|
-
function create$1(context) {
|
|
496
|
-
const { ctx, listeners } = ER.useHookCollector();
|
|
497
|
-
return {
|
|
498
|
-
...listeners,
|
|
499
|
-
"Program:exit"(program) {
|
|
500
|
-
const allHooks = ctx.getAllHooks(program);
|
|
501
|
-
for (const { id, name: name$2, node, hookCalls } of allHooks.values()) {
|
|
502
|
-
if (WELL_KNOWN_HOOKS.includes(name$2)) continue;
|
|
503
|
-
if (AST.isFunctionEmpty(node)) continue;
|
|
504
|
-
if (hookCalls.length > 0) continue;
|
|
505
|
-
if (containsUseComments(context, node)) continue;
|
|
506
|
-
if (id != null) {
|
|
507
|
-
context.report({
|
|
508
|
-
messageId: "noUnnecessaryUsePrefix",
|
|
509
|
-
data: { name: name$2 },
|
|
510
|
-
loc: getPreferredLoc(context, id)
|
|
511
|
-
});
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
context.report({
|
|
515
|
-
messageId: "noUnnecessaryUsePrefix",
|
|
516
|
-
node,
|
|
517
|
-
data: { name: name$2 }
|
|
238
|
+
for (const setStateCall of setStateCalls) context.report({
|
|
239
|
+
messageId: "noDirectSetStateInUseEffect",
|
|
240
|
+
node: setStateCall,
|
|
241
|
+
data: { name: getCallName(setStateCall) }
|
|
518
242
|
});
|
|
519
243
|
}
|
|
520
244
|
}
|
|
521
245
|
};
|
|
522
246
|
}
|
|
523
|
-
function getPreferredLoc(context, id) {
|
|
524
|
-
if (AST.isMultiLine(id)) return id.loc;
|
|
525
|
-
if (!context.sourceCode.getText(id).startsWith("use")) return id.loc;
|
|
526
|
-
return {
|
|
527
|
-
end: {
|
|
528
|
-
column: id.loc.start.column + 3,
|
|
529
|
-
line: id.loc.start.line
|
|
530
|
-
},
|
|
531
|
-
start: {
|
|
532
|
-
column: id.loc.start.column,
|
|
533
|
-
line: id.loc.start.line
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
//#endregion
|
|
539
|
-
//#region src/rules-removed/prefer-use-state-lazy-initialization.ts
|
|
540
|
-
const RULE_NAME = "prefer-use-state-lazy-initialization";
|
|
541
|
-
const RULE_FEATURES = ["EXP"];
|
|
542
|
-
const ALLOW_LIST = [
|
|
543
|
-
"Boolean",
|
|
544
|
-
"String",
|
|
545
|
-
"Number"
|
|
546
|
-
];
|
|
547
|
-
var prefer_use_state_lazy_initialization_default = createRule({
|
|
548
|
-
meta: {
|
|
549
|
-
type: "problem",
|
|
550
|
-
deprecated: {
|
|
551
|
-
deprecatedSince: "2.0.0",
|
|
552
|
-
replacedBy: [{
|
|
553
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
554
|
-
plugin: {
|
|
555
|
-
name: "eslint-plugin-react-x",
|
|
556
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
|
|
557
|
-
},
|
|
558
|
-
rule: {
|
|
559
|
-
name: "prefer-use-state-lazy-initialization",
|
|
560
|
-
url: "https://eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
|
|
561
|
-
}
|
|
562
|
-
}, {
|
|
563
|
-
message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
|
|
564
|
-
plugin: {
|
|
565
|
-
name: "@eslint-react/eslint-plugin",
|
|
566
|
-
url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
|
|
567
|
-
},
|
|
568
|
-
rule: {
|
|
569
|
-
name: "prefer-use-state-lazy-initialization",
|
|
570
|
-
url: "https://eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
|
|
571
|
-
}
|
|
572
|
-
}]
|
|
573
|
-
},
|
|
574
|
-
docs: {
|
|
575
|
-
description: "Enforces function calls made inside `useState` to be wrapped in an `initializer function`.",
|
|
576
|
-
[Symbol.for("rule_features")]: RULE_FEATURES
|
|
577
|
-
},
|
|
578
|
-
messages: { preferUseStateLazyInitialization: "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'." },
|
|
579
|
-
schema: []
|
|
580
|
-
},
|
|
581
|
-
name: RULE_NAME,
|
|
582
|
-
create,
|
|
583
|
-
defaultOptions: []
|
|
584
|
-
});
|
|
585
|
-
function create(context) {
|
|
586
|
-
const alias = getSettingsFromContext(context).additionalHooks.useState ?? [];
|
|
587
|
-
const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias);
|
|
588
|
-
return { CallExpression(node) {
|
|
589
|
-
if (!ER.isReactHookCall(node)) return;
|
|
590
|
-
if (!isUseStateCall(node)) return;
|
|
591
|
-
const [useStateInput] = node.arguments;
|
|
592
|
-
if (useStateInput == null) return;
|
|
593
|
-
for (const expr of AST.getNestedNewExpressions(useStateInput)) {
|
|
594
|
-
if (!("name" in expr.callee)) continue;
|
|
595
|
-
if (ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
596
|
-
if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
|
|
597
|
-
context.report({
|
|
598
|
-
messageId: "preferUseStateLazyInitialization",
|
|
599
|
-
node: expr
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
for (const expr of AST.getNestedCallExpressions(useStateInput)) {
|
|
603
|
-
if (!("name" in expr.callee)) continue;
|
|
604
|
-
if (ER.isReactHookName(expr.callee.name)) continue;
|
|
605
|
-
if (ALLOW_LIST.includes(expr.callee.name)) continue;
|
|
606
|
-
if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
|
|
607
|
-
context.report({
|
|
608
|
-
messageId: "preferUseStateLazyInitialization",
|
|
609
|
-
node: expr
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
} };
|
|
613
|
-
}
|
|
614
247
|
|
|
615
248
|
//#endregion
|
|
616
249
|
//#region src/plugin.ts
|
|
@@ -619,14 +252,7 @@ const plugin = {
|
|
|
619
252
|
name,
|
|
620
253
|
version
|
|
621
254
|
},
|
|
622
|
-
rules: {
|
|
623
|
-
"no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default,
|
|
624
|
-
"no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_layout_effect_default,
|
|
625
|
-
"no-unnecessary-use-callback": no_unnecessary_use_callback_default,
|
|
626
|
-
"no-unnecessary-use-memo": no_unnecessary_use_memo_default,
|
|
627
|
-
"no-unnecessary-use-prefix": no_unnecessary_use_prefix_default,
|
|
628
|
-
"prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
|
|
629
|
-
}
|
|
255
|
+
rules: { "no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default }
|
|
630
256
|
};
|
|
631
257
|
|
|
632
258
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-hooks-extra",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.173",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for React Hooks related rules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"@typescript-eslint/utils": "^8.42.0",
|
|
43
43
|
"string-ts": "^2.2.1",
|
|
44
44
|
"ts-pattern": "^5.8.0",
|
|
45
|
-
"@eslint-react/
|
|
46
|
-
"@eslint-react/
|
|
47
|
-
"@eslint-react/kit": "2.0.0-beta.
|
|
48
|
-
"@eslint-react/shared": "2.0.0-beta.
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
45
|
+
"@eslint-react/ast": "2.0.0-beta.173",
|
|
46
|
+
"@eslint-react/core": "2.0.0-beta.173",
|
|
47
|
+
"@eslint-react/kit": "2.0.0-beta.173",
|
|
48
|
+
"@eslint-react/shared": "2.0.0-beta.173",
|
|
49
|
+
"@eslint-react/var": "2.0.0-beta.173",
|
|
50
|
+
"@eslint-react/eff": "2.0.0-beta.173"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react": "^19.1.12",
|