eslint-plugin-react-hooks-extra 2.0.0-next.45 → 2.0.0-next.48

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.
Files changed (2) hide show
  1. package/dist/index.js +556 -558
  2. package/package.json +11 -11
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import * as AST5 from '@eslint-react/ast';
2
- import * as ER5 from '@eslint-react/core';
1
+ import * as AST from '@eslint-react/ast';
2
+ import * as ER from '@eslint-react/core';
3
3
  import { identity, getOrElseUpdate, constVoid, not } from '@eslint-react/eff';
4
4
  import { getDocsUrl, getSettingsFromContext } from '@eslint-react/shared';
5
- import * as VAR3 from '@eslint-react/var';
5
+ import * as VAR from '@eslint-react/var';
6
6
  import { AST_NODE_TYPES } from '@typescript-eslint/types';
7
7
  import { match } from 'ts-pattern';
8
8
  import { ESLintUtils } from '@typescript-eslint/utils';
@@ -27,50 +27,279 @@ var rules = {
27
27
 
28
28
  // package.json
29
29
  var name2 = "eslint-plugin-react-hooks-extra";
30
- var version = "2.0.0-next.45";
30
+ var version = "2.0.0-next.48";
31
+ function useNoDirectSetStateInUseEffect(context, options) {
32
+ const { onViolation, useEffectKind } = options;
33
+ const settings = getSettingsFromContext(context);
34
+ const hooks = settings.additionalHooks;
35
+ const getText = (n) => context.sourceCode.getText(n);
36
+ const isUseEffectLikeCall = ER.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]);
37
+ const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState);
38
+ const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo);
39
+ const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", hooks.useCallback);
40
+ const functionEntries = [];
41
+ const setupFunctionRef = { current: null };
42
+ const setupFunctionIdentifiers = [];
43
+ const indFunctionCalls = [];
44
+ const indSetStateCalls = /* @__PURE__ */ new WeakMap();
45
+ const indSetStateCallsInUseEffectArg0 = /* @__PURE__ */ new WeakMap();
46
+ const indSetStateCallsInUseEffectSetup = /* @__PURE__ */ new Map();
47
+ const indSetStateCallsInUseMemoOrCallback = /* @__PURE__ */ new WeakMap();
48
+ const onSetupFunctionEnter = (node) => {
49
+ setupFunctionRef.current = node;
50
+ };
51
+ const onSetupFunctionExit = (node) => {
52
+ if (setupFunctionRef.current === node) {
53
+ setupFunctionRef.current = null;
54
+ }
55
+ };
56
+ function isFunctionOfUseEffectSetup(node) {
57
+ return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectLikeCall(node.parent);
58
+ }
59
+ function getCallName(node) {
60
+ if (node.type === AST_NODE_TYPES.CallExpression) {
61
+ return AST.toStringFormat(node.callee, getText);
62
+ }
63
+ return AST.toStringFormat(node, getText);
64
+ }
65
+ function getCallKind(node) {
66
+ return match(node).when(isUseStateCall, () => "useState").when(isUseEffectLikeCall, () => useEffectKind).when(isSetStateCall, () => "setState").when(AST.isThenCall, () => "then").otherwise(() => "other");
67
+ }
68
+ function getFunctionKind(node) {
69
+ const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
70
+ switch (true) {
71
+ case node.async:
72
+ case (parent.type === AST_NODE_TYPES.CallExpression && AST.isThenCall(parent)):
73
+ return "deferred";
74
+ case (node.type !== AST_NODE_TYPES.FunctionDeclaration && parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node):
75
+ return "immediate";
76
+ case isFunctionOfUseEffectSetup(node):
77
+ return "setup";
78
+ default:
79
+ return "other";
80
+ }
81
+ }
82
+ function isIdFromUseStateCall(topLevelId, at) {
83
+ const variable = VAR.findVariable(topLevelId, context.sourceCode.getScope(topLevelId));
84
+ const variableNode = VAR.getVariableInitNode(variable, 0);
85
+ if (variableNode == null) return false;
86
+ if (variableNode.type !== AST_NODE_TYPES.CallExpression) return false;
87
+ if (!ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState)(variableNode)) return false;
88
+ const variableNodeParent = variableNode.parent;
89
+ if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) {
90
+ return true;
91
+ }
92
+ return variableNodeParent.id.elements.findIndex((e) => e?.type === AST_NODE_TYPES.Identifier && e.name === topLevelId.name) === at;
93
+ }
94
+ function isSetStateCall(node) {
95
+ switch (node.callee.type) {
96
+ // const data = useState();
97
+ // data.at(1)();
98
+ case AST_NODE_TYPES.CallExpression: {
99
+ const { callee } = node.callee;
100
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) {
101
+ return false;
102
+ }
103
+ if (!("name" in callee.object)) {
104
+ return false;
105
+ }
106
+ const isAt = callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "at";
107
+ const [index] = node.callee.arguments;
108
+ if (!isAt || index == null) {
109
+ return false;
110
+ }
111
+ const indexScope = context.sourceCode.getScope(node);
112
+ const indexValue = VAR.toStaticValue({
113
+ kind: "lazy",
114
+ node: index,
115
+ initialScope: indexScope
116
+ }).value;
117
+ return indexValue === 1 && isIdFromUseStateCall(callee.object);
118
+ }
119
+ // const [data, setData] = useState();
120
+ // setData();
121
+ case AST_NODE_TYPES.Identifier: {
122
+ return isIdFromUseStateCall(node.callee, 1);
123
+ }
124
+ // const data = useState();
125
+ // data[1]();
126
+ case AST_NODE_TYPES.MemberExpression: {
127
+ if (!("name" in node.callee.object)) {
128
+ return false;
129
+ }
130
+ const property = node.callee.property;
131
+ const propertyScope = context.sourceCode.getScope(node);
132
+ const propertyValue = VAR.toStaticValue({
133
+ kind: "lazy",
134
+ node: property,
135
+ initialScope: propertyScope
136
+ }).value;
137
+ return propertyValue === 1 && isIdFromUseStateCall(node.callee.object, 1);
138
+ }
139
+ default: {
140
+ return false;
141
+ }
142
+ }
143
+ }
144
+ return {
145
+ ":function"(node) {
146
+ const kind = getFunctionKind(node);
147
+ functionEntries.push({ kind, node });
148
+ if (kind === "setup") {
149
+ onSetupFunctionEnter(node);
150
+ }
151
+ },
152
+ ":function:exit"(node) {
153
+ const { kind } = functionEntries.at(-1) ?? {};
154
+ if (kind === "setup") {
155
+ onSetupFunctionExit(node);
156
+ }
157
+ functionEntries.pop();
158
+ },
159
+ CallExpression(node) {
160
+ const setupFunction = setupFunctionRef.current;
161
+ const pEntry = functionEntries.at(-1);
162
+ if (pEntry == null || pEntry.node.async) {
163
+ return;
164
+ }
165
+ match(getCallKind(node)).with("setState", () => {
166
+ switch (true) {
167
+ case pEntry.kind === "deferred":
168
+ case pEntry.node.async:
169
+ break;
170
+ case pEntry.node === setupFunction:
171
+ case (pEntry.kind === "immediate" && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction): {
172
+ onViolation(context, node, {
173
+ name: context.sourceCode.getText(node.callee)
174
+ });
175
+ return;
176
+ }
177
+ default: {
178
+ const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall);
179
+ if (vd == null) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node);
180
+ else getOrElseUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node);
181
+ }
182
+ }
183
+ }).with(useEffectKind, () => {
184
+ if (AST.isFunction(node.arguments.at(0))) return;
185
+ setupFunctionIdentifiers.push(...AST.getNestedIdentifiers(node));
186
+ }).with("other", () => {
187
+ if (pEntry.node !== setupFunction) return;
188
+ indFunctionCalls.push(node);
189
+ }).otherwise(constVoid);
190
+ },
191
+ Identifier(node) {
192
+ if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) {
193
+ return;
194
+ }
195
+ if (!isIdFromUseStateCall(node, 1)) {
196
+ return;
197
+ }
198
+ switch (node.parent.type) {
199
+ case AST_NODE_TYPES.ArrowFunctionExpression: {
200
+ const parent = node.parent.parent;
201
+ if (parent.type !== AST_NODE_TYPES.CallExpression) {
202
+ break;
203
+ }
204
+ if (!isUseMemoCall(parent)) {
205
+ break;
206
+ }
207
+ const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall);
208
+ if (vd != null) {
209
+ getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
210
+ }
211
+ break;
212
+ }
213
+ case AST_NODE_TYPES.CallExpression: {
214
+ if (node !== node.parent.arguments.at(0)) {
215
+ break;
216
+ }
217
+ if (isUseCallbackCall(node.parent)) {
218
+ const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
219
+ if (vd != null) {
220
+ getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
221
+ }
222
+ break;
223
+ }
224
+ if (isUseEffectLikeCall(node.parent)) {
225
+ getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node);
226
+ }
227
+ }
228
+ }
229
+ },
230
+ "Program:exit"() {
231
+ const getSetStateCalls = (id, initialScope) => {
232
+ const node = VAR.getVariableInitNode(VAR.findVariable(id, initialScope), 0);
233
+ switch (node?.type) {
234
+ case AST_NODE_TYPES.ArrowFunctionExpression:
235
+ case AST_NODE_TYPES.FunctionDeclaration:
236
+ case AST_NODE_TYPES.FunctionExpression:
237
+ return indSetStateCalls.get(node) ?? [];
238
+ case AST_NODE_TYPES.CallExpression:
239
+ return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? [];
240
+ }
241
+ return [];
242
+ };
243
+ for (const [, calls] of indSetStateCallsInUseEffectSetup) {
244
+ for (const call of calls) {
245
+ onViolation(context, call, { name: call.name });
246
+ }
247
+ }
248
+ for (const { callee } of indFunctionCalls) {
249
+ if (!("name" in callee)) {
250
+ continue;
251
+ }
252
+ const { name: name3 } = callee;
253
+ const setStateCalls = getSetStateCalls(name3, context.sourceCode.getScope(callee));
254
+ for (const setStateCall of setStateCalls) {
255
+ onViolation(context, setStateCall, {
256
+ name: getCallName(setStateCall)
257
+ });
258
+ }
259
+ }
260
+ for (const id of setupFunctionIdentifiers) {
261
+ const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
262
+ for (const setStateCall of setStateCalls) {
263
+ onViolation(context, setStateCall, {
264
+ name: getCallName(setStateCall)
265
+ });
266
+ }
267
+ }
268
+ }
269
+ };
270
+ }
271
+ function isInitFromHookCall(init) {
272
+ if (init?.type !== AST_NODE_TYPES.CallExpression) return false;
273
+ switch (init.callee.type) {
274
+ case AST_NODE_TYPES.Identifier:
275
+ return ER.isReactHookName(init.callee.name);
276
+ case AST_NODE_TYPES.MemberExpression:
277
+ return init.callee.property.type === AST_NODE_TYPES.Identifier && ER.isReactHookName(init.callee.property.name);
278
+ default:
279
+ return false;
280
+ }
281
+ }
282
+ function isVariableDeclaratorFromHookCall(node) {
283
+ if (node.type !== AST_NODE_TYPES.VariableDeclarator) return false;
284
+ if (node.id.type !== AST_NODE_TYPES.Identifier) return false;
285
+ return isInitFromHookCall(node.init);
286
+ }
31
287
  var createRule = ESLintUtils.RuleCreator(getDocsUrl("hooks-extra"));
