eslint-plugin-react-hooks-extra 2.0.0-next.61 → 2.0.0

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