eslint-plugin-react-hooks-extra 1.5.25 → 1.5.26-beta.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,9 +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
- | `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
@@ -8,6 +8,8 @@ declare const rules: {
8
8
  readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_CUSTOM_HOOKS_USING_OTHER_HOOKS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
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
+ 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>;
11
13
  readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"PREFER_USE_STATE_LAZY_INITIALIZATION", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
14
  };
13
15
 
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ declare const rules: {
8
8
  readonly "ensure-custom-hooks-using-other-hooks": _typescript_eslint_utils_ts_eslint.RuleModule<"ENSURE_CUSTOM_HOOKS_USING_OTHER_HOOKS", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
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
+ 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>;
11
13
  readonly "prefer-use-state-lazy-initialization": _typescript_eslint_utils_ts_eslint.RuleModule<"PREFER_USE_STATE_LAZY_INITIALIZATION", [], _typescript_eslint_utils_ts_eslint.RuleListener>;
12
14
  };
13
15
 
package/dist/index.js CHANGED
@@ -5,10 +5,11 @@ var shared = require('@eslint-react/shared');
5
5
  var ast = require('@eslint-react/ast');
6
6
  var tools = require('@eslint-react/tools');
7
7
  var _var = require('@eslint-react/var');
8
+ var astUtils = require('@typescript-eslint/utils/ast-utils');
8
9
 
9
10
  // package.json
10
11
  var name = "eslint-plugin-react-hooks-extra";
11
- var version = "1.5.25";
12
+ var version = "1.5.26-beta.2";
12
13
  var createRule = shared.createRuleForPlugin("hooks-extra");
13
14
 
14
15
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -226,7 +227,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
226
227
  description: "enforce 'useCallback' has non-empty dependencies array"
227
228
  },
228
229
  messages: {
229
- 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."
230
231
  },
231
232
  schema: []
232
233
  },
@@ -235,12 +236,15 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
235
236
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useCallback ?? [];
236
237
  return {
237
238
  CallExpression(node) {
238
- const initialScope = context.sourceCode.getScope(node);
239
239
  if (!core.isReactHookCall(node)) return;
240
+ const initialScope = context.sourceCode.getScope(node);
240
241
  if (!core.isUseCallbackCall(node, context) && !alias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node))) {
241
242
  return;
242
243
  }
243
- 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;
244
248
  if (!deps) {
245
249
  context.report({
246
250
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
@@ -248,7 +252,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
248
252
  });
249
253
  return;
250
254
  }
251
- const maybeDescriptor = tools.F.pipe(
255
+ const hasEmptyDeps = tools.F.pipe(
252
256
  $(deps).with({ type: ast.NodeType.ArrayExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
253
257
  return tools.F.pipe(
254
258
  _var.findVariable(n2.name, initialScope),
@@ -256,13 +260,32 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
256
260
  tools.O.filter(ast.is(ast.NodeType.ArrayExpression))
257
261
  );
258
262
  }).otherwise(tools.O.none),
259
- tools.O.filter((x2) => x2.elements.length === 0),
260
- tools.O.map(() => ({
263
+ tools.O.exists((x2) => x2.elements.length === 0)
264
+ );
265
+ if (!hasEmptyDeps) return;
266
+ if (!cb) {
267
+ context.report({
261
268
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
262
269
  node
263
- }))
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))
264
283
  );
265
- 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
+ });
266
289
  }
267
290
  };
268
291
  },
@@ -276,7 +299,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
276
299
  description: "enforce 'useMemo' has non-empty dependencies array"
277
300
  },
278
301
  messages: {
279
- 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."
280
303
  },
281
304
  schema: []
282
305
  },
@@ -285,10 +308,15 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
285
308
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useMemo ?? [];
286
309
  return {
287
310
  CallExpression(node) {
288
- const initialScope = context.sourceCode.getScope(node);
289
311
  if (!core.isReactHookCall(node)) return;
290
- if (!core.isUseMemoCall(node, context) && !alias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node))) return;
291
- 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;
292
320
  if (!deps) {
293
321
  context.report({
294
322
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
@@ -296,7 +324,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
296
324
  });
297
325
  return;
298
326
  }
299
- const maybeDescriptor = tools.F.pipe(
327
+ const hasEmptyDeps = tools.F.pipe(
300
328
  $(deps).with({ type: ast.NodeType.ArrayExpression }, tools.O.some).with({ type: ast.NodeType.Identifier }, (n2) => {
301
329
  return tools.F.pipe(
302
330
  _var.findVariable(n2.name, initialScope),
@@ -304,19 +332,194 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
304
332
  tools.O.filter(ast.is(ast.NodeType.ArrayExpression))
305
333
  );
306
334
  }).otherwise(tools.O.none),
307
- tools.O.filter((x2) => x2.elements.length === 0),
308
- tools.O.map(() => ({
335
+ tools.O.exists((x2) => x2.elements.length === 0)
336
+ );
337
+ if (!hasEmptyDeps) return;
338
+ if (!cb) {
339
+ context.report({
309
340
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
310
341
  node
311
- }))
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))
355
+ );
356
+ if (isReferencedToComponentScope) return;
357
+ context.report({
358
+ messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
359
+ node
360
+ });
361
+ }
362
+ };
363
+ },
364
+ defaultOptions: []
365
+ });
366
+ var RULE_NAME4 = "no-direct-set-state-in-use-effect";
367
+ var no_direct_set_state_in_use_effect_default = createRule({
368
+ meta: {
369
+ type: "problem",
370
+ docs: {
371
+ description: "disallow direct calls to the 'set' function of 'useState' in 'useEffect'"
372
+ },
373
+ messages: {
374
+ NO_DIRECT_SET_STATE_IN_USE_EFFECT: "Do not call the 'set' function of 'useState' directly in 'useEffect'."
375
+ },
376
+ schema: []
377
+ },
378
+ name: RULE_NAME4,
379
+ create(context) {
380
+ const settings = shared.getESLintReactSettings(context.settings);
381
+ const { useEffect: useEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
382
+ function isUseEffectCallWithAlias(node, context2) {
383
+ return core.isUseEffectCall(node, context2) || useEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
384
+ }
385
+ function isUseStateCallWithAlias(node, context2) {
386
+ return core.isUseStateCall(node, context2) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
387
+ }
388
+ function isEffectFunction(node) {
389
+ return node.parent?.type === ast.NodeType.CallExpression && isUseEffectCallWithAlias(node.parent, context);
390
+ }
391
+ return {
392
+ CallExpression(node) {
393
+ const effectFunction = ast.traverseUp(node, isEffectFunction);
394
+ if (tools.O.isNone(effectFunction)) return;
395
+ const scope = context.sourceCode.getScope(node);
396
+ if (scope.block !== effectFunction.value) return;
397
+ const name2 = $(node.callee).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({ type: ast.NodeType.MemberExpression }, (n2) => {
398
+ if (!("name" in n2.object)) return tools.O.none();
399
+ const initialScope = context.sourceCode.getScope(n2);
400
+ const property = astUtils.getStaticValue(n2.property, initialScope);
401
+ if (property?.value === 1) return tools.O.fromNullable(n2.object.name);
402
+ return tools.O.none();
403
+ }).with({ type: ast.NodeType.CallExpression }, (n2) => {
404
+ if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
405
+ if (!("name" in n2.callee.object)) return tools.O.none();
406
+ const isAt = $(n2.callee).with(
407
+ {
408
+ type: ast.NodeType.MemberExpression,
409
+ property: {
410
+ type: ast.NodeType.Identifier,
411
+ name: "at"
412
+ }
413
+ },
414
+ tools.F.constTrue
415
+ ).otherwise(tools.F.constFalse);
416
+ if (!isAt) return tools.O.none();
417
+ const [index] = n2.arguments;
418
+ if (!index) return tools.O.none();
419
+ const initialScope = context.sourceCode.getScope(n2);
420
+ const value = astUtils.getStaticValue(index, initialScope);
421
+ if (value?.value === 1) return tools.O.fromNullable(n2.callee.object.name);
422
+ return tools.O.none();
423
+ }).otherwise(tools.O.none);
424
+ tools.F.pipe(
425
+ name2,
426
+ tools.O.flatMap(_var.findVariable(scope)),
427
+ tools.O.flatMap(_var.getVariableInit(0)),
428
+ tools.O.filter(ast.is(ast.NodeType.CallExpression)),
429
+ tools.O.filter((name3) => isUseStateCallWithAlias(name3, context)),
430
+ tools.O.map((name3) => ({
431
+ data: {
432
+ setState: name3
433
+ },
434
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
435
+ node
436
+ })),
437
+ tools.O.map(context.report)
438
+ );
439
+ }
440
+ };
441
+ },
442
+ defaultOptions: []
443
+ });
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)
312
516
  );
313
- tools.O.map(maybeDescriptor, context.report);
314
517
  }
315
518
  };
316
519
  },
317
520
  defaultOptions: []
318
521
  });
319
- var RULE_NAME4 = "prefer-use-state-lazy-initialization";
522
+ var RULE_NAME6 = "prefer-use-state-lazy-initialization";
320
523
  var ALLOW_LIST = ["Boolean", "String", "Number"];
321
524
  var prefer_use_state_lazy_initialization_default = createRule({
322
525
  meta: {
@@ -329,7 +532,7 @@ var prefer_use_state_lazy_initialization_default = createRule({
329
532
  },
330
533
  schema: []
331
534
  },
332
- name: RULE_NAME4,
535
+ name: RULE_NAME6,
333
536
  create(context) {
334
537
  const alias = shared.getESLintReactSettings(context.settings).additionalHooks?.useState ?? [];
335
538
  return {
@@ -362,6 +565,8 @@ var rules = {
362
565
  "ensure-custom-hooks-using-other-hooks": ensure_custom_hooks_using_other_hooks_default,
363
566
  "ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
364
567
  "ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
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,
365
570
  "prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
366
571
  };
367
572
 
package/dist/index.mjs CHANGED
@@ -1,12 +1,13 @@
1
- import { useHookCollector, isReactHookCall, isUseCallbackCall, isReactHookCallWithNameLoose, isUseMemoCall, isUseStateCall } 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, 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
+ import { getStaticValue } from '@typescript-eslint/utils/ast-utils';
6
7
 
7
8
  // package.json
8
9
  var name = "eslint-plugin-react-hooks-extra";
9
- var version = "1.5.25";
10
+ var version = "1.5.26-beta.2";
10
11
  var createRule = createRuleForPlugin("hooks-extra");
11
12
 
12
13
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -224,7 +225,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
224
225
  description: "enforce 'useCallback' has non-empty dependencies array"
225
226
  },
226
227
  messages: {
227
- 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."
228
229
  },
229
230
  schema: []
230
231
  },
@@ -233,12 +234,15 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
233
234
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useCallback ?? [];
234
235
  return {
235
236
  CallExpression(node) {
236
- const initialScope = context.sourceCode.getScope(node);
237
237
  if (!isReactHookCall(node)) return;
238
+ const initialScope = context.sourceCode.getScope(node);
238
239
  if (!isUseCallbackCall(node, context) && !alias.some(F.flip(isReactHookCallWithNameLoose)(node))) {
239
240
  return;
240
241
  }
241
- 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;
242
246
  if (!deps) {
243
247
  context.report({
244
248
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
@@ -246,7 +250,7 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
246
250
  });
247
251
  return;
248
252
  }
249
- const maybeDescriptor = F.pipe(
253
+ const hasEmptyDeps = F.pipe(
250
254
  $(deps).with({ type: NodeType.ArrayExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
251
255
  return F.pipe(
252
256
  findVariable(n2.name, initialScope),
@@ -254,13 +258,32 @@ var ensure_use_callback_has_non_empty_deps_default = createRule({
254
258
  O.filter(is(NodeType.ArrayExpression))
255
259
  );
256
260
  }).otherwise(O.none),
257
- O.filter((x2) => x2.elements.length === 0),
258
- O.map(() => ({
261
+ O.exists((x2) => x2.elements.length === 0)
262
+ );
263
+ if (!hasEmptyDeps) return;
264
+ if (!cb) {
265
+ context.report({
259
266
  messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
260
267
  node
261
- }))
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))
262
281
  );
263
- O.map(maybeDescriptor, context.report);
282
+ if (isReferencedToComponentScope) return;
283
+ context.report({
284
+ messageId: "ENSURE_USE_CALLBACK_HAS_NON_EMPTY_DEPS",
285
+ node
286
+ });
264
287
  }
265
288
  };
266
289
  },
@@ -274,7 +297,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
274
297
  description: "enforce 'useMemo' has non-empty dependencies array"
275
298
  },
276
299
  messages: {
277
- 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."
278
301
  },
279
302
  schema: []
280
303
  },
@@ -283,10 +306,15 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
283
306
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useMemo ?? [];
284
307
  return {
285
308
  CallExpression(node) {
286
- const initialScope = context.sourceCode.getScope(node);
287
309
  if (!isReactHookCall(node)) return;
288
- if (!isUseMemoCall(node, context) && !alias.some(F.flip(isReactHookCallWithNameLoose)(node))) return;
289
- 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;
290
318
  if (!deps) {
291
319
  context.report({
292
320
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
@@ -294,7 +322,7 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
294
322
  });
295
323
  return;
296
324
  }
297
- const maybeDescriptor = F.pipe(
325
+ const hasEmptyDeps = F.pipe(
298
326
  $(deps).with({ type: NodeType.ArrayExpression }, O.some).with({ type: NodeType.Identifier }, (n2) => {
299
327
  return F.pipe(
300
328
  findVariable(n2.name, initialScope),
@@ -302,19 +330,194 @@ var ensure_use_memo_has_non_empty_deps_default = createRule({
302
330
  O.filter(is(NodeType.ArrayExpression))
303
331
  );
304
332
  }).otherwise(O.none),
305
- O.filter((x2) => x2.elements.length === 0),
306
- O.map(() => ({
333
+ O.exists((x2) => x2.elements.length === 0)
334
+ );
335
+ if (!hasEmptyDeps) return;
336
+ if (!cb) {
337
+ context.report({
307
338
  messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
308
339
  node
309
- }))
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))
353
+ );
354
+ if (isReferencedToComponentScope) return;
355
+ context.report({
356
+ messageId: "ENSURE_USE_MEMO_HAS_NON_EMPTY_DEPS",
357
+ node
358
+ });
359
+ }
360
+ };
361
+ },
362
+ defaultOptions: []
363
+ });
364
+ var RULE_NAME4 = "no-direct-set-state-in-use-effect";
365
+ var no_direct_set_state_in_use_effect_default = createRule({
366
+ meta: {
367
+ type: "problem",
368
+ docs: {
369
+ description: "disallow direct calls to the 'set' function of 'useState' in 'useEffect'"
370
+ },
371
+ messages: {
372
+ NO_DIRECT_SET_STATE_IN_USE_EFFECT: "Do not call the 'set' function of 'useState' directly in 'useEffect'."
373
+ },
374
+ schema: []
375
+ },
376
+ name: RULE_NAME4,
377
+ create(context) {
378
+ const settings = getESLintReactSettings(context.settings);
379
+ const { useEffect: useEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
380
+ function isUseEffectCallWithAlias(node, context2) {
381
+ return isUseEffectCall(node, context2) || useEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
382
+ }
383
+ function isUseStateCallWithAlias(node, context2) {
384
+ return isUseStateCall(node, context2) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
385
+ }
386
+ function isEffectFunction(node) {
387
+ return node.parent?.type === NodeType.CallExpression && isUseEffectCallWithAlias(node.parent, context);
388
+ }
389
+ return {
390
+ CallExpression(node) {
391
+ const effectFunction = traverseUp(node, isEffectFunction);
392
+ if (O.isNone(effectFunction)) return;
393
+ const scope = context.sourceCode.getScope(node);
394
+ if (scope.block !== effectFunction.value) return;
395
+ const name2 = $(node.callee).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({ type: NodeType.MemberExpression }, (n2) => {
396
+ if (!("name" in n2.object)) return O.none();
397
+ const initialScope = context.sourceCode.getScope(n2);
398
+ const property = getStaticValue(n2.property, initialScope);
399
+ if (property?.value === 1) return O.fromNullable(n2.object.name);
400
+ return O.none();
401
+ }).with({ type: NodeType.CallExpression }, (n2) => {
402
+ if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
403
+ if (!("name" in n2.callee.object)) return O.none();
404
+ const isAt = $(n2.callee).with(
405
+ {
406
+ type: NodeType.MemberExpression,
407
+ property: {
408
+ type: NodeType.Identifier,
409
+ name: "at"
410
+ }
411
+ },
412
+ F.constTrue
413
+ ).otherwise(F.constFalse);
414
+ if (!isAt) return O.none();
415
+ const [index] = n2.arguments;
416
+ if (!index) return O.none();
417
+ const initialScope = context.sourceCode.getScope(n2);
418
+ const value = getStaticValue(index, initialScope);
419
+ if (value?.value === 1) return O.fromNullable(n2.callee.object.name);
420
+ return O.none();
421
+ }).otherwise(O.none);
422
+ F.pipe(
423
+ name2,
424
+ O.flatMap(findVariable(scope)),
425
+ O.flatMap(getVariableInit(0)),
426
+ O.filter(is(NodeType.CallExpression)),
427
+ O.filter((name3) => isUseStateCallWithAlias(name3, context)),
428
+ O.map((name3) => ({
429
+ data: {
430
+ setState: name3
431
+ },
432
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
433
+ node
434
+ })),
435
+ O.map(context.report)
436
+ );
437
+ }
438
+ };
439
+ },
440
+ defaultOptions: []
441
+ });
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)
310
514
  );
311
- O.map(maybeDescriptor, context.report);
312
515
  }
313
516
  };
314
517
  },
315
518
  defaultOptions: []
316
519
  });
317
- var RULE_NAME4 = "prefer-use-state-lazy-initialization";
520
+ var RULE_NAME6 = "prefer-use-state-lazy-initialization";
318
521
  var ALLOW_LIST = ["Boolean", "String", "Number"];
319
522
  var prefer_use_state_lazy_initialization_default = createRule({
320
523
  meta: {
@@ -327,7 +530,7 @@ var prefer_use_state_lazy_initialization_default = createRule({
327
530
  },
328
531
  schema: []
329
532
  },
330
- name: RULE_NAME4,
533
+ name: RULE_NAME6,
331
534
  create(context) {
332
535
  const alias = getESLintReactSettings(context.settings).additionalHooks?.useState ?? [];
333
536
  return {
@@ -360,6 +563,8 @@ var rules = {
360
563
  "ensure-custom-hooks-using-other-hooks": ensure_custom_hooks_using_other_hooks_default,
361
564
  "ensure-use-callback-has-non-empty-deps": ensure_use_callback_has_non_empty_deps_default,
362
565
  "ensure-use-memo-has-non-empty-deps": ensure_use_memo_has_non_empty_deps_default,
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,
363
568
  "prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default
364
569
  };
365
570
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-hooks-extra",
3
- "version": "1.5.25",
3
+ "version": "1.5.26-beta.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/core": "1.5.25",
43
- "@eslint-react/shared": "1.5.25",
44
- "@eslint-react/jsx": "1.5.25",
45
- "@eslint-react/tools": "1.5.25",
46
- "@eslint-react/types": "1.5.25",
47
- "@eslint-react/ast": "1.5.25",
48
- "@eslint-react/var": "1.5.25"
42
+ "@eslint-react/ast": "1.5.26-beta.2",
43
+ "@eslint-react/jsx": "1.5.26-beta.2",
44
+ "@eslint-react/core": "1.5.26-beta.2",
45
+ "@eslint-react/shared": "1.5.26-beta.2",
46
+ "@eslint-react/tools": "1.5.26-beta.2",
47
+ "@eslint-react/types": "1.5.26-beta.2",
48
+ "@eslint-react/var": "1.5.26-beta.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "string-ts": "2.2.0",