32
288
 
33
- // src/rules-removed/no-unnecessary-use-callback.ts
34
- var RULE_NAME = "no-unnecessary-use-callback";
289
+ // src/rules/no-direct-set-state-in-use-effect.ts
290
+ var RULE_NAME = "no-direct-set-state-in-use-effect";
35
291
  var RULE_FEATURES = [
36
292
  "EXP"
37
293
  ];
38
- var no_unnecessary_use_callback_default = createRule({
294
+ var no_direct_set_state_in_use_effect_default = createRule({
39
295
  meta: {
40
296
  type: "problem",
41
- deprecated: {
42
- deprecatedSince: "2.0.0",
43
- replacedBy: [
44
- {
45
- message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
46
- plugin: {
47
- name: "eslint-plugin-react-x",
48
- url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
49
- },
50
- rule: {
51
- name: "no-unnecessary-use-callback",
52
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
53
- }
54
- },
55
- {
56
- message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
57
- plugin: {
58
- name: "@eslint-react/eslint-plugin",
59
- url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
60
- },
61
- rule: {
62
- name: "no-unnecessary-use-callback",
63
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
64
- }
65
- }
66
- ]
67
- },
68
297
  docs: {
69
- description: "Disallow unnecessary usage of `useCallback`.",
298
+ description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
70
299
  [Symbol.for("rule_features")]: RULE_FEATURES
71
300
  },
72
301
  messages: {
73
- noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary."
302
+ noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'."
74
303
  },
75
304
  schema: []
76
305
  },
@@ -79,69 +308,50 @@ var no_unnecessary_use_callback_default = createRule({
79
308
  defaultOptions: []
80
309
  });
81
310
  function create(context) {
82
- if (!context.sourceCode.text.includes("use")) return {};
83
- const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? [];
84
- const isUseCallbackCall = ER5.isReactHookCallWithNameAlias(context, "useCallback", alias);
85
- return {
86
- CallExpression(node) {
87
- if (!ER5.isReactHookCall(node)) {
88
- return;
89
- }
90
- const initialScope = context.sourceCode.getScope(node);
91
- if (!isUseCallbackCall(node)) {
92
- return;
93
- }
94
- const scope = context.sourceCode.getScope(node);
95
- const component = scope.block;
96
- if (!AST5.isFunction(component)) {
97
- return;
98
- }
99
- const [arg0, arg1] = node.arguments;
100
- if (arg0 == null || arg1 == null) {
101
- return;
102
- }
103
- const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
104
- const variable = VAR3.findVariable(n.name, initialScope);
105
- const variableNode = VAR3.getVariableInitNode(variable, 0);
106
- if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) {
107
- return false;
108
- }
109
- return variableNode.elements.length === 0;
110
- }).otherwise(() => false);
111
- if (!hasEmptyDeps) {
112
- return;
113
- }
114
- const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
115
- if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
116
- return n.body;
117
- }
118
- return n;
119
- }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
120
- const variable = VAR3.findVariable(n.name, initialScope);
121
- const variableNode = VAR3.getVariableInitNode(variable, 0);
122
- if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) {
123
- return null;
124
- }
125
- return variableNode;
126
- }).otherwise(() => null);
127
- if (arg0Node == null) return;
128
- const arg0NodeScope = context.sourceCode.getScope(arg0Node);
129
- const arg0NodeReferences = VAR3.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
130
- const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
131
- if (!isReferencedToComponentScope) {
132
- context.report({
133
- messageId: "noUnnecessaryUseCallback",
134
- node
135
- });
136
- }
137
- }
138
- };
311
+ if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
312
+ return useNoDirectSetStateInUseEffect(context, {
313
+ onViolation(ctx, node, data) {
314
+ ctx.report({ messageId: "noDirectSetStateInUseEffect", node, data });
315
+ },
316
+ useEffectKind: "useEffect"
317
+ });
139
318
  }
