eslint-plugin-react-hooks-extra 2.0.0-next.4 → 2.0.0-next.44

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