eslint-plugin-react-hooks-extra 1.5.26-next.0 → 1.5.26-next.2

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
@@ -30,7 +30,8 @@ export default [
30
30
  "react-hooks-extra": reactHooksExtra,
31
31
  rules: {
32
32
  // react-hooks-extra recommended rules
33
- "react-hooks-extra/ensure-custom-hooks-using-other-hooks": "warn",
33
+ "react-hooks-extra/no-direct-set-state-in-use-effect": "warn",
34
+ "react-hooks-extra/no-direct-set-state-in-use-layout-effect": "warn",
34
35
  "react-hooks-extra/prefer-use-state-lazy-initialization": "warn",
35
36
  }
36
37
  ],
@@ -40,10 +41,11 @@ export default [
40
41
 
41
42
  ## Rules
42
43
 
43
- | Rule | Description | 💼 | 💭 | ❌ |
44
- | :--------------------------------------- | :------------------------------------------------------------------------ | :-: | :-: | :-: |
45
- | `ensure-custom-hooks-using-other-hooks` | Warns when custom Hooks that don't use other Hooks. | ✔️ | | |
46
- | `ensure-use-callback-has-non-empty-deps` | Warns when `useCallback` is called with empty dependencies array. | 🧐 | | |
47
- | `ensure-use-memo-has-non-empty-deps` | Warns when `useMemo` is called with empty dependencies array. | 🧐 | | |
48
- | `no-direct-set-state-in-use-effect` | Disallow direct calls to the `set` function of `useState` in `useEffect`. | ✔️ | | |
49
- | `prefer-use-state-lazy-initialization` | Warns function calls made inside `useState` calls. | 🚀 | | |
44
+ | Rule | Description | 💼 | 💭 | ❌ |
45
+ | :----------------------------------------- | :------------------------------------------------------------------------------ | :-: | :-: | :-: |
46
+ | `ensure-custom-hooks-using-other-hooks` | Warns when custom Hooks that don't use other Hooks. | ✔️ | | |
47
+ | `ensure-use-callback-has-non-empty-deps` | Warns when `useCallback` is called with empty dependencies array. | 🧐 | | |
48
+ | `ensure-use-memo-has-non-empty-deps` | Warns when `useMemo` is called with empty dependencies array. | 🧐 | | |
49
+ | `no-direct-set-state-in-use-effect` | Disallow direct calls to the `set` function of `useState` in `useEffect`. | ✔️ | | |
50
+ | `no-direct-set-state-in-use-layout-effect` | Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`. | ✔️ | | |
51
+ | `prefer-use-state-lazy-initialization` | Warns function calls made inside `useState` calls. | 🚀 | | |
package/dist/index.d.mts CHANGED
@@ -9,6 +9,7 @@ declare const rules: {
9
9
  readonly "ensure-use-callback-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
10
10
  readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
11
11
  readonly "no-direct-set-state-in-use-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"NO_DIRECT_SET_STATE_IN_USE_EFFECT", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
+ readonly "no-direct-set-state-in-use-layout-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
13
  readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"PREFER_USE_STATE_LAZY_INITIALIZATION", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
13
14
  };
14
15
 
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ declare const rules: {
9
9
  readonly "ensure-use-callback-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
10
10
  readonly "ensure-use-memo-has-non-empty-deps": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
11
11
  readonly "no-direct-set-state-in-use-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"NO_DIRECT_SET_STATE_IN_USE_EFFECT", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
+ readonly "no-direct-set-state-in-use-layout-effect": _typescript_eslint_utils_ts_eslint.RuleModule<"NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
13
  readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"PREFER_USE_STATE_LAZY_INITIALIZATION", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
13
14
  };
14
15
 
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var astUtils = require('@typescript-eslint/utils/ast-utils');
9
9
 
10
10
  // package.json
11
11
  var name = "eslint-plugin-react-hooks-extra";
12
- var version = "1.5.26-next.0";
12
+ var version = "1.5.26-next.2";
13
13
  var createRule = shared.createRuleForPlugin("hooks-extra");
14
14
 
15
15
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -227,7 +227,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
227
227
  description: "enforce 'useCallback' has non-empty dependencies array"
228
228
  },
229
229
  messages: {
230
- ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS: "An useCallback should have a non-empty dependencies array."
230
+ ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS: "An 'useCallback' should have a non-empty dependencies array."
231
231
  },
232
232
  schema: []
233
233
  },
@@ -236,12 +236,15 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
236
236
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useCallback ?? [];
237
237
  return {
238
238
  CallExpression(node) {
239
- const initialScope = context.sourceCode.getScope(node);
240
239
  if (!core.isReactHookCall(node)) return;
240
+ const initialScope = context.sourceCode.getScope(node);
241
241
  if (!core.isUseCallbackCall(node, context) && !alias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node))) {
242
242
  return;
243
243
  }
244
- const [_2, deps] = node.arguments;
244
+ const scope = context.sourceCode.getScope(node);
245
+ const component = scope.block;
246
+ if (!ast.isFunction(component)) return;
247
+ const [cb, deps] = node.arguments;
245
248
  if (!deps) {
246
249
  context.report({
247
250
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
@@ -249,7 +252,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
249
252
  });
250
253
  return;
251
254
  }
252
- const maybeDescriptor = tools.F.pipe(
255
+ const hasEmptyDeps = tools.F.pipe(
253
256
  $(deps).with({ type: ast.NodeType.ArrayExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
254
257
  return tools.F.pipe(
255
258
  _var.findVariable(n2.name, initialScope),
@@ -257,13 +260,32 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
257
260
  tools.O.filter(ast.is(ast.NodeType.ArrayExpression))
258
261
  );
259
262
  }).otherwise(tools.O.none),
260
- tools.O.filter((x2) => x2.elements.length === 0),
261
- tools.O.map(() => ({
263
+ tools.O.exists((x2) => x2.elements.length === 0)
264
+ );
265
+ if (!hasEmptyDeps) return;
266
+ if (!cb) {
267
+ context.report({
262
268
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
263
269
  node
264
- }))
270
+ });
271
+ return;
272
+ }
273
+ const isReferencedToComponentScope = tools.F.pipe(
274
+ $(cb).with({ type: ast.NodeType.ArrowFunctionExpression }, tools.O.some).with({ type: ast.NodeType.FunctionExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
275
+ return tools.F.pipe(
276
+ _var.findVariable(n2.name, initialScope),
277
+ tools.O.flatMap(_var.getVariableInit(0)),
278
+ tools.O.filter(ast.isFunction)
279
+ );
280
+ }).otherwise(tools.O.none),
281
+ tools.O.map((n2) => context.sourceCode.getScope(n2).references),
282
+ tools.O.exists((refs) => refs.some((x2) => x2.resolved?.scope.block === component))
265
283
  );
266
- tools.O.map(maybeDescriptor, context.report);
284
+ if (isReferencedToComponentScope) return;
285
+ context.report({
286
+ messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
287
+ node
288
+ });
267
289
  }
268
290
  };
269
291
  },
@@ -277,7 +299,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
277
299
  description: "enforce 'useMemo' has non-empty dependencies array"
278
300
  },
279
301
  messages: {
280
- ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS: "An useMemo should have a non-empty dependencies array."
302
+ ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS: "An 'useMemo' should have a non-empty dependencies array."
281
303
  },
282
304
  schema: []
283
305
  },
@@ -286,10 +308,15 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
286
308
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useMemo ?? [];
287
309
  return {
288
310
  CallExpression(node) {
289
- const initialScope = context.sourceCode.getScope(node);
290
311
  if (!core.isReactHookCall(node)) return;
291
- if (!core.isUseMemoCall(node, context) && !alias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node))) return;
292
- const [_2, deps] = node.arguments;
312
+ const initialScope = context.sourceCode.getScope(node);
313
+ if (!core.isUseMemoCall(node, context) && !alias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node))) {
314
+ return;
315
+ }
316
+ const scope = context.sourceCode.getScope(node);
317
+ const component = scope.block;
318
+ if (!ast.isFunction(component)) return;
319
+ const [cb, deps] = node.arguments;
293
320
  if (!deps) {
294
321
  context.report({
295
322
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
@@ -297,7 +324,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
297
324
  });
298
325
  return;
299
326
  }
300
- const maybeDescriptor = tools.F.pipe(
327
+ const hasEmptyDeps = tools.F.pipe(
301
328
  $(deps).with({ type: ast.NodeType.ArrayExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
302
329
  return tools.F.pipe(
303
330
  _var.findVariable(n2.name, initialScope),
@@ -305,13 +332,32 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
305
332
  tools.O.filter(ast.is(ast.NodeType.ArrayExpression))
306
333
  );
307
334
  }).otherwise(tools.O.none),
308
- tools.O.filter((x2) => x2.elements.length === 0),
309
- tools.O.map(() => ({
335
+ tools.O.exists((x2) => x2.elements.length === 0)
336
+ );
337
+ if (!hasEmptyDeps) return;
338
+ if (!cb) {
339
+ context.report({
310
340
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
311
341
  node
312
- }))
342
+ });
343
+ return;
344
+ }
345
+ const isReferencedToComponentScope = tools.F.pipe(
346
+ $(cb).with({ type: ast.NodeType.ArrowFunctionExpression }, tools.O.some).with({ type: ast.NodeType.FunctionExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
347
+ return tools.F.pipe(
348
+ _var.findVariable(n2.name, initialScope),
349
+ tools.O.flatMap(_var.getVariableInit(0)),
350
+ tools.O.filter(ast.isFunction)
351
+ );
352
+ }).otherwise(tools.O.none),
353
+ tools.O.map((n2) => context.sourceCode.getScope(n2).references),
354
+ tools.O.exists((refs) => refs.some((x2) => x2.resolved?.scope.block === component))
313
355
  );
314
- tools.O.map(maybeDescriptor, context.report);
356
+ if (isReferencedToComponentScope) return;
357
+ context.report({
358
+ messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
359
+ node
360
+ });
315
361
  }
316
362
  };
317
363
  },
@@ -395,7 +441,85 @@ var no_direct_set_state_in_use_effect_default = createRule({
395
441
  },
396
442
  defaultOptions: []
397
443
  });
398
- var RULE_NAME5 = "prefer-use-state-lazy-initialization";
444
+ var RULE_NAME5 = "no-direct-set-state-in-use-layout-effect";
445
+ var no_direct_set_state_in_use_layout_effect_default = createRule({
446
+ meta: {
447
+ type: "problem",
448
+ docs: {
449
+ description: "disallow direct calls to the 'set' function of 'useState' in 'useLayoutEffect'"
450
+ },
451
+ messages: {
452
+ NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT: "Do not call the 'set' function of 'useState' directly in 'useLayoutEffect'."
453
+ },
454
+ schema: []
455
+ },
456
+ name: RULE_NAME5,
457
+ create(context) {
458
+ const settings = shared.getESLintReactSettings(context.settings);
459
+ const { useLayoutEffect: useLayoutEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
460
+ function isUseLayoutEffectCallWithAlias(node, context2) {
461
+ return core.isUseLayoutEffectCall(node, context2) || useLayoutEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
462
+ }
463
+ function isUseStateCallWithAlias(node, context2) {
464
+ return core.isUseStateCall(node, context2) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
465
+ }
466
+ function isEffectFunction(node) {
467
+ return node.parent?.type === ast.NodeType.CallExpression && isUseLayoutEffectCallWithAlias(node.parent, context);
468
+ }
469
+ return {
470
+ CallExpression(node) {
471
+ const effectFunction = ast.traverseUp(node, isEffectFunction);
472
+ if (tools.O.isNone(effectFunction)) return;
473
+ const scope = context.sourceCode.getScope(node);
474
+ if (scope.block !== effectFunction.value) return;
475
+ const name2 = $(node.callee).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({ type: ast.NodeType.MemberExpression }, (n2) => {
476
+ if (!("name" in n2.object)) return tools.O.none();
477
+ const initialScope = context.sourceCode.getScope(n2);
478
+ const property = astUtils.getStaticValue(n2.property, initialScope);
479
+ if (property?.value === 1) return tools.O.fromNullable(n2.object.name);
480
+ return tools.O.none();
481
+ }).with({ type: ast.NodeType.CallExpression }, (n2) => {
482
+ if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
483
+ if (!("name" in n2.callee.object)) return tools.O.none();
484
+ const isAt = $(n2.callee).with(
485
+ {
486
+ type: ast.NodeType.MemberExpression,
487
+ property: {
488
+ type: ast.NodeType.Identifier,
489
+ name: "at"
490
+ }
491
+ },
492
+ tools.F.constTrue
493
+ ).otherwise(tools.F.constFalse);
494
+ if (!isAt) return tools.O.none();
495
+ const [index] = n2.arguments;
496
+ if (!index) return tools.O.none();
497
+ const initialScope = context.sourceCode.getScope(n2);
498
+ const value = astUtils.getStaticValue(index, initialScope);
499
+ if (value?.value === 1) return tools.O.fromNullable(n2.callee.object.name);
500
+ return tools.O.none();
501
+ }).otherwise(tools.O.none);
502
+ tools.F.pipe(
503
+ name2,
504
+ tools.O.flatMap(_var.findVariable(scope)),
505
+ tools.O.flatMap(_var.getVariableInit(0)),
506
+ tools.O.filter(ast.is(ast.NodeType.CallExpression)),
507
+ tools.O.filter((name3) => isUseStateCallWithAlias(name3, context)),
508
+ tools.O.map((name3) => ({
509
+ data: {
510
+ setState: name3
511
+ },
512
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
513
+ node
514
+ })),
515
+ tools.O.map(context.report)
516
+ );
517
+ }
518
+ };
519
+ },
520
+ defaultOptions: []
521
+ });
522
+ var RULE_NAME6 = "prefer-use-state-lazy-initialization";
399
523
  var ALLOW_LIST = ["Boolean", "String", "Number"];
400
524
  var prefer_use_state_lazy_initialization_default = createRule({
401
525
  meta: {
@@ -408,7 +532,7 @@ var prefer_use_state_lazy_initialization_default = createRule({
408
532
  },
409
533
  schema: []
410
534
  },
411
- name: RULE_NAME5,
535
+ name: RULE_NAME6,
412
536
  create(context) {
413
537
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useState ?? [];
414
538
  return {
@@ -442,6 +566,7 @@ var rules = {
442
566
  "ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
443
567
  "ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
444
568
  "no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default,
569
+ "no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_layout_effect_default,
445
570
  "prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
446
571
  };
447
572
 
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { useHookCollector, isReactHookCall, isUseCallbackCall, isReactHookCallWithNameLoose, isUseMemoCall, isUseStateCall, isUseEffectCall } from '@eslint-react/core';
1
+ import { useHookCollector, isReactHookCall, isUseCallbackCall, isReactHookCallWithNameLoose, isUseMemoCall, isUseStateCall, isUseEffectCall, isUseLayoutEffectCall } from '@eslint-react/core';
2
2
  import { createRuleForPlugin, getESLintReactSettings } from '@eslint-react/shared';
3
- import { NodeType, is, traverseUp, getNestedCallExpressions } from '@eslint-react/ast';
3
+ import { isFunction, NodeType, is, traverseUp, getNestedCallExpressions } from '@eslint-react/ast';
4
4
  import { F, O } from '@eslint-react/tools';
5
5
  import { findVariable, getVariableInit } from '@eslint-react/var';
6
6
  import { getStaticValue } from '@typescript-eslint/utils/ast-utils';
7
7
 
8
8
  // package.json
9
9
  var name = "eslint-plugin-react-hooks-extra";
10
- var version = "1.5.26-next.0";
10
+ var version = "1.5.26-next.2";
11
11
  var createRule = createRuleForPlugin("hooks-extra");
12
12
 
13
13
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -225,7 +225,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
225
225
  description: "enforce 'useCallback' has non-empty dependencies array"
226
226
  },
227
227
  messages: {
228
- ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS: "An useCallback should have a non-empty dependencies array."
228
+ ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS: "An 'useCallback' should have a non-empty dependencies array."
229
229
  },
230
230
  schema: []
231
231
  },
@@ -234,12 +234,15 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
234
234
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useCallback ?? [];
235
235
  return {
236
236
  CallExpression(node) {
237
- const initialScope = context.sourceCode.getScope(node);
238
237
  if (!isReactHookCall(node)) return;
238
+ const initialScope = context.sourceCode.getScope(node);
239
239
  if (!isUseCallbackCall(node, context) && !alias.some(F.flip(isReactHookCallWithNameLoose)(node))) {
240
240
  return;
241
241
  }
242
- const [_2, deps] = node.arguments;
242
+ const scope = context.sourceCode.getScope(node);
243
+ const component = scope.block;
244
+ if (!isFunction(component)) return;
245
+ const [cb, deps] = node.arguments;
243
246
  if (!deps) {
244
247
  context.report({
245
248
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
@@ -247,7 +250,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
247
250
  });
248
251
  return;
249
252
  }
250
- const maybeDescriptor = F.pipe(
253
+ const hasEmptyDeps = F.pipe(
251
254
  $(deps).with({ type: NodeType.ArrayExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
252
255
  return F.pipe(
253
256
  findVariable(n2.name, initialScope),
@@ -255,13 +258,32 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
255
258
  O.filter(is(NodeType.ArrayExpression))
256
259
  );
257
260
  }).otherwise(O.none),
258
- O.filter((x2) => x2.elements.length === 0),
259
- O.map(() => ({
261
+ O.exists((x2) => x2.elements.length === 0)
262
+ );
263
+ if (!hasEmptyDeps) return;
264
+ if (!cb) {
265
+ context.report({
260
266
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
261
267
  node
262
- }))
268
+ });
269
+ return;
270
+ }
271
+ const isReferencedToComponentScope = F.pipe(
272
+ $(cb).with({ type: NodeType.ArrowFunctionExpression }, O.some).with({ type: NodeType.FunctionExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
273
+ return F.pipe(
274
+ findVariable(n2.name, initialScope),
275
+ O.flatMap(getVariableInit(0)),
276
+ O.filter(isFunction)
277
+ );
278
+ }).otherwise(O.none),
279
+ O.map((n2) => context.sourceCode.getScope(n2).references),
280
+ O.exists((refs) => refs.some((x2) => x2.resolved?.scope.block === component))
263
281
  );
264
- O.map(maybeDescriptor, context.report);
282
+ if (isReferencedToComponentScope) return;
283
+ context.report({
284
+ messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
285
+ node
286
+ });
265
287
  }
266
288
  };
267
289
  },
@@ -275,7 +297,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
275
297
  description: "enforce 'useMemo' has non-empty dependencies array"
276
298
  },
277
299
  messages: {
278
- ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS: "An useMemo should have a non-empty dependencies array."
300
+ ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS: "An 'useMemo' should have a non-empty dependencies array."
279
301
  },
280
302
  schema: []
281
303
  },
@@ -284,10 +306,15 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
284
306
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useMemo ?? [];
285
307
  return {
286
308
  CallExpression(node) {
287
- const initialScope = context.sourceCode.getScope(node);
288
309
  if (!isReactHookCall(node)) return;
289
- if (!isUseMemoCall(node, context) && !alias.some(F.flip(isReactHookCallWithNameLoose)(node))) return;
290
- const [_2, deps] = node.arguments;
310
+ const initialScope = context.sourceCode.getScope(node);
311
+ if (!isUseMemoCall(node, context) && !alias.some(F.flip(isReactHookCallWithNameLoose)(node))) {
312
+ return;
313
+ }
314
+ const scope = context.sourceCode.getScope(node);
315
+ const component = scope.block;
316
+ if (!isFunction(component)) return;
317
+ const [cb, deps] = node.arguments;
291
318
  if (!deps) {
292
319
  context.report({
293
320
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
@@ -295,7 +322,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
295
322
  });
296
323
  return;
297
324
  }
298
- const maybeDescriptor = F.pipe(
325
+ const hasEmptyDeps = F.pipe(
299
326
  $(deps).with({ type: NodeType.ArrayExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
300
327
  return F.pipe(
301
328
  findVariable(n2.name, initialScope),
@@ -303,13 +330,32 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
303
330
  O.filter(is(NodeType.ArrayExpression))
304
331
  );
305
332
  }).otherwise(O.none),
306
- O.filter((x2) => x2.elements.length === 0),
307
- O.map(() => ({
333
+ O.exists((x2) => x2.elements.length === 0)
334
+ );
335
+ if (!hasEmptyDeps) return;
336
+ if (!cb) {
337
+ context.report({
308
338
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
309
339
  node
310
- }))
340
+ });
341
+ return;
342
+ }
343
+ const isReferencedToComponentScope = F.pipe(
344
+ $(cb).with({ type: NodeType.ArrowFunctionExpression }, O.some).with({ type: NodeType.FunctionExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
345
+ return F.pipe(
346
+ findVariable(n2.name, initialScope),
347
+ O.flatMap(getVariableInit(0)),
348
+ O.filter(isFunction)
349
+ );
350
+ }).otherwise(O.none),
351
+ O.map((n2) => context.sourceCode.getScope(n2).references),
352
+ O.exists((refs) => refs.some((x2) => x2.resolved?.scope.block === component))
311
353
  );
312
- O.map(maybeDescriptor, context.report);
354
+ if (isReferencedToComponentScope) return;
355
+ context.report({
356
+ messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
357
+ node
358
+ });
313
359
  }
314
360
  };
315
361
  },
@@ -393,7 +439,85 @@ var no_direct_set_state_in_use_effect_default = createRule({
393
439
  },
394
440
  defaultOptions: []
395
441
  });
396
- var RULE_NAME5 = "prefer-use-state-lazy-initialization";
442
+ var RULE_NAME5 = "no-direct-set-state-in-use-layout-effect";
443
+ var no_direct_set_state_in_use_layout_effect_default = createRule({
444
+ meta: {
445
+ type: "problem",
446
+ docs: {
447
+ description: "disallow direct calls to the 'set' function of 'useState' in 'useLayoutEffect'"
448
+ },
449
+ messages: {
450
+ NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT: "Do not call the 'set' function of 'useState' directly in 'useLayoutEffect'."
451
+ },
452
+ schema: []
453
+ },
454
+ name: RULE_NAME5,
455
+ create(context) {
456
+ const settings = getESLintReactSettings(context.settings);
457
+ const { useLayoutEffect: useLayoutEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
458
+ function isUseLayoutEffectCallWithAlias(node, context2) {
459
+ return isUseLayoutEffectCall(node, context2) || useLayoutEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
460
+ }
461
+ function isUseStateCallWithAlias(node, context2) {
462
+ return isUseStateCall(node, context2) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
463
+ }
464
+ function isEffectFunction(node) {
465
+ return node.parent?.type === NodeType.CallExpression && isUseLayoutEffectCallWithAlias(node.parent, context);
466
+ }
467
+ return {
468
+ CallExpression(node) {
469
+ const effectFunction = traverseUp(node, isEffectFunction);
470
+ if (O.isNone(effectFunction)) return;
471
+ const scope = context.sourceCode.getScope(node);
472
+ if (scope.block !== effectFunction.value) return;
473
+ const name2 = $(node.callee).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({ type: NodeType.MemberExpression }, (n2) => {
474
+ if (!("name" in n2.object)) return O.none();
475
+ const initialScope = context.sourceCode.getScope(n2);
476
+ const property = getStaticValue(n2.property, initialScope);
477
+ if (property?.value === 1) return O.fromNullable(n2.object.name);
478
+ return O.none();
479
+ }).with({ type: NodeType.CallExpression }, (n2) => {
480
+ if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
481
+ if (!("name" in n2.callee.object)) return O.none();
482
+ const isAt = $(n2.callee).with(
483
+ {
484
+ type: NodeType.MemberExpression,
485
+ property: {
486
+ type: NodeType.Identifier,
487
+ name: "at"
488
+ }
489
+ },
490
+ F.constTrue
491
+ ).otherwise(F.constFalse);
492
+ if (!isAt) return O.none();
493
+ const [index] = n2.arguments;
494
+ if (!index) return O.none();
495
+ const initialScope = context.sourceCode.getScope(n2);
496
+ const value = getStaticValue(index, initialScope);
497
+ if (value?.value === 1) return O.fromNullable(n2.callee.object.name);
498
+ return O.none();
499
+ }).otherwise(O.none);
500
+ F.pipe(
501
+ name2,
502
+ O.flatMap(findVariable(scope)),
503
+ O.flatMap(getVariableInit(0)),
504
+ O.filter(is(NodeType.CallExpression)),
505
+ O.filter((name3) => isUseStateCallWithAlias(name3, context)),
506
+ O.map((name3) => ({
507
+ data: {
508
+ setState: name3
509
+ },
510
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
511
+ node
512
+ })),
513
+ O.map(context.report)
514
+ );
515
+ }
516
+ };
517
+ },
518
+ defaultOptions: []
519
+ });
520
+ var RULE_NAME6 = "prefer-use-state-lazy-initialization";
397
521
  var ALLOW_LIST = ["Boolean", "String", "Number"];
398
522
  var prefer_use_state_lazy_initialization_default = createRule({
399
523
  meta: {
@@ -406,7 +530,7 @@ var prefer_use_state_lazy_initialization_default = createRule({
406
530
  },
407
531
  schema: []
408
532
  },
409
- name: RULE_NAME5,
533
+ name: RULE_NAME6,
410
534
  create(context) {
411
535
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useState ?? [];
412
536
  return {
@@ -440,6 +564,7 @@ var rules = {
440
564
  "ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
441
565
  "ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
442
566
  "no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default,
567
+ "no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_layout_effect_default,
443
568
  "prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
444
569
  };
445
570
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-hooks-extra",
3
- "version": "1.5.26-next.0",
3
+ "version": "1.5.26-next.2",
4
4
  "description": "ESLint React's ESLint plugin for React Hooks related rules.",
5
5
  "homepage": "https://github.com/rel1cx/eslint-react",
6
6
  "bugs": {
@@ -39,13 +39,13 @@
39
39
  "@typescript-eslint/type-utils": "^7.16.0",
40
40
  "@typescript-eslint/types": "^7.16.0",
41
41
  "@typescript-eslint/utils": "^7.16.0",
42
- "@eslint-react/ast": "1.5.26-next.0",
43
- "@eslint-react/core": "1.5.26-next.0",
44
- "@eslint-react/jsx": "1.5.26-next.0",
45
- "@eslint-react/tools": "1.5.26-next.0",
46
- "@eslint-react/shared": "1.5.26-next.0",
47
- "@eslint-react/types": "1.5.26-next.0",
48
- "@eslint-react/var": "1.5.26-next.0"
42
+ "@eslint-react/ast": "1.5.26-next.2",
43
+ "@eslint-react/jsx": "1.5.26-next.2",
44
+ "@eslint-react/shared": "1.5.26-next.2",
45
+ "@eslint-react/tools": "1.5.26-next.2",
46
+ "@eslint-react/types": "1.5.26-next.2",
47
+ "@eslint-react/core": "1.5.26-next.2",
48
+ "@eslint-react/var": "1.5.26-next.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "string-ts": "2.2.0",