140
- var RULE_NAME2 = "no-unnecessary-use-memo";
319
+
320
+ // src/rules/no-direct-set-state-in-use-layout-effect.ts
321
+ var RULE_NAME2 = "no-direct-set-state-in-use-layout-effect";
141
322
  var RULE_FEATURES2 = [
142
323
  "EXP"
143
324
  ];
144
- var no_unnecessary_use_memo_default = createRule({
325
+ var no_direct_set_state_in_use_layout_effect_default = createRule({
326
+ meta: {
327
+ type: "problem",
328
+ docs: {
329
+ description: "Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`.",
330
+ [Symbol.for("rule_features")]: RULE_FEATURES2
331
+ },
332
+ messages: {
333
+ noDirectSetStateInUseLayoutEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useLayoutEffect'."
334
+ },
335
+ schema: []
336
+ },
337
+ name: RULE_NAME2,
338
+ create: create2,
339
+ defaultOptions: []
340
+ });
341
+ function create2(context) {
342
+ if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
343
+ return useNoDirectSetStateInUseEffect(context, {
344
+ onViolation(ctx, node, data) {
345
+ ctx.report({ messageId: "noDirectSetStateInUseLayoutEffect", node, data });
346
+ },
347
+ useEffectKind: "useLayoutEffect"
348
+ });
349
+ }
350
+ var RULE_NAME3 = "no-unnecessary-use-callback";
351
+ var RULE_FEATURES3 = [
352
+ "EXP"
353
+ ];
354
+ var no_unnecessary_use_callback_default = createRule({
145
355
  meta: {
146
356
  type: "problem",
147
357
  deprecated: {
@@ -154,8 +364,8 @@ var no_unnecessary_use_memo_default = createRule({
154
364
  url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
155
365
  },
156
366
  rule: {
157
- name: "no-unnecessary-use-memo",
158
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
367
+ name: "no-unnecessary-use-callback",
368
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
159
369
  }
160
370
  },
161
371
  {
@@ -165,54 +375,50 @@ var no_unnecessary_use_memo_default = createRule({
165
375
  url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
166
376
  },
167
377
  rule: {
168
- name: "no-unnecessary-use-memo",
169
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
378
+ name: "no-unnecessary-use-callback",
379
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-callback"
170
380
  }
171
381
  }
172
382
  ]
173
383
  },
174
384
  docs: {
175
- description: "Disallow unnecessary usage of `useMemo`.",
176
- [Symbol.for("rule_features")]: RULE_FEATURES2
385
+ description: "Disallow unnecessary usage of `useCallback`.",
386
+ [Symbol.for("rule_features")]: RULE_FEATURES3
177
387
  },
178
388
  messages: {
179
- noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary."
389
+ noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary."
180
390
  },
181
391
  schema: []
182
392
  },
183
- name: RULE_NAME2,
184
- create: create2,
393
+ name: RULE_NAME3,
394
+ create: create3,
185
395
  defaultOptions: []
186
396
  });
187
- function create2(context) {
397
+ function create3(context) {
188
398
  if (!context.sourceCode.text.includes("use")) return {};
189
- const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? [];
190
- const isUseMemoCall = ER5.isReactHookCallWithNameAlias(context, "useMemo", alias);
399
+ const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? [];
400
+ const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", alias);
191
401
  return {
192
402
  CallExpression(node) {
193
- if (!ER5.isReactHookCall(node)) {
403
+ if (!ER.isReactHookCall(node)) {
194
404
  return;
195
405
  }
196
406
  const initialScope = context.sourceCode.getScope(node);
197
- if (!isUseMemoCall(node)) {
407
+ if (!isUseCallbackCall(node)) {
198
408
  return;
199
409
  }
200
410
  const scope = context.sourceCode.getScope(node);
201
411
  const component = scope.block;
202
- if (!AST5.isFunction(component)) {
412
+ if (!AST.isFunction(component)) {
203
413
  return;
204
414
  }
205
415
  const [arg0, arg1] = node.arguments;
206
416
  if (arg0 == null || arg1 == null) {
207
417
  return;
208
418
  }
209
- const hasCallInArg0 = AST5.isFunction(arg0) && [...AST5.getNestedCallExpressions(arg0.body), ...AST5.getNestedNewExpressions(arg0.body)].length > 0;
210
- if (hasCallInArg0) {
211
- return;
212
- }
213
419
  const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
214
- const variable = VAR3.findVariable(n.name, initialScope);
215
- const variableNode = VAR3.getVariableInitNode(variable, 0);
420
+ const variable = VAR.findVariable(n.name, initialScope);
421
+ const variableNode = VAR.getVariableInitNode(variable, 0);
216
422
  if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) {
217
423
  return false;
218
424
  }
@@ -223,145 +429,35 @@ function create2(context) {
223
429
  }
224
430
  const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
225
431
  if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
226
- return n.body;
227
- }
228
- return n;
229
- }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
230
- const variable = VAR3.findVariable(n.name, initialScope);
231
- const variableNode = VAR3.getVariableInitNode(variable, 0);
232
- if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) {
233
- return null;
234
- }
235
- return variableNode;
236
- }).otherwise(() => null);
237
- if (arg0Node == null) return;
238
- const arg0NodeScope = context.sourceCode.getScope(arg0Node);
239
- const arg0NodeReferences = VAR3.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
240
- const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
241
- if (!isReferencedToComponentScope) {
242
- context.report({
243
- messageId: "noUnnecessaryUseMemo",
244
- node
245
- });
246
- }
247
- }
248
- };
249
- }
250
- var RULE_NAME3 = "no-unnecessary-use-prefix";
251
- var RULE_FEATURES3 = [];
252
- var WELL_KNOWN_HOOKS = [
253
- "useMDXComponents"
254
- ];
255
- function containsUseComments(context, node) {
256
- return context.sourceCode.getCommentsInside(node).some(({ value }) => /use\([\s\S]*?\)/u.test(value) || /use[A-Z0-9]\w*\([\s\S]*?\)/u.test(value));
257
- }
258
- var no_unnecessary_use_prefix_default = createRule({
259
- meta: {
260
- type: "problem",
261
- deprecated: {
262
- deprecatedSince: "2.0.0",
263
- replacedBy: [
264
- {
265
- message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
266
- plugin: {
267
- name: "eslint-plugin-react-x",
268
- url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
269
- },
270
- rule: {
271
- name: "no-unnecessary-use-prefix",
272
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
273
- }
274
- },
275
- {
276
- message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
277
- plugin: {
278
- name: "@eslint-react/eslint-plugin",
279
- url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
280
- },
281
- rule: {
282
- name: "no-unnecessary-use-prefix",
283
- url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
284
- }
285
- }
286
- ]
287
- },
288
- docs: {
289
- description: "Enforces that a function with the `use` prefix should use at least one Hook inside of it.",
290
- [Symbol.for("rule_features")]: RULE_FEATURES3
291
- },
292
- messages: {
293
- noUnnecessaryUsePrefix: "If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix."
294
- },
295
- schema: []
296
- },
297
- name: RULE_NAME3,
298
- create: create3,
299
- defaultOptions: []
300
- });
301
- function create3(context) {
302
- const { ctx, listeners } = ER5.useHookCollector();
303
- return {
304
- ...listeners,
305
- "Program:exit"(program) {
306
- const allHooks = ctx.getAllHooks(program);
307
- for (const { id, name: name3, node, hookCalls } of allHooks.values()) {
308
- if (WELL_KNOWN_HOOKS.includes(name3)) {
309
- continue;
310
- }
311
- if (AST5.isEmptyFunction(node)) {
312
- continue;
313
- }
314
- if (hookCalls.length > 0) {
315
- continue;
316
- }
317
- if (containsUseComments(context, node)) {
318
- continue;
319
- }
320
- if (id != null) {
321
- context.report({
322
- messageId: "noUnnecessaryUsePrefix",
323
- data: {
324
- name: name3
325
- },
326
- loc: getPreferredLoc(context, id)
327
- });
328
- continue;
432
+ return n.body;
433
+ }
434
+ return n;
435
+ }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
436
+ const variable = VAR.findVariable(n.name, initialScope);
437
+ const variableNode = VAR.getVariableInitNode(variable, 0);
438
+ if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) {
439
+ return null;
329
440
  }
441
+ return variableNode;
442
+ }).otherwise(() => null);
443
+ if (arg0Node == null) return;
444
+ const arg0NodeScope = context.sourceCode.getScope(arg0Node);
445
+ const arg0NodeReferences = VAR.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
446
+ const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
447
+ if (!isReferencedToComponentScope) {
330
448
  context.report({
331
- messageId: "noUnnecessaryUsePrefix",
332
- node,
333
- data: {
334
- name: name3
335
- }
449
+ messageId: "noUnnecessaryUseCallback",
450
+ node
336
451
  });
337
452
  }
