eslint-plugin-react-hooks-extra 1.5.28-beta.0 → 1.5.28-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.
Files changed (3) hide show
  1. package/dist/index.js +319 -116
  2. package/dist/index.mjs +321 -118
  3. package/package.json +9 -9
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.28-beta.0";
12
+ var version = "1.5.28-beta.2";
13
13
  var createRule = shared.createRuleForPlugin("hooks-extra");
14
14
 
15
15
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -63,32 +63,45 @@ var o = (n2, s2, c2) => {
63
63
  if (!r(s2)) return false;
64
64
  if (Array.isArray(n2)) {
65
65
  if (!Array.isArray(s2)) return false;
66
- let t2 = [], r2 = [], a = [];
66
+ let t2 = [], r2 = [], a2 = [];
67
67
  for (const o2 of n2.keys()) {
68
68
  const s3 = n2[o2];
69
- i(s3) && s3[e] ? a.push(s3) : a.length ? r2.push(s3) : t2.push(s3);
69
+ i(s3) && s3[e] ? a2.push(s3) : a2.length ? r2.push(s3) : t2.push(s3);
70
70
  }
71
- if (a.length) {
72
- if (a.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
71
+ if (a2.length) {
72
+ if (a2.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
73
73
  if (s2.length < t2.length + r2.length) return false;
74
74
  const e2 = s2.slice(0, t2.length), n3 = 0 === r2.length ? [] : s2.slice(-r2.length), i2 = s2.slice(t2.length, 0 === r2.length ? Infinity : -r2.length);
75
- return t2.every((t3, n4) => o(t3, e2[n4], c2)) && r2.every((t3, e3) => o(t3, n3[e3], c2)) && (0 === a.length || o(a[0], i2, c2));
75
+ return t2.every((t3, n4) => o(t3, e2[n4], c2)) && r2.every((t3, e3) => o(t3, n3[e3], c2)) && (0 === a2.length || o(a2[0], i2, c2));
76
76
  }
77
77
  return n2.length === s2.length && n2.every((t3, e2) => o(t3, s2[e2], c2));
78
78
  }
79
79
  return Object.keys(n2).every((e2) => {
80
80
  const r2 = n2[e2];
81
- return (e2 in s2 || i(a = r2) && "optional" === a[t]().matcherType) && o(r2, s2[e2], c2);
82
- var a;
81
+ return (e2 in s2 || i(a2 = r2) && "optional" === a2[t]().matcherType) && o(r2, s2[e2], c2);
82
+ var a2;
83
83
  });
84
84
  }
85
85
  return Object.is(s2, n2);
86
86
  };
87
87
  var s = (e2) => {
88
- var n2, o2, a;
89
- return r(e2) ? i(e2) ? null != (n2 = null == (o2 = (a = e2[t]()).getSelectionKeys) ? void 0 : o2.call(a)) ? n2 : [] : Array.isArray(e2) ? c(e2, s) : c(Object.values(e2), s) : [];
88
+ var n2, o2, a2;
89
+ return r(e2) ? i(e2) ? null != (n2 = null == (o2 = (a2 = e2[t]()).getSelectionKeys) ? void 0 : o2.call(a2)) ? n2 : [] : Array.isArray(e2) ? c(e2, s) : c(Object.values(e2), s) : [];
90
90
  };
91
91
  var c = (t2, e2) => t2.reduce((t3, n2) => t3.concat(e2(n2)), []);
92
+ function a(...t2) {
93
+ if (1 === t2.length) {
94
+ const [e2] = t2;
95
+ return (t3) => o(e2, t3, () => {
96
+ });
97
+ }
98
+ if (2 === t2.length) {
99
+ const [e2, n2] = t2;
100
+ return o(e2, n2, () => {
101
+ });
102
+ }
103
+ throw new Error(`isMatching wasn't given the right number of arguments: expected 1 or 2, received ${t2.length}.`);
104
+ }
92
105
  function u(t2) {
93
106
  return Object.assign(t2, { optional: () => l(t2), and: (e2) => m(t2, e2), or: (e2) => d(t2, e2), select: (e2) => void 0 === e2 ? p(t2) : p(e2, t2) });
94
107
  }
@@ -187,9 +200,9 @@ var z = class _z {
187
200
  let i2;
188
201
  3 === t2.length && "function" == typeof t2[1] ? i2 = t2[1] : t2.length > 2 && r2.push(...t2.slice(1, t2.length - 1));
189
202
  let s2 = false, c2 = {};
190
- const a = (t3, e3) => {
203
+ const a2 = (t3, e3) => {
191
204
  s2 = true, c2[t3] = e3;
192
- }, u2 = !r2.some((t3) => o(t3, this.input, a)) || i2 && !Boolean(i2(this.input)) ? W : { matched: true, value: e2(s2 ? n in c2 ? c2[n] : c2 : this.input, this.input) };
205
+ }, u2 = !r2.some((t3) => o(t3, this.input, a2)) || i2 && !Boolean(i2(this.input)) ? W : { matched: true, value: e2(s2 ? n in c2 ? c2[n] : c2 : this.input, this.input) };
193
206
  return new _z(this.input, u2);
194
207
  }
195
208
  when(t2, e2) {
@@ -391,64 +404,159 @@ var no_direct_set_state_in_use_effect_default = createRule({
391
404
  create(context) {
392
405
  const settings = shared.getESLintReactSettings(context.settings);
393
406
  const { useEffect: useEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
394
- function isUseEffectCallWithAlias(node, context2) {
395
- return core.isUseEffectCall(node, context2) || useEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
407
+ function isUseEffectCallWithAlias(node) {
408
+ return core.isUseEffectCall(node, context) || useEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
396
409
  }
397
- function isUseStateCallWithAlias(node, context2) {
398
- return core.isUseStateCall(node, context2) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
410
+ function isUseStateCallWithAlias(node) {
411
+ return core.isUseStateCall(node, context) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
399
412
  }
400
413
  function isEffectFunction(node) {
401
- return node.parent?.type === ast.NodeType.CallExpression && isUseEffectCallWithAlias(node.parent, context);
414
+ return node.parent?.type === ast.NodeType.CallExpression && node.parent.callee !== node && isUseEffectCallWithAlias(node.parent);
415
+ }
416
+ function isCleanUpFunction(_2) {
402
417
  }
418
+ function isFromUseStateCall(id) {
419
+ return tools.F.pipe(
420
+ $(id).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({
421
+ type: ast.NodeType.MemberExpression,
422
+ object: { type: ast.NodeType.Identifier }
423
+ }, (n2) => tools.O.some(n2.object.name)).otherwise(tools.O.none),
424
+ tools.O.flatMap(_var.findVariable(context.sourceCode.getScope(id))),
425
+ tools.O.flatMap(_var.getVariableInit(0)),
426
+ tools.O.filter(ast.is(ast.NodeType.CallExpression)),
427
+ tools.O.exists(isUseStateCallWithAlias)
428
+ );
429
+ }
430
+ function isSetStateCall(node) {
431
+ const id = $(node.callee).with({ type: ast.NodeType.Identifier }, tools.O.some).with({ type: ast.NodeType.MemberExpression }, (n2) => {
432
+ if (!("name" in n2.object)) return tools.O.none();
433
+ const initialScope = context.sourceCode.getScope(n2);
434
+ const property = astUtils.getStaticValue(n2.property, initialScope);
435
+ if (property?.value === 1) return tools.O.fromNullable(n2.object);
436
+ return tools.O.none();
437
+ }).with({ type: ast.NodeType.CallExpression }, (n2) => {
438
+ if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
439
+ if (!("name" in n2.callee.object)) return tools.O.none();
440
+ const isAt = a({
441
+ type: ast.NodeType.MemberExpression,
442
+ property: {
443
+ type: ast.NodeType.Identifier,
444
+ name: "at"
445
+ }
446
+ }, n2.callee);
447
+ const [index] = n2.arguments;
448
+ if (!isAt || !index) return tools.O.none();
449
+ const initialScope = context.sourceCode.getScope(n2);
450
+ const value = astUtils.getStaticValue(index, initialScope);
451
+ if (value?.value === 1) return tools.O.fromNullable(n2.callee.object);
452
+ return tools.O.none();
453
+ }).otherwise(tools.O.none);
454
+ return tools.O.exists(id, isFromUseStateCall);
455
+ }
456
+ function isThenCall(node) {
457
+ if (node.callee.type !== ast.NodeType.MemberExpression) return false;
458
+ return a({
459
+ type: ast.NodeType.Identifier,
460
+ name: "then"
461
+ }, node.callee.property);
462
+ }
463
+ function getCallKind(node) {
464
+ return $(node).when(isThenCall, () => "then").when(isSetStateCall, () => "setState").when(isUseStateCallWithAlias, () => "useState").when(isUseEffectCallWithAlias, () => "useEffect").otherwise(() => "other");
465
+ }
466
+ function getFunctionKind(node) {
467
+ return $(node).when(isEffectFunction, () => "effect").when(isCleanUpFunction, () => "cleanup").when(ast.isIIFE, () => "immediate").otherwise(() => "other");
468
+ }
469
+ const useEffectCallRef = tools.MutRef.make(null);
470
+ const callStack = tools.MutList.make();
471
+ const functionStack = tools.MutList.make();
472
+ const effectFunctionRef = tools.MutRef.make(null);
473
+ const cleanUpFunctionRef = tools.MutRef.make(null);
474
+ const indirectFunctionCalls = [];
475
+ const indirectSetStateCalls = /* @__PURE__ */ new Map();
476
+ const onUseEffectCallEnter = (node) => void tools.MutRef.set(useEffectCallRef, node);
477
+ const onUseEffectCallExit = () => void tools.MutRef.set(useEffectCallRef, null);
403
478
  return {
479
+ Identifier(node) {
480
+ const isInUseEffectCall = tools.MutRef.get(useEffectCallRef) !== null;
481
+ const parentFn = tools.MutList.tail(functionStack)?.[0];
482
+ const useEffectCall = tools.MutRef.get(useEffectCallRef);
483
+ const isEffectFunction2 = useEffectCall && tools.O.isSome(ast.traverseUp(useEffectCall, (n2) => n2 === parentFn));
484
+ if (isFromUseStateCall(node) && isInUseEffectCall && isEffectFunction2) {
485
+ context.report({
486
+ node,
487
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT"
488
+ });
489
+ }
490
+ },
404
491
  CallExpression(node) {
405
- const effectFunction = ast.traverseUp(node, isEffectFunction);
406
- const parentFunction = ast.traverseUp(node, ast.isFunction);
407
- if (tools.O.isNone(effectFunction) || tools.O.isNone(parentFunction)) return;
408
- if (parentFunction.value !== effectFunction.value) return;
409
- const callScope = context.sourceCode.getScope(node);
410
- const name2 = $(node.callee).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({ type: ast.NodeType.MemberExpression }, (n2) => {
411
- if (!("name" in n2.object)) return tools.O.none();
412
- const initialScope = context.sourceCode.getScope(n2);
413
- const property = astUtils.getStaticValue(n2.property, initialScope);
414
- if (property?.value === 1) return tools.O.fromNullable(n2.object.name);
415
- return tools.O.none();
416
- }).with({ type: ast.NodeType.CallExpression }, (n2) => {
417
- if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
418
- if (!("name" in n2.callee.object)) return tools.O.none();
419
- const isAt = $(n2.callee).with(
420
- {
421
- type: ast.NodeType.MemberExpression,
422
- property: {
423
- type: ast.NodeType.Identifier,
424
- name: "at"
425
- }
426
- },
427
- tools.F.constTrue
428
- ).otherwise(tools.F.constFalse);
429
- if (!isAt) return tools.O.none();
430
- const [index] = n2.arguments;
431
- if (!index) return tools.O.none();
432
- const initialScope = context.sourceCode.getScope(n2);
433
- const value = astUtils.getStaticValue(index, initialScope);
434
- if (value?.value === 1) return tools.O.fromNullable(n2.callee.object.name);
435
- return tools.O.none();
436
- }).otherwise(tools.O.none);
437
- tools.F.pipe(
438
- name2,
439
- tools.O.flatMap(_var.findVariable(callScope)),
440
- tools.O.flatMap(_var.getVariableInit(0)),
441
- tools.O.filter(ast.is(ast.NodeType.CallExpression)),
442
- tools.O.filter((name3) => isUseStateCallWithAlias(name3, context)),
443
- tools.O.map((name3) => ({
444
- data: {
445
- setState: name3
446
- },
447
- messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
448
- node
449
- })),
450
- tools.O.map(context.report)
451
- );
492
+ tools.MutList.append(callStack, node);
493
+ const effectFn = tools.MutRef.get(effectFunctionRef);
494
+ const [parentFn, parentFnKind] = tools.MutList.tail(functionStack) ?? [];
495
+ if (parentFn?.async) return;
496
+ const callKind = getCallKind(node);
497
+ $(callKind).with("setState", () => {
498
+ if (!parentFn) return;
499
+ if (parentFn !== effectFn && parentFnKind !== "immediate") {
500
+ indirectSetStateCalls.set(parentFn, [...indirectSetStateCalls.get(parentFn) ?? [], node]);
501
+ return;
502
+ }
503
+ context.report({
504
+ node,
505
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT"
506
+ });
507
+ }).with("useEffect", () => {
508
+ onUseEffectCallEnter(node);
509
+ }).with("useState", () => {
510
+ }).with("then", () => {
511
+ }).with("other", () => {
512
+ indirectFunctionCalls.push(node);
513
+ }).exhaustive();
514
+ },
515
+ ":function"(node) {
516
+ const functionKind = getFunctionKind(node);
517
+ tools.MutList.append(functionStack, [node, functionKind]);
518
+ $(functionKind).with("effect", () => {
519
+ tools.MutRef.set(effectFunctionRef, node);
520
+ }).with("cleanup", () => {
521
+ tools.MutRef.set(cleanUpFunctionRef, node);
522
+ }).otherwise(() => {
523
+ });
524
+ },
525
+ ":function:exit"(node) {
526
+ const effectFn = tools.MutRef.get(effectFunctionRef);
527
+ if (effectFn === node) {
528
+ tools.MutRef.set(effectFunctionRef, null);
529
+ }
530
+ tools.MutList.pop(functionStack);
531
+ },
532
+ "CallExpression:exit"(node) {
533
+ tools.MutList.pop(callStack);
534
+ if (tools.MutRef.get(useEffectCallRef) === node) {
535
+ onUseEffectCallExit();
536
+ tools.MutRef.set(useEffectCallRef, null);
537
+ }
538
+ },
539
+ "Program:exit"() {
540
+ for (const call of indirectFunctionCalls) {
541
+ if (!("name" in call.callee)) continue;
542
+ const { name: name2 } = call.callee;
543
+ const setStateCalls = tools.F.pipe(
544
+ _var.findVariable(name2, context.sourceCode.getScope(call)),
545
+ tools.O.flatMap(_var.getVariableInit(0)),
546
+ tools.O.filter(ast.isFunction),
547
+ tools.O.flatMapNullable((init) => indirectSetStateCalls.get(init)),
548
+ tools.O.getOrElse(() => [])
549
+ );
550
+ for (const setStateCall of setStateCalls) {
551
+ context.report({
552
+ data: {
553
+ name: name2
554
+ },
555
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
556
+ node: setStateCall
557
+ });
558
+ }
559
+ }
452
560
  }
453
561
  };
454
562
  },
@@ -470,64 +578,159 @@ var no_direct_set_state_in_use_layout_effect_default = createRule({
470
578
  create(context) {
471
579
  const settings = shared.getESLintReactSettings(context.settings);
472
580
  const { useLayoutEffect: useLayoutEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
473
- function isUseLayoutEffectCallWithAlias(node, context2) {
474
- return core.isUseLayoutEffectCall(node, context2) || useLayoutEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
581
+ function isUseLayoutEffectCallWithAlias(node) {
582
+ return core.isUseLayoutEffectCall(node, context) || useLayoutEffectAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
475
583
  }
476
- function isUseStateCallWithAlias(node, context2) {
477
- return core.isUseStateCall(node, context2) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
584
+ function isUseStateCallWithAlias(node) {
585
+ return core.isUseStateCall(node, context) || useStateAlias.some(tools.F.flip(core.isReactHookCallWithNameLoose)(node));
478
586
  }
479
587
  function isEffectFunction(node) {
480
- return node.parent?.type === ast.NodeType.CallExpression && isUseLayoutEffectCallWithAlias(node.parent, context);
588
+ return node.parent?.type === ast.NodeType.CallExpression && node.parent.callee !== node && isUseLayoutEffectCallWithAlias(node.parent);
589
+ }
590
+ function isCleanUpFunction(_2) {
591
+ }
592
+ function isFromUseStateCall(id) {
593
+ return tools.F.pipe(
594
+ $(id).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({
595
+ type: ast.NodeType.MemberExpression,
596
+ object: { type: ast.NodeType.Identifier }
597
+ }, (n2) => tools.O.some(n2.object.name)).otherwise(tools.O.none),
598
+ tools.O.flatMap(_var.findVariable(context.sourceCode.getScope(id))),
599
+ tools.O.flatMap(_var.getVariableInit(0)),
600
+ tools.O.filter(ast.is(ast.NodeType.CallExpression)),
601
+ tools.O.exists(isUseStateCallWithAlias)
602
+ );
603
+ }
604
+ function isSetStateCall(node) {
605
+ const id = $(node.callee).with({ type: ast.NodeType.Identifier }, tools.O.some).with({ type: ast.NodeType.MemberExpression }, (n2) => {
606
+ if (!("name" in n2.object)) return tools.O.none();
607
+ const initialScope = context.sourceCode.getScope(n2);
608
+ const property = astUtils.getStaticValue(n2.property, initialScope);
609
+ if (property?.value === 1) return tools.O.fromNullable(n2.object);
610
+ return tools.O.none();
611
+ }).with({ type: ast.NodeType.CallExpression }, (n2) => {
612
+ if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
613
+ if (!("name" in n2.callee.object)) return tools.O.none();
614
+ const isAt = a({
615
+ type: ast.NodeType.MemberExpression,
616
+ property: {
617
+ type: ast.NodeType.Identifier,
618
+ name: "at"
619
+ }
620
+ }, n2.callee);
621
+ const [index] = n2.arguments;
622
+ if (!isAt || !index) return tools.O.none();
623
+ const initialScope = context.sourceCode.getScope(n2);
624
+ const value = astUtils.getStaticValue(index, initialScope);
625
+ if (value?.value === 1) return tools.O.fromNullable(n2.callee.object);
626
+ return tools.O.none();
627
+ }).otherwise(tools.O.none);
628
+ return tools.O.exists(id, isFromUseStateCall);
629
+ }
630
+ function isThenCall(node) {
631
+ if (node.callee.type !== ast.NodeType.MemberExpression) return false;
632
+ return a({
633
+ type: ast.NodeType.Identifier,
634
+ name: "then"
635
+ }, node.callee.property);
481
636
  }
637
+ function getCallKind(node) {
638
+ return $(node).when(isThenCall, () => "then").when(isSetStateCall, () => "setState").when(isUseStateCallWithAlias, () => "useState").when(isUseLayoutEffectCallWithAlias, () => "useLayoutEffect").otherwise(() => "other");
639
+ }
640
+ function getFunctionKind(node) {
641
+ return $(node).when(isEffectFunction, () => "effect").when(isCleanUpFunction, () => "cleanup").when(ast.isIIFE, () => "immediate").otherwise(() => "other");
642
+ }
643
+ const useLayoutEffectCallRef = tools.MutRef.make(null);
644
+ const callStack = tools.MutList.make();
645
+ const functionStack = tools.MutList.make();
646
+ const effectFunctionRef = tools.MutRef.make(null);
647
+ const cleanUpFunctionRef = tools.MutRef.make(null);
648
+ const indirectFunctionCalls = [];
649
+ const indirectSetStateCalls = /* @__PURE__ */ new Map();
650
+ const onUseLayoutEffectCallEnter = (node) => void tools.MutRef.set(useLayoutEffectCallRef, node);
651
+ const onUseLayoutEffectCallExit = () => void tools.MutRef.set(useLayoutEffectCallRef, null);
482
652
  return {
653
+ Identifier(node) {
654
+ const isInUseLayoutEffectCall = tools.MutRef.get(useLayoutEffectCallRef) !== null;
655
+ const parentFn = tools.MutList.tail(functionStack)?.[0];
656
+ const useLayoutEffectCall = tools.MutRef.get(useLayoutEffectCallRef);
657
+ const isEffectFunction2 = useLayoutEffectCall && tools.O.isSome(ast.traverseUp(useLayoutEffectCall, (n2) => n2 === parentFn));
658
+ if (isFromUseStateCall(node) && isInUseLayoutEffectCall && isEffectFunction2) {
659
+ context.report({
660
+ node,
661
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT"
662
+ });
663
+ }
664
+ },
483
665
  CallExpression(node) {
484
- const effectFunction = ast.traverseUp(node, isEffectFunction);
485
- const parentFunction = ast.traverseUp(node, ast.isFunction);
486
- if (tools.O.isNone(effectFunction) || tools.O.isNone(parentFunction)) return;
487
- if (parentFunction.value !== effectFunction.value) return;
488
- const callScope = context.sourceCode.getScope(node);
489
- const name2 = $(node.callee).with({ type: ast.NodeType.Identifier }, (n2) => tools.O.some(n2.name)).with({ type: ast.NodeType.MemberExpression }, (n2) => {
490
- if (!("name" in n2.object)) return tools.O.none();
491
- const initialScope = context.sourceCode.getScope(n2);
492
- const property = astUtils.getStaticValue(n2.property, initialScope);
493
- if (property?.value === 1) return tools.O.fromNullable(n2.object.name);
494
- return tools.O.none();
495
- }).with({ type: ast.NodeType.CallExpression }, (n2) => {
496
- if (!ast.is(ast.NodeType.MemberExpression)(n2.callee)) return tools.O.none();
497
- if (!("name" in n2.callee.object)) return tools.O.none();
498
- const isAt = $(n2.callee).with(
499
- {
500
- type: ast.NodeType.MemberExpression,
501
- property: {
502
- type: ast.NodeType.Identifier,
503
- name: "at"
504
- }
505
- },
506
- tools.F.constTrue
507
- ).otherwise(tools.F.constFalse);
508
- if (!isAt) return tools.O.none();
509
- const [index] = n2.arguments;
510
- if (!index) return tools.O.none();
511
- const initialScope = context.sourceCode.getScope(n2);
512
- const value = astUtils.getStaticValue(index, initialScope);
513
- if (value?.value === 1) return tools.O.fromNullable(n2.callee.object.name);
514
- return tools.O.none();
515
- }).otherwise(tools.O.none);
516
- tools.F.pipe(
517
- name2,
518
- tools.O.flatMap(_var.findVariable(callScope)),
519
- tools.O.flatMap(_var.getVariableInit(0)),
520
- tools.O.filter(ast.is(ast.NodeType.CallExpression)),
521
- tools.O.filter((name3) => isUseStateCallWithAlias(name3, context)),
522
- tools.O.map((name3) => ({
523
- data: {
524
- setState: name3
525
- },
526
- messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
527
- node
528
- })),
529
- tools.O.map(context.report)
530
- );
666
+ tools.MutList.append(callStack, node);
667
+ const effectFn = tools.MutRef.get(effectFunctionRef);
668
+ const [parentFn, parentFnKind] = tools.MutList.tail(functionStack) ?? [];
669
+ if (parentFn?.async) return;
670
+ const callKind = getCallKind(node);
671
+ $(callKind).with("setState", () => {
672
+ if (!parentFn) return;
673
+ if (parentFn !== effectFn && parentFnKind !== "immediate") {
674
+ indirectSetStateCalls.set(parentFn, [...indirectSetStateCalls.get(parentFn) ?? [], node]);
675
+ return;
676
+ }
677
+ context.report({
678
+ node,
679
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT"
680
+ });
681
+ }).with("useLayoutEffect", () => {
682
+ onUseLayoutEffectCallEnter(node);
683
+ }).with("useState", () => {
684
+ }).with("then", () => {
685
+ }).with("other", () => {
686
+ indirectFunctionCalls.push(node);
687
+ }).exhaustive();
688
+ },
689
+ ":function"(node) {
690
+ const functionKind = getFunctionKind(node);
691
+ tools.MutList.append(functionStack, [node, functionKind]);
692
+ $(functionKind).with("effect", () => {
693
+ tools.MutRef.set(effectFunctionRef, node);
694
+ }).with("cleanup", () => {
695
+ tools.MutRef.set(cleanUpFunctionRef, node);
696
+ }).otherwise(() => {
697
+ });
698
+ },
699
+ ":function:exit"(node) {
700
+ const effectFn = tools.MutRef.get(effectFunctionRef);
701
+ if (effectFn === node) {
702
+ tools.MutRef.set(effectFunctionRef, null);
703
+ }
704
+ tools.MutList.pop(functionStack);
705
+ },
706
+ "CallExpression:exit"(node) {
707
+ tools.MutList.pop(callStack);
708
+ if (tools.MutRef.get(useLayoutEffectCallRef) === node) {
709
+ onUseLayoutEffectCallExit();
710
+ tools.MutRef.set(useLayoutEffectCallRef, null);
711
+ }
712
+ },
713
+ "Program:exit"() {
714
+ for (const call of indirectFunctionCalls) {
715
+ if (!("name" in call.callee)) continue;
716
+ const { name: name2 } = call.callee;
717
+ const setStateCalls = tools.F.pipe(
718
+ _var.findVariable(name2, context.sourceCode.getScope(call)),
719
+ tools.O.flatMap(_var.getVariableInit(0)),
720
+ tools.O.filter(ast.isFunction),
721
+ tools.O.flatMapNullable((init) => indirectSetStateCalls.get(init)),
722
+ tools.O.getOrElse(() => [])
723
+ );
724
+ for (const setStateCall of setStateCalls) {
725
+ context.report({
726
+ data: {
727
+ name: name2
728
+ },
729
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
730
+ node: setStateCall
731
+ });
732
+ }
733
+ }
531
734
  }
532
735
  };
533
736
  },
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
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 { isFunction, NodeType, is, traverseUp, getNestedCallExpressions } from '@eslint-react/ast';
4
- import { F, O } from '@eslint-react/tools';
3
+ import { isFunction, NodeType, is, traverseUp, getNestedCallExpressions, isIIFE } from '@eslint-react/ast';
4
+ import { F, O, MutRef, MutList } 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.28-beta.0";
10
+ var version = "1.5.28-beta.2";
11
11
  var createRule = createRuleForPlugin("hooks-extra");
12
12
 
13
13
  // src/rules/ensure-custom-hooks-using-other-hooks.ts
@@ -61,32 +61,45 @@ var o = (n2, s2, c2) => {
61
61
  if (!r(s2)) return false;
62
62
  if (Array.isArray(n2)) {
63
63
  if (!Array.isArray(s2)) return false;
64
- let t2 = [], r2 = [], a = [];
64
+ let t2 = [], r2 = [], a2 = [];
65
65
  for (const o2 of n2.keys()) {
66
66
  const s3 = n2[o2];
67
- i(s3) && s3[e] ? a.push(s3) : a.length ? r2.push(s3) : t2.push(s3);
67
+ i(s3) && s3[e] ? a2.push(s3) : a2.length ? r2.push(s3) : t2.push(s3);
68
68
  }
69
- if (a.length) {
70
- if (a.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
69
+ if (a2.length) {
70
+ if (a2.length > 1) throw new Error("Pattern error: Using `...P.array(...)` several times in a single pattern is not allowed.");
71
71
  if (s2.length < t2.length + r2.length) return false;
72
72
  const e2 = s2.slice(0, t2.length), n3 = 0 === r2.length ? [] : s2.slice(-r2.length), i2 = s2.slice(t2.length, 0 === r2.length ? Infinity : -r2.length);
73
- return t2.every((t3, n4) => o(t3, e2[n4], c2)) && r2.every((t3, e3) => o(t3, n3[e3], c2)) && (0 === a.length || o(a[0], i2, c2));
73
+ return t2.every((t3, n4) => o(t3, e2[n4], c2)) && r2.every((t3, e3) => o(t3, n3[e3], c2)) && (0 === a2.length || o(a2[0], i2, c2));
74
74
  }
75
75
  return n2.length === s2.length && n2.every((t3, e2) => o(t3, s2[e2], c2));
76
76
  }
77
77
  return Object.keys(n2).every((e2) => {
78
78
  const r2 = n2[e2];
79
- return (e2 in s2 || i(a = r2) && "optional" === a[t]().matcherType) && o(r2, s2[e2], c2);
80
- var a;
79
+ return (e2 in s2 || i(a2 = r2) && "optional" === a2[t]().matcherType) && o(r2, s2[e2], c2);
80
+ var a2;
81
81
  });
82
82
  }
83
83
  return Object.is(s2, n2);
84
84
  };
85
85
  var s = (e2) => {
86
- var n2, o2, a;
87
- return r(e2) ? i(e2) ? null != (n2 = null == (o2 = (a = e2[t]()).getSelectionKeys) ? void 0 : o2.call(a)) ? n2 : [] : Array.isArray(e2) ? c(e2, s) : c(Object.values(e2), s) : [];
86
+ var n2, o2, a2;
87
+ return r(e2) ? i(e2) ? null != (n2 = null == (o2 = (a2 = e2[t]()).getSelectionKeys) ? void 0 : o2.call(a2)) ? n2 : [] : Array.isArray(e2) ? c(e2, s) : c(Object.values(e2), s) : [];
88
88
  };
89
89
  var c = (t2, e2) => t2.reduce((t3, n2) => t3.concat(e2(n2)), []);
90
+ function a(...t2) {
91
+ if (1 === t2.length) {
92
+ const [e2] = t2;
93
+ return (t3) => o(e2, t3, () => {
94
+ });
95
+ }
96
+ if (2 === t2.length) {
97
+ const [e2, n2] = t2;
98
+ return o(e2, n2, () => {
99
+ });
100
+ }
101
+ throw new Error(`isMatching wasn't given the right number of arguments: expected 1 or 2, received ${t2.length}.`);
102
+ }
90
103
  function u(t2) {
91
104
  return Object.assign(t2, { optional: () => l(t2), and: (e2) => m(t2, e2), or: (e2) => d(t2, e2), select: (e2) => void 0 === e2 ? p(t2) : p(e2, t2) });
92
105
  }
@@ -185,9 +198,9 @@ var z = class _z {
185
198
  let i2;
186
199
  3 === t2.length && "function" == typeof t2[1] ? i2 = t2[1] : t2.length > 2 && r2.push(...t2.slice(1, t2.length - 1));
187
200
  let s2 = false, c2 = {};
188
- const a = (t3, e3) => {
201
+ const a2 = (t3, e3) => {
189
202
  s2 = true, c2[t3] = e3;
190
- }, u2 = !r2.some((t3) => o(t3, this.input, a)) || i2 && !Boolean(i2(this.input)) ? W : { matched: true, value: e2(s2 ? n in c2 ? c2[n] : c2 : this.input, this.input) };
203
+ }, u2 = !r2.some((t3) => o(t3, this.input, a2)) || i2 && !Boolean(i2(this.input)) ? W : { matched: true, value: e2(s2 ? n in c2 ? c2[n] : c2 : this.input, this.input) };
191
204
  return new _z(this.input, u2);
192
205
  }
193
206
  when(t2, e2) {
@@ -389,64 +402,159 @@ var no_direct_set_state_in_use_effect_default = createRule({
389
402
  create(context) {
390
403
  const settings = getESLintReactSettings(context.settings);
391
404
  const { useEffect: useEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
392
- function isUseEffectCallWithAlias(node, context2) {
393
- return isUseEffectCall(node, context2) || useEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
405
+ function isUseEffectCallWithAlias(node) {
406
+ return isUseEffectCall(node, context) || useEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
394
407
  }
395
- function isUseStateCallWithAlias(node, context2) {
396
- return isUseStateCall(node, context2) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
408
+ function isUseStateCallWithAlias(node) {
409
+ return isUseStateCall(node, context) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
397
410
  }
398
411
  function isEffectFunction(node) {
399
- return node.parent?.type === NodeType.CallExpression && isUseEffectCallWithAlias(node.parent, context);
412
+ return node.parent?.type === NodeType.CallExpression && node.parent.callee !== node && isUseEffectCallWithAlias(node.parent);
413
+ }
414
+ function isCleanUpFunction(_2) {
400
415
  }
416
+ function isFromUseStateCall(id) {
417
+ return F.pipe(
418
+ $(id).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({
419
+ type: NodeType.MemberExpression,
420
+ object: { type: NodeType.Identifier }
421
+ }, (n2) => O.some(n2.object.name)).otherwise(O.none),
422
+ O.flatMap(findVariable(context.sourceCode.getScope(id))),
423
+ O.flatMap(getVariableInit(0)),
424
+ O.filter(is(NodeType.CallExpression)),
425
+ O.exists(isUseStateCallWithAlias)
426
+ );
427
+ }
428
+ function isSetStateCall(node) {
429
+ const id = $(node.callee).with({ type: NodeType.Identifier }, O.some).with({ type: NodeType.MemberExpression }, (n2) => {
430
+ if (!("name" in n2.object)) return O.none();
431
+ const initialScope = context.sourceCode.getScope(n2);
432
+ const property = getStaticValue(n2.property, initialScope);
433
+ if (property?.value === 1) return O.fromNullable(n2.object);
434
+ return O.none();
435
+ }).with({ type: NodeType.CallExpression }, (n2) => {
436
+ if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
437
+ if (!("name" in n2.callee.object)) return O.none();
438
+ const isAt = a({
439
+ type: NodeType.MemberExpression,
440
+ property: {
441
+ type: NodeType.Identifier,
442
+ name: "at"
443
+ }
444
+ }, n2.callee);
445
+ const [index] = n2.arguments;
446
+ if (!isAt || !index) return O.none();
447
+ const initialScope = context.sourceCode.getScope(n2);
448
+ const value = getStaticValue(index, initialScope);
449
+ if (value?.value === 1) return O.fromNullable(n2.callee.object);
450
+ return O.none();
451
+ }).otherwise(O.none);
452
+ return O.exists(id, isFromUseStateCall);
453
+ }
454
+ function isThenCall(node) {
455
+ if (node.callee.type !== NodeType.MemberExpression) return false;
456
+ return a({
457
+ type: NodeType.Identifier,
458
+ name: "then"
459
+ }, node.callee.property);
460
+ }
461
+ function getCallKind(node) {
462
+ return $(node).when(isThenCall, () => "then").when(isSetStateCall, () => "setState").when(isUseStateCallWithAlias, () => "useState").when(isUseEffectCallWithAlias, () => "useEffect").otherwise(() => "other");
463
+ }
464
+ function getFunctionKind(node) {
465
+ return $(node).when(isEffectFunction, () => "effect").when(isCleanUpFunction, () => "cleanup").when(isIIFE, () => "immediate").otherwise(() => "other");
466
+ }
467
+ const useEffectCallRef = MutRef.make(null);
468
+ const callStack = MutList.make();
469
+ const functionStack = MutList.make();
470
+ const effectFunctionRef = MutRef.make(null);
471
+ const cleanUpFunctionRef = MutRef.make(null);
472
+ const indirectFunctionCalls = [];
473
+ const indirectSetStateCalls = /* @__PURE__ */ new Map();
474
+ const onUseEffectCallEnter = (node) => void MutRef.set(useEffectCallRef, node);
475
+ const onUseEffectCallExit = () => void MutRef.set(useEffectCallRef, null);
401
476
  return {
477
+ Identifier(node) {
478
+ const isInUseEffectCall = MutRef.get(useEffectCallRef) !== null;
479
+ const parentFn = MutList.tail(functionStack)?.[0];
480
+ const useEffectCall = MutRef.get(useEffectCallRef);
481
+ const isEffectFunction2 = useEffectCall && O.isSome(traverseUp(useEffectCall, (n2) => n2 === parentFn));
482
+ if (isFromUseStateCall(node) && isInUseEffectCall && isEffectFunction2) {
483
+ context.report({
484
+ node,
485
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT"
486
+ });
487
+ }
488
+ },
402
489
  CallExpression(node) {
403
- const effectFunction = traverseUp(node, isEffectFunction);
404
- const parentFunction = traverseUp(node, isFunction);
405
- if (O.isNone(effectFunction) || O.isNone(parentFunction)) return;
406
- if (parentFunction.value !== effectFunction.value) return;
407
- const callScope = context.sourceCode.getScope(node);
408
- const name2 = $(node.callee).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({ type: NodeType.MemberExpression }, (n2) => {
409
- if (!("name" in n2.object)) return O.none();
410
- const initialScope = context.sourceCode.getScope(n2);
411
- const property = getStaticValue(n2.property, initialScope);
412
- if (property?.value === 1) return O.fromNullable(n2.object.name);
413
- return O.none();
414
- }).with({ type: NodeType.CallExpression }, (n2) => {
415
- if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
416
- if (!("name" in n2.callee.object)) return O.none();
417
- const isAt = $(n2.callee).with(
418
- {
419
- type: NodeType.MemberExpression,
420
- property: {
421
- type: NodeType.Identifier,
422
- name: "at"
423
- }
424
- },
425
- F.constTrue
426
- ).otherwise(F.constFalse);
427
- if (!isAt) return O.none();
428
- const [index] = n2.arguments;
429
- if (!index) return O.none();
430
- const initialScope = context.sourceCode.getScope(n2);
431
- const value = getStaticValue(index, initialScope);
432
- if (value?.value === 1) return O.fromNullable(n2.callee.object.name);
433
- return O.none();
434
- }).otherwise(O.none);
435
- F.pipe(
436
- name2,
437
- O.flatMap(findVariable(callScope)),
438
- O.flatMap(getVariableInit(0)),
439
- O.filter(is(NodeType.CallExpression)),
440
- O.filter((name3) => isUseStateCallWithAlias(name3, context)),
441
- O.map((name3) => ({
442
- data: {
443
- setState: name3
444
- },
445
- messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
446
- node
447
- })),
448
- O.map(context.report)
449
- );
490
+ MutList.append(callStack, node);
491
+ const effectFn = MutRef.get(effectFunctionRef);
492
+ const [parentFn, parentFnKind] = MutList.tail(functionStack) ?? [];
493
+ if (parentFn?.async) return;
494
+ const callKind = getCallKind(node);
495
+ $(callKind).with("setState", () => {
496
+ if (!parentFn) return;
497
+ if (parentFn !== effectFn && parentFnKind !== "immediate") {
498
+ indirectSetStateCalls.set(parentFn, [...indirectSetStateCalls.get(parentFn) ?? [], node]);
499
+ return;
500
+ }
501
+ context.report({
502
+ node,
503
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT"
504
+ });
505
+ }).with("useEffect", () => {
506
+ onUseEffectCallEnter(node);
507
+ }).with("useState", () => {
508
+ }).with("then", () => {
509
+ }).with("other", () => {
510
+ indirectFunctionCalls.push(node);
511
+ }).exhaustive();
512
+ },
513
+ ":function"(node) {
514
+ const functionKind = getFunctionKind(node);
515
+ MutList.append(functionStack, [node, functionKind]);
516
+ $(functionKind).with("effect", () => {
517
+ MutRef.set(effectFunctionRef, node);
518
+ }).with("cleanup", () => {
519
+ MutRef.set(cleanUpFunctionRef, node);
520
+ }).otherwise(() => {
521
+ });
522
+ },
523
+ ":function:exit"(node) {
524
+ const effectFn = MutRef.get(effectFunctionRef);
525
+ if (effectFn === node) {
526
+ MutRef.set(effectFunctionRef, null);
527
+ }
528
+ MutList.pop(functionStack);
529
+ },
530
+ "CallExpression:exit"(node) {
531
+ MutList.pop(callStack);
532
+ if (MutRef.get(useEffectCallRef) === node) {
533
+ onUseEffectCallExit();
534
+ MutRef.set(useEffectCallRef, null);
535
+ }
536
+ },
537
+ "Program:exit"() {
538
+ for (const call of indirectFunctionCalls) {
539
+ if (!("name" in call.callee)) continue;
540
+ const { name: name2 } = call.callee;
541
+ const setStateCalls = F.pipe(
542
+ findVariable(name2, context.sourceCode.getScope(call)),
543
+ O.flatMap(getVariableInit(0)),
544
+ O.filter(isFunction),
545
+ O.flatMapNullable((init) => indirectSetStateCalls.get(init)),
546
+ O.getOrElse(() => [])
547
+ );
548
+ for (const setStateCall of setStateCalls) {
549
+ context.report({
550
+ data: {
551
+ name: name2
552
+ },
553
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_EFFECT",
554
+ node: setStateCall
555
+ });
556
+ }
557
+ }
450
558
  }
451
559
  };
452
560
  },
@@ -468,64 +576,159 @@ var no_direct_set_state_in_use_layout_effect_default = createRule({
468
576
  create(context) {
469
577
  const settings = getESLintReactSettings(context.settings);
470
578
  const { useLayoutEffect: useLayoutEffectAlias = [], useState: useStateAlias = [] } = settings.additionalHooks ?? {};
471
- function isUseLayoutEffectCallWithAlias(node, context2) {
472
- return isUseLayoutEffectCall(node, context2) || useLayoutEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
579
+ function isUseLayoutEffectCallWithAlias(node) {
580
+ return isUseLayoutEffectCall(node, context) || useLayoutEffectAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
473
581
  }
474
- function isUseStateCallWithAlias(node, context2) {
475
- return isUseStateCall(node, context2) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
582
+ function isUseStateCallWithAlias(node) {
583
+ return isUseStateCall(node, context) || useStateAlias.some(F.flip(isReactHookCallWithNameLoose)(node));
476
584
  }
477
585
  function isEffectFunction(node) {
478
- return node.parent?.type === NodeType.CallExpression && isUseLayoutEffectCallWithAlias(node.parent, context);
586
+ return node.parent?.type === NodeType.CallExpression && node.parent.callee !== node && isUseLayoutEffectCallWithAlias(node.parent);
587
+ }
588
+ function isCleanUpFunction(_2) {
589
+ }
590
+ function isFromUseStateCall(id) {
591
+ return F.pipe(
592
+ $(id).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({
593
+ type: NodeType.MemberExpression,
594
+ object: { type: NodeType.Identifier }
595
+ }, (n2) => O.some(n2.object.name)).otherwise(O.none),
596
+ O.flatMap(findVariable(context.sourceCode.getScope(id))),
597
+ O.flatMap(getVariableInit(0)),
598
+ O.filter(is(NodeType.CallExpression)),
599
+ O.exists(isUseStateCallWithAlias)
600
+ );
601
+ }
602
+ function isSetStateCall(node) {
603
+ const id = $(node.callee).with({ type: NodeType.Identifier }, O.some).with({ type: NodeType.MemberExpression }, (n2) => {
604
+ if (!("name" in n2.object)) return O.none();
605
+ const initialScope = context.sourceCode.getScope(n2);
606
+ const property = getStaticValue(n2.property, initialScope);
607
+ if (property?.value === 1) return O.fromNullable(n2.object);
608
+ return O.none();
609
+ }).with({ type: NodeType.CallExpression }, (n2) => {
610
+ if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
611
+ if (!("name" in n2.callee.object)) return O.none();
612
+ const isAt = a({
613
+ type: NodeType.MemberExpression,
614
+ property: {
615
+ type: NodeType.Identifier,
616
+ name: "at"
617
+ }
618
+ }, n2.callee);
619
+ const [index] = n2.arguments;
620
+ if (!isAt || !index) return O.none();
621
+ const initialScope = context.sourceCode.getScope(n2);
622
+ const value = getStaticValue(index, initialScope);
623
+ if (value?.value === 1) return O.fromNullable(n2.callee.object);
624
+ return O.none();
625
+ }).otherwise(O.none);
626
+ return O.exists(id, isFromUseStateCall);
627
+ }
628
+ function isThenCall(node) {
629
+ if (node.callee.type !== NodeType.MemberExpression) return false;
630
+ return a({
631
+ type: NodeType.Identifier,
632
+ name: "then"
633
+ }, node.callee.property);
479
634
  }
635
+ function getCallKind(node) {
636
+ return $(node).when(isThenCall, () => "then").when(isSetStateCall, () => "setState").when(isUseStateCallWithAlias, () => "useState").when(isUseLayoutEffectCallWithAlias, () => "useLayoutEffect").otherwise(() => "other");
637
+ }
638
+ function getFunctionKind(node) {
639
+ return $(node).when(isEffectFunction, () => "effect").when(isCleanUpFunction, () => "cleanup").when(isIIFE, () => "immediate").otherwise(() => "other");
640
+ }
641
+ const useLayoutEffectCallRef = MutRef.make(null);
642
+ const callStack = MutList.make();
643
+ const functionStack = MutList.make();
644
+ const effectFunctionRef = MutRef.make(null);
645
+ const cleanUpFunctionRef = MutRef.make(null);
646
+ const indirectFunctionCalls = [];
647
+ const indirectSetStateCalls = /* @__PURE__ */ new Map();
648
+ const onUseLayoutEffectCallEnter = (node) => void MutRef.set(useLayoutEffectCallRef, node);
649
+ const onUseLayoutEffectCallExit = () => void MutRef.set(useLayoutEffectCallRef, null);
480
650
  return {
651
+ Identifier(node) {
652
+ const isInUseLayoutEffectCall = MutRef.get(useLayoutEffectCallRef) !== null;
653
+ const parentFn = MutList.tail(functionStack)?.[0];
654
+ const useLayoutEffectCall = MutRef.get(useLayoutEffectCallRef);
655
+ const isEffectFunction2 = useLayoutEffectCall && O.isSome(traverseUp(useLayoutEffectCall, (n2) => n2 === parentFn));
656
+ if (isFromUseStateCall(node) && isInUseLayoutEffectCall && isEffectFunction2) {
657
+ context.report({
658
+ node,
659
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT"
660
+ });
661
+ }
662
+ },
481
663
  CallExpression(node) {
482
- const effectFunction = traverseUp(node, isEffectFunction);
483
- const parentFunction = traverseUp(node, isFunction);
484
- if (O.isNone(effectFunction) || O.isNone(parentFunction)) return;
485
- if (parentFunction.value !== effectFunction.value) return;
486
- const callScope = context.sourceCode.getScope(node);
487
- const name2 = $(node.callee).with({ type: NodeType.Identifier }, (n2) => O.some(n2.name)).with({ type: NodeType.MemberExpression }, (n2) => {
488
- if (!("name" in n2.object)) return O.none();
489
- const initialScope = context.sourceCode.getScope(n2);
490
- const property = getStaticValue(n2.property, initialScope);
491
- if (property?.value === 1) return O.fromNullable(n2.object.name);
492
- return O.none();
493
- }).with({ type: NodeType.CallExpression }, (n2) => {
494
- if (!is(NodeType.MemberExpression)(n2.callee)) return O.none();
495
- if (!("name" in n2.callee.object)) return O.none();
496
- const isAt = $(n2.callee).with(
497
- {
498
- type: NodeType.MemberExpression,
499
- property: {
500
- type: NodeType.Identifier,
501
- name: "at"
502
- }
503
- },
504
- F.constTrue
505
- ).otherwise(F.constFalse);
506
- if (!isAt) return O.none();
507
- const [index] = n2.arguments;
508
- if (!index) return O.none();
509
- const initialScope = context.sourceCode.getScope(n2);
510
- const value = getStaticValue(index, initialScope);
511
- if (value?.value === 1) return O.fromNullable(n2.callee.object.name);
512
- return O.none();
513
- }).otherwise(O.none);
514
- F.pipe(
515
- name2,
516
- O.flatMap(findVariable(callScope)),
517
- O.flatMap(getVariableInit(0)),
518
- O.filter(is(NodeType.CallExpression)),
519
- O.filter((name3) => isUseStateCallWithAlias(name3, context)),
520
- O.map((name3) => ({
521
- data: {
522
- setState: name3
523
- },
524
- messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
525
- node
526
- })),
527
- O.map(context.report)
528
- );
664
+ MutList.append(callStack, node);
665
+ const effectFn = MutRef.get(effectFunctionRef);
666
+ const [parentFn, parentFnKind] = MutList.tail(functionStack) ?? [];
667
+ if (parentFn?.async) return;
668
+ const callKind = getCallKind(node);
669
+ $(callKind).with("setState", () => {
670
+ if (!parentFn) return;
671
+ if (parentFn !== effectFn && parentFnKind !== "immediate") {
672
+ indirectSetStateCalls.set(parentFn, [...indirectSetStateCalls.get(parentFn) ?? [], node]);
673
+ return;
674
+ }
675
+ context.report({
676
+ node,
677
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT"
678
+ });
679
+ }).with("useLayoutEffect", () => {
680
+ onUseLayoutEffectCallEnter(node);
681
+ }).with("useState", () => {
682
+ }).with("then", () => {
683
+ }).with("other", () => {
684
+ indirectFunctionCalls.push(node);
685
+ }).exhaustive();
686
+ },
687
+ ":function"(node) {
688
+ const functionKind = getFunctionKind(node);
689
+ MutList.append(functionStack, [node, functionKind]);
690
+ $(functionKind).with("effect", () => {
691
+ MutRef.set(effectFunctionRef, node);
692
+ }).with("cleanup", () => {
693
+ MutRef.set(cleanUpFunctionRef, node);
694
+ }).otherwise(() => {
695
+ });
696
+ },
697
+ ":function:exit"(node) {
698
+ const effectFn = MutRef.get(effectFunctionRef);
699
+ if (effectFn === node) {
700
+ MutRef.set(effectFunctionRef, null);
701
+ }
702
+ MutList.pop(functionStack);
703
+ },
704
+ "CallExpression:exit"(node) {
705
+ MutList.pop(callStack);
706
+ if (MutRef.get(useLayoutEffectCallRef) === node) {
707
+ onUseLayoutEffectCallExit();
708
+ MutRef.set(useLayoutEffectCallRef, null);
709
+ }
710
+ },
711
+ "Program:exit"() {
712
+ for (const call of indirectFunctionCalls) {
713
+ if (!("name" in call.callee)) continue;
714
+ const { name: name2 } = call.callee;
715
+ const setStateCalls = F.pipe(
716
+ findVariable(name2, context.sourceCode.getScope(call)),
717
+ O.flatMap(getVariableInit(0)),
718
+ O.filter(isFunction),
719
+ O.flatMapNullable((init) => indirectSetStateCalls.get(init)),
720
+ O.getOrElse(() => [])
721
+ );
722
+ for (const setStateCall of setStateCalls) {
723
+ context.report({
724
+ data: {
725
+ name: name2
726
+ },
727
+ messageId: "NO_DIRECT_SET_STATE_IN_USE_LAYOUT_EFFECT",
728
+ node: setStateCall
729
+ });
730
+ }
731
+ }
529
732
  }
530
733
  };
531
734
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-hooks-extra",
3
- "version": "1.5.28-beta.0",
3
+ "version": "1.5.28-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,17 +39,17 @@
39
39
  "@typescript-eslint/type-utils": "^7.16.1",
40
40
  "@typescript-eslint/types": "^7.16.1",
41
41
  "@typescript-eslint/utils": "^7.16.1",
42
- "@eslint-react/ast": "1.5.28-beta.0",
43
- "@eslint-react/core": "1.5.28-beta.0",
44
- "@eslint-react/jsx": "1.5.28-beta.0",
45
- "@eslint-react/tools": "1.5.28-beta.0",
46
- "@eslint-react/shared": "1.5.28-beta.0",
47
- "@eslint-react/types": "1.5.28-beta.0",
48
- "@eslint-react/var": "1.5.28-beta.0"
42
+ "@eslint-react/ast": "1.5.28-beta.2",
43
+ "@eslint-react/core": "1.5.28-beta.2",
44
+ "@eslint-react/tools": "1.5.28-beta.2",
45
+ "@eslint-react/shared": "1.5.28-beta.2",
46
+ "@eslint-react/types": "1.5.28-beta.2",
47
+ "@eslint-react/var": "1.5.28-beta.2",
48
+ "@eslint-react/jsx": "1.5.28-beta.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "string-ts": "2.2.0",
52
- "tsup": "8.1.1"
52
+ "tsup": "8.2.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": "^8.57.0 || ^9.0.0",