338
453
  }
339
454
  };
340
455
  }
341
- function getPreferredLoc(context, id) {
342
- if (AST5.isMultiLine(id)) return id.loc;
343
- if (!context.sourceCode.getText(id).startsWith("use")) return id.loc;
344
- return {
345
- end: {
346
- column: id.loc.start.column + 3,
347
- line: id.loc.start.line
348
- },
349
- start: {
350
- column: id.loc.start.column,
351
- line: id.loc.start.line
352
- }
353
- };
354
- }
355
- var RULE_NAME4 = "prefer-use-state-lazy-initialization";
456
+ var RULE_NAME4 = "no-unnecessary-use-memo";
356
457
  var RULE_FEATURES4 = [
357
458
  "EXP"
358
459
  ];
359
- var ALLOW_LIST = [
360
- "Boolean",
361
- "String",
362
- "Number"
363
- ];
364
- var prefer_use_state_lazy_initialization_default = createRule({
460
+ var no_unnecessary_use_memo_default = createRule({
365
461
  meta: {
366
462
  type: "problem",
367
463
  deprecated: {
@@ -374,8 +470,8 @@ var prefer_use_state_lazy_initialization_default = createRule({
374
470
  url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
375
471
  },
376
472
  rule: {
377
- name: "prefer-use-state-lazy-initialization",
378
- url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
473
+ name: "no-unnecessary-use-memo",
474
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
379
475
  }
380
476
  },
381
477
  {
@@ -385,18 +481,18 @@ var prefer_use_state_lazy_initialization_default = createRule({
385
481
  url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
386
482
  },
387
483
  rule: {
388
- name: "prefer-use-state-lazy-initialization",
389
- url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
484
+ name: "no-unnecessary-use-memo",
485
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-memo"
390
486
  }
391
487
  }
392
488
  ]
393
489
  },
394
490
  docs: {
395
- description: "Enforces function calls made inside `useState` to be wrapped in an `initializer function`.",
491
+ description: "Disallow unnecessary usage of `useMemo`.",
396
492
  [Symbol.for("rule_features")]: RULE_FEATURES4
397
493
  },
398
494
  messages: {
399
- preferUseStateLazyInitialization: "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'."
495
+ noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary."
400
496
  },
401
497
  schema: []
402
498
  },
@@ -405,313 +501,112 @@ var prefer_use_state_lazy_initialization_default = createRule({
405
501
  defaultOptions: []
406
502
  });
407
503
  function create4(context) {
408
- const alias = getSettingsFromContext(context).additionalHooks.useState ?? [];
409
- const isUseStateCall = ER5.isReactHookCallWithNameAlias(context, "useState", alias);
410
- return {
411
- CallExpression(node) {
412
- if (!ER5.isReactHookCall(node)) {
413
- return;
414
- }
415
- if (!isUseStateCall(node)) {
416
- return;
417
- }
418
- const [useStateInput] = node.arguments;
419
- if (useStateInput == null) {
420
- return;
421
- }
422
- for (const expr of AST5.getNestedNewExpressions(useStateInput)) {
423
- if (!("name" in expr.callee)) continue;
424
- if (ALLOW_LIST.includes(expr.callee.name)) continue;
425
- if (AST5.findParentNode(expr, (n) => ER5.isUseCall(context, n)) != null) continue;
426
- context.report({
427
- messageId: "preferUseStateLazyInitialization",
428
- node: expr
429
- });
430
- }
431
- for (const expr of AST5.getNestedCallExpressions(useStateInput)) {
432
- if (!("name" in expr.callee)) continue;
433
- if (ER5.isReactHookName(expr.callee.name)) continue;
434
- if (ALLOW_LIST.includes(expr.callee.name)) continue;
435
- if (AST5.findParentNode(expr, (n) => ER5.isUseCall(context, n)) != null) continue;
436
- context.report({
437
- messageId: "preferUseStateLazyInitialization",
438
- node: expr
439
- });
440
- }
441
- }
442
- };
443
- }
444
- function useNoDirectSetStateInUseEffect(context, options) {
445
- const { onViolation, useEffectKind } = options;
446
- const settings = getSettingsFromContext(context);
447
- const hooks = settings.additionalHooks;
448
- const getText = (n) => context.sourceCode.getText(n);
449
- const isUseEffectLikeCall = ER5.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]);
450
- const isUseStateCall = ER5.isReactHookCallWithNameAlias(context, "useState", hooks.useState);
451
- const isUseMemoCall = ER5.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo);
452
- const isUseCallbackCall = ER5.isReactHookCallWithNameAlias(context, "useCallback", hooks.useCallback);
453
- const functionEntries = [];
454
- const setupFunctionRef = { current: null };
455
- const setupFunctionIdentifiers = [];
456
- const indFunctionCalls = [];
457
- const indSetStateCalls = /* @__PURE__ */ new WeakMap();
458
- const indSetStateCallsInUseEffectArg0 = /* @__PURE__ */ new WeakMap();
459
- const indSetStateCallsInUseEffectSetup = /* @__PURE__ */ new Map();
460
- const indSetStateCallsInUseMemoOrCallback = /* @__PURE__ */ new WeakMap();
461
- const onSetupFunctionEnter = (node) => {
462
- setupFunctionRef.current = node;
463
- };
464
- const onSetupFunctionExit = (node) => {
465
- if (setupFunctionRef.current === node) {
466
- setupFunctionRef.current = null;
467
- }
468
- };
469
- function isFunctionOfUseEffectSetup(node) {
470
- return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectLikeCall(node.parent);
471
- }
472
- function getCallName(node) {
473
- if (node.type === AST_NODE_TYPES.CallExpression) {
474
- return AST5.toStringFormat(node.callee, getText);
475
- }
476
- return AST5.toStringFormat(node, getText);
477
- }
478
- function getCallKind(node) {
479
- return match(node).when(isUseStateCall, () => "useState").when(isUseEffectLikeCall, () => useEffectKind).when(isSetStateCall, () => "setState").when(AST5.isThenCall, () => "then").otherwise(() => "other");
480
- }
481
- function getFunctionKind(node) {
482
- const parent = AST5.findParentNode(node, not(AST5.isTypeExpression)) ?? node.parent;
483
- switch (true) {
484
- case node.async:
485
- case (parent.type === AST_NODE_TYPES.CallExpression && AST5.isThenCall(parent)):
486
- return "deferred";
487
- case (node.type !== AST_NODE_TYPES.FunctionDeclaration && parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node):
488
- return "immediate";
489
- case isFunctionOfUseEffectSetup(node):
490
- return "setup";
491
- default:
492
- return "other";
493
- }
494
- }
495
- function isIdFromUseStateCall(topLevelId, at) {
496
- const variable = VAR3.findVariable(topLevelId, context.sourceCode.getScope(topLevelId));
497
- const variableNode = VAR3.getVariableInitNode(variable, 0);
498
- if (variableNode == null) return false;
499
- if (variableNode.type !== AST_NODE_TYPES.CallExpression) return false;
500
- if (!ER5.isReactHookCallWithNameAlias(context, "useState", hooks.useState)(variableNode)) return false;
501
- const variableNodeParent = variableNode.parent;
502
- if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) {
503
- return true;
504
- }
505
- return variableNodeParent.id.elements.findIndex((e) => e?.type === AST_NODE_TYPES.Identifier && e.name === topLevelId.name) === at;
506
- }
507
- function isSetStateCall(node) {
508
- switch (node.callee.type) {
509
- // const data = useState();
510
- // data.at(1)();
511
- case AST_NODE_TYPES.CallExpression: {
512
- const { callee } = node.callee;
513
- if (callee.type !== AST_NODE_TYPES.MemberExpression) {
514
- return false;
515
- }
516
- if (!("name" in callee.object)) {
517
- return false;
518
- }
519
- const isAt = callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "at";
520
- const [index] = node.callee.arguments;
521
- if (!isAt || index == null) {
522
- return false;
523
- }
524
- const indexScope = context.sourceCode.getScope(node);
525
- const indexValue = VAR3.toStaticValue({
526
- kind: "lazy",
527
- node: index,
528
- initialScope: indexScope
529
- }).value;
530
- return indexValue === 1 && isIdFromUseStateCall(callee.object);
531
- }
532
- // const [data, setData] = useState();
533
- // setData();
534
- case AST_NODE_TYPES.Identifier: {
535
- return isIdFromUseStateCall(node.callee, 1);
536
- }
537
- // const data = useState();
538
- // data[1]();
539
- case AST_NODE_TYPES.MemberExpression: {
540
- if (!("name" in node.callee.object)) {
541
- return false;
542
- }
543
- const property = node.callee.property;
544
- const propertyScope = context.sourceCode.getScope(node);
545
- const propertyValue = VAR3.toStaticValue({
546
- kind: "lazy",
547
- node: property,
548
- initialScope: propertyScope
549
- }).value;
550
- return propertyValue === 1 && isIdFromUseStateCall(node.callee.object, 1);
551
- }
552
- default: {
553
- return false;
554
- }
555
- }
556
- }
504
+ if (!context.sourceCode.text.includes("use")) return {};
505
+ const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? [];
506
+ const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", alias);
557
507
  return {
558
- ":function"(node) {
559
- const kind = getFunctionKind(node);
560
- functionEntries.push({ kind, node });
561
- if (kind === "setup") {
562
- onSetupFunctionEnter(node);
563
- }
564
- },
565
- ":function:exit"(node) {
566
- const { kind } = functionEntries.at(-1) ?? {};
567
- if (kind === "setup") {
568
- onSetupFunctionExit(node);
569
- }
570
- functionEntries.pop();
571
- },
572
508
  CallExpression(node) {
573
- const setupFunction = setupFunctionRef.current;
574
- const pEntry = functionEntries.at(-1);
575
- if (pEntry == null || pEntry.node.async) {
576
- return;
577
- }
578
- match(getCallKind(node)).with("setState", () => {
579
- switch (true) {
580
- case pEntry.kind === "deferred":
581
- case pEntry.node.async:
582
- break;
583
- case pEntry.node === setupFunction:
584
- case (pEntry.kind === "immediate" && AST5.findParentNode(pEntry.node, AST5.isFunction) === setupFunction): {
585
- onViolation(context, node, {
586
- name: context.sourceCode.getText(node.callee)
587
- });
588
- return;
589
- }
590
- default: {
591
- const vd = AST5.findParentNode(node, isVariableDeclaratorFromHookCall);
592
- if (vd == null) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node);
593
- else getOrElseUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node);
594
- }
595
- }
596
- }).with(useEffectKind, () => {
597
- if (AST5.isFunction(node.arguments.at(0))) return;
598
- setupFunctionIdentifiers.push(...AST5.getNestedIdentifiers(node));
599
- }).with("other", () => {
600
- if (pEntry.node !== setupFunction) return;
601
- indFunctionCalls.push(node);
602
- }).otherwise(constVoid);
603
- },
604
- Identifier(node) {
605
- if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) {
509
+ if (!ER.isReactHookCall(node)) {
606
510
  return;
607
511
  }
608
- if (!isIdFromUseStateCall(node, 1)) {
609
- return;
610
- }
611
- switch (node.parent.type) {
612
- case AST_NODE_TYPES.ArrowFunctionExpression: {
613
- const parent = node.parent.parent;
614
- if (parent.type !== AST_NODE_TYPES.CallExpression) {
615
- break;
616
- }
617
- if (!isUseMemoCall(parent)) {
618
- break;
619
- }
620
- const vd = AST5.findParentNode(parent, isVariableDeclaratorFromHookCall);
621
- if (vd != null) {
622
- getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
623
- }
624
- break;
625
- }
626
- case AST_NODE_TYPES.CallExpression: {
627
- if (node !== node.parent.arguments.at(0)) {
628
- break;
629
- }
630
- if (isUseCallbackCall(node.parent)) {
631
- const vd = AST5.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
632
- if (vd != null) {
633
- getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
634
- }
635
- break;
636
- }
637
- if (isUseEffectLikeCall(node.parent)) {
638
- getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node);
639
- }
640
- }
641
- }
642
- },
643
- "Program:exit"() {
644
- const getSetStateCalls = (id, initialScope) => {
645
- const node = VAR3.getVariableInitNode(VAR3.findVariable(id, initialScope), 0);
646
- switch (node?.type) {
647
- case AST_NODE_TYPES.ArrowFunctionExpression:
648
- case AST_NODE_TYPES.FunctionDeclaration:
649
- case AST_NODE_TYPES.FunctionExpression:
650
- return indSetStateCalls.get(node) ?? [];
651
- case AST_NODE_TYPES.CallExpression:
652
- return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? [];
653
- }
654
- return [];
655
- };
656
- for (const [, calls] of indSetStateCallsInUseEffectSetup) {
657
- for (const call of calls) {
658
- onViolation(context, call, { name: call.name });
659
- }
512
+ const initialScope = context.sourceCode.getScope(node);
513
+ if (!isUseMemoCall(node)) {
514
+ return;
660
515
  }
661
- for (const { callee } of indFunctionCalls) {
662
- if (!("name" in callee)) {
663
- continue;
664
- }
665
- const { name: name3 } = callee;
666
- const setStateCalls = getSetStateCalls(name3, context.sourceCode.getScope(callee));
667
- for (const setStateCall of setStateCalls) {
668
- onViolation(context, setStateCall, {
669
- name: getCallName(setStateCall)
670
- });
516
+ const scope = context.sourceCode.getScope(node);
517
+ const component = scope.block;
518
+ if (!AST.isFunction(component)) {
519
+ return;
520
+ }
521
+ const [arg0, arg1] = node.arguments;
522
+ if (arg0 == null || arg1 == null) {
523
+ return;
524
+ }
525
+ const hasCallInArg0 = AST.isFunction(arg0) && [...AST.getNestedCallExpressions(arg0.body), ...AST.getNestedNewExpressions(arg0.body)].length > 0;
526
+ if (hasCallInArg0) {
527
+ return;
528
+ }
529
+ const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
530
+ const variable = VAR.findVariable(n.name, initialScope);
531
+ const variableNode = VAR.getVariableInitNode(variable, 0);
532
+ if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) {
533
+ return false;
671
534
  }
535
+ return variableNode.elements.length === 0;
536
+ }).otherwise(() => false);
537
+ if (!hasEmptyDeps) {
538
+ return;
672
539
  }
673
- for (const id of setupFunctionIdentifiers) {
674
- const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
675
- for (const setStateCall of setStateCalls) {
676
- onViolation(context, setStateCall, {
677
- name: getCallName(setStateCall)
678
- });
540
+ const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => {
541
+ if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) {
542
+ return n.body;
543
+ }
544
+ return n;
545
+ }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => {
546
+ const variable = VAR.findVariable(n.name, initialScope);
547
+ const variableNode = VAR.getVariableInitNode(variable, 0);
548
+ if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) {
549
+ return null;
679
550
  }
551
+ return variableNode;
552
+ }).otherwise(() => null);
553
+ if (arg0Node == null) return;
554
+ const arg0NodeScope = context.sourceCode.getScope(arg0Node);
555
+ const arg0NodeReferences = VAR.getChidScopes(arg0NodeScope).flatMap((x) => x.references);
556
+ const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component);
557
+ if (!isReferencedToComponentScope) {
558
+ context.report({
559
+ messageId: "noUnnecessaryUseMemo",
560
+ node
561
+ });
680
562
  }
681
563
  }
682
564
  };
683
565
  }
684
- function isInitFromHookCall(init) {
685
- if (init?.type !== AST_NODE_TYPES.CallExpression) return false;
686
- switch (init.callee.type) {
687
- case AST_NODE_TYPES.Identifier:
688
- return ER5.isReactHookName(init.callee.name);
689
- case AST_NODE_TYPES.MemberExpression:
690
- return init.callee.property.type === AST_NODE_TYPES.Identifier && ER5.isReactHookName(init.callee.property.name);
691
- default:
692
- return false;
693
- }
694
- }
695
- function isVariableDeclaratorFromHookCall(node) {
696
- if (node.type !== AST_NODE_TYPES.VariableDeclarator) return false;
697
- if (node.id.type !== AST_NODE_TYPES.Identifier) return false;
698
- return isInitFromHookCall(node.init);
699
- }
700
-
701
- // src/rules/no-direct-set-state-in-use-effect.ts
702
- var RULE_NAME5 = "no-direct-set-state-in-use-effect";
703
- var RULE_FEATURES5 = [
704
- "EXP"
566
+ var RULE_NAME5 = "no-unnecessary-use-prefix";
567
+ var RULE_FEATURES5 = [];
568
+ var WELL_KNOWN_HOOKS = [
569
+ "useMDXComponents"
705
570
  ];
706
- var no_direct_set_state_in_use_effect_default = createRule({
571
+ function containsUseComments(context, node) {
572
+ return context.sourceCode.getCommentsInside(node).some(({ value }) => /use\([\s\S]*?\)/u.test(value) || /use[A-Z0-9]\w*\([\s\S]*?\)/u.test(value));
573
+ }
574
+ var no_unnecessary_use_prefix_default = createRule({
707
575
  meta: {
708
576
  type: "problem",
577
+ deprecated: {
578
+ deprecatedSince: "2.0.0",
579
+ replacedBy: [
580
+ {
581
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
582
+ plugin: {
583
+ name: "eslint-plugin-react-x",
584
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
585
+ },
586
+ rule: {
587
+ name: "no-unnecessary-use-prefix",
588
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
589
+ }
590
+ },
591
+ {
592
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
593
+ plugin: {
594
+ name: "@eslint-react/eslint-plugin",
595
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
596
+ },
597
+ rule: {
598
+ name: "no-unnecessary-use-prefix",
599
+ url: "https://next.eslint-react.xyz/docs/rules/no-unnecessary-use-prefix"
600
+ }
601
+ }
602
+ ]
603
+ },
709
604
  docs: {
710
- description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
605
+ description: "Enforces that a function with the `use` prefix should use at least one Hook inside of it.",
711
606
  [Symbol.for("rule_features")]: RULE_FEATURES5
712
607
  },
713
608
  messages: {
714
- noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'."
609
+ noUnnecessaryUsePrefix: "If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix."
715
610
  },
716
611
  schema: []
717
612
  },
@@ -720,29 +615,104 @@ var no_direct_set_state_in_use_effect_default = createRule({
720
615
  defaultOptions: []
721
616
  });
722
617
  function create5(context) {
723
- if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
724
- return useNoDirectSetStateInUseEffect(context, {
725
- onViolation(ctx, node, data) {
726
- ctx.report({ messageId: "noDirectSetStateInUseEffect", node, data });
618
+ const { ctx, listeners } = ER.useHookCollector();
619
+ return {
620
+ ...listeners,
621
+ "Program:exit"(program) {
622
+ const allHooks = ctx.getAllHooks(program);
623
+ for (const { id, name: name3, node, hookCalls } of allHooks.values()) {
624
+ if (WELL_KNOWN_HOOKS.includes(name3)) {
625
+ continue;
626
+ }
627
+ if (AST.isEmptyFunction(node)) {
628
+ continue;
629
+ }
630
+ if (hookCalls.length > 0) {
631
+ continue;
632
+ }
633
+ if (containsUseComments(context, node)) {
634
+ continue;
635
+ }
636
+ if (id != null) {
637
+ context.report({
638
+ messageId: "noUnnecessaryUsePrefix",
639
+ data: {
640
+ name: name3
641
+ },
642
+ loc: getPreferredLoc(context, id)
643
+ });
644
+ continue;
645
+ }
646
+ context.report({
647
+ messageId: "noUnnecessaryUsePrefix",
648
+ node,
649
+ data: {
650
+ name: name3
651
+ }
652
+ });
653
+ }
654
+ }
655
+ };
656
+ }
657
+ function getPreferredLoc(context, id) {
658
+ if (AST.isMultiLine(id)) return id.loc;
659
+ if (!context.sourceCode.getText(id).startsWith("use")) return id.loc;
660
+ return {
661
+ end: {
662
+ column: id.loc.start.column + 3,
663
+ line: id.loc.start.line
727
664
  },
728
- useEffectKind: "useEffect"
729
- });
665
+ start: {
666
+ column: id.loc.start.column,
667
+ line: id.loc.start.line
668
+ }
669
+ };
730
670
  }
731
-
732
- // src/rules/no-direct-set-state-in-use-layout-effect.ts
733
- var RULE_NAME6 = "no-direct-set-state-in-use-layout-effect";
671
+ var RULE_NAME6 = "prefer-use-state-lazy-initialization";
734
672
  var RULE_FEATURES6 = [
735
673
  "EXP"
736
674
  ];
737
- var no_direct_set_state_in_use_layout_effect_default = createRule({
675
+ var ALLOW_LIST = [
676
+ "Boolean",
677
+ "String",
678
+ "Number"
679
+ ];
680
+ var prefer_use_state_lazy_initialization_default = createRule({
738
681
  meta: {
739
682
  type: "problem",
683
+ deprecated: {
684
+ deprecatedSince: "2.0.0",
685
+ replacedBy: [
686
+ {
687
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
688
+ plugin: {
689
+ name: "eslint-plugin-react-x",
690
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x"
691
+ },
692
+ rule: {
693
+ name: "prefer-use-state-lazy-initialization",
694
+ url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
695
+ }
696
+ },
697
+ {
698
+ message: "Use the same rule from `eslint-plugin-react-x` or `@eslint-react/eslint-plugin` instead.",
699
+ plugin: {
700
+ name: "@eslint-react/eslint-plugin",
701
+ url: "https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin"
702
+ },
703
+ rule: {
704
+ name: "prefer-use-state-lazy-initialization",
705
+ url: "https://next.eslint-react.xyz/docs/rules/prefer-use-state-lazy-initialization"
706
+ }
707
+ }
708
+ ]
709
+ },
740
710
  docs: {
741
- description: "Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`.",
711
+ description: "Enforces function calls made inside `useState` to be wrapped in an `initializer function`.",
742
712
  [Symbol.for("rule_features")]: RULE_FEATURES6
743
713
  },
744
714
  messages: {
745
- noDirectSetStateInUseLayoutEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useLayoutEffect'."
715
+ preferUseStateLazyInitialization: "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'."
746
716
  },
747
717
  schema: []
748
718
  },
@@ -751,13 +721,41 @@ var no_direct_set_state_in_use_layout_effect_default = createRule({
751
721
  defaultOptions: []
752
722
  });
753
723
  function create6(context) {
754
- if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
755
- return useNoDirectSetStateInUseEffect(context, {
756
- onViolation(ctx, node, data) {
757
- ctx.report({ messageId: "noDirectSetStateInUseLayoutEffect", node, data });
758
- },
759
- useEffectKind: "useLayoutEffect"
760
- });
724
+ const alias = getSettingsFromContext(context).additionalHooks.useState ?? [];
725
+ const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias);
726
+ return {
727
+ CallExpression(node) {
728
+ if (!ER.isReactHookCall(node)) {
729
+ return;
730
+ }
731
+ if (!isUseStateCall(node)) {
732
+ return;
733
+ }
734
+ const [useStateInput] = node.arguments;
735
+ if (useStateInput == null) {
736
+ return;
737
+ }
738
+ for (const expr of AST.getNestedNewExpressions(useStateInput)) {
739
+ if (!("name" in expr.callee)) continue;
740
+ if (ALLOW_LIST.includes(expr.callee.name)) continue;
741
+ if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
742
+ context.report({
743
+ messageId: "preferUseStateLazyInitialization",
744
+ node: expr
745
+ });
746
+ }
747
+ for (const expr of AST.getNestedCallExpressions(useStateInput)) {
748
+ if (!("name" in expr.callee)) continue;
749
+ if (ER.isReactHookName(expr.callee.name)) continue;
750
+ if (ALLOW_LIST.includes(expr.callee.name)) continue;
751
+ if (AST.findParentNode(expr, (n) => ER.isUseCall(context, n)) != null) continue;
752
+ context.report({
753
+ messageId: "preferUseStateLazyInitialization",
754
+ node: expr
755
+ });
756
+ }
757
+ }
758
+ };
761
759
  }
762
760
 
763
761
  // src/plugin.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-hooks-extra",
3
- "version": "2.0.0-next.45",
3
+ "version": "2.0.0-next.48",
4
4
  "description": "ESLint React's ESLint plugin for React Hooks related rules.",
5
5
  "keywords": [
6
6
  "react",
@@ -36,18 +36,18 @@
36
36
  "./package.json"
37
37
  ],
38
38
  "dependencies": {
39
- "@typescript-eslint/scope-manager": "^8.34.0",
40
- "@typescript-eslint/type-utils": "^8.34.0",
41
- "@typescript-eslint/types": "^8.34.0",
42
- "@typescript-eslint/utils": "^8.34.0",
39
+ "@typescript-eslint/scope-manager": "^8.34.1",
40
+ "@typescript-eslint/type-utils": "^8.34.1",
41
+ "@typescript-eslint/types": "^8.34.1",
42
+ "@typescript-eslint/utils": "^8.34.1",
43
43
  "string-ts": "^2.2.1",
44
44
  "ts-pattern": "^5.7.1",
45
- "@eslint-react/ast": "2.0.0-next.45",
46
- "@eslint-react/kit": "2.0.0-next.45",
47
- "@eslint-react/eff": "2.0.0-next.45",
48
- "@eslint-react/shared": "2.0.0-next.45",
49
- "@eslint-react/var": "2.0.0-next.45",
50
- "@eslint-react/core": "2.0.0-next.45"
45
+ "@eslint-react/ast": "2.0.0-next.48",
46
+ "@eslint-react/eff": "2.0.0-next.48",
47
+ "@eslint-react/core": "2.0.0-next.48",
48
+ "@eslint-react/kit": "2.0.0-next.48",
49
+ "@eslint-react/shared": "2.0.0-next.48",
50
+ "@eslint-react/var": "2.0.0-next.48"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/react": "^19.1.8",