eslint-plugin-react-web-api 5.24.6 → 5.24.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +167 -25
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
2
2
  import * as ESLintUtils from "@typescript-eslint/utils/eslint-utils";
3
3
  import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
4
- import { getFunctionId, isUseEffectCleanupCallback, isUseEffectSetupCallback } from "@eslint-react/core";
4
+ import { getFunctionId, isUseEffectCleanupCallback, isUseEffectSetupCallback, isUseRefLikeCall } from "@eslint-react/core";
5
5
  import "@eslint-react/eslint";
6
6
  import { isAssignmentTargetEqual, isInitializedFromReactNative, isValueEqual, resolve, resolveEnclosingAssignmentTarget, resolveOrigin } from "@eslint-react/var";
7
7
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
@@ -21,7 +21,7 @@ var __exportAll = (all, no_symbols) => {
21
21
  //#endregion
22
22
  //#region package.json
23
23
  var name$1 = "eslint-plugin-react-web-api";
24
- var version = "5.24.6";
24
+ var version = "5.24.8";
25
25
  //#endregion
26
26
  //#region src/utils/create-rule.ts
27
27
  function getDocsUrl(ruleName) {
@@ -131,6 +131,18 @@ dual(2, (self, predicate) => {
131
131
  });
132
132
  //#endregion
133
133
  //#region src/rules/no-leaked-event-listener/lib.ts
134
+ function isSignalPropertyBinding(context, node) {
135
+ const defNode = findVariable(context.sourceCode.getScope(node), node)?.defs.at(-1)?.node;
136
+ if (defNode?.type !== AST_NODE_TYPES.VariableDeclarator || defNode.id.type !== AST_NODE_TYPES.ObjectPattern) return false;
137
+ for (const prop of defNode.id.properties) {
138
+ if (prop.type !== AST_NODE_TYPES.Property || prop.computed) continue;
139
+ const key = Extract.unwrap(prop.key);
140
+ if (key.type !== AST_NODE_TYPES.Identifier || key.name !== "signal") continue;
141
+ const value = Extract.unwrap(prop.value);
142
+ return value.type === AST_NODE_TYPES.Identifier && value.name === node.name;
143
+ }
144
+ return false;
145
+ }
134
146
  function getSignalValueExpression(context, node) {
135
147
  if (node == null) return null;
136
148
  switch (node.type) {
@@ -138,6 +150,10 @@ function getSignalValueExpression(context, node) {
138
150
  const resolved = resolveOrigin(context, node);
139
151
  const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
140
152
  if (unwrapped != null && Check.isFunction(unwrapped)) return node;
153
+ if (unwrapped != null && unwrapped.type === AST_NODE_TYPES.NewExpression && isSignalPropertyBinding(context, node)) {
154
+ const newCallee = Extract.unwrap(unwrapped.callee);
155
+ if (newCallee.type === AST_NODE_TYPES.Identifier && newCallee.name === "AbortController") return node;
156
+ }
141
157
  return getSignalValueExpression(context, unwrapped);
142
158
  }
143
159
  case AST_NODE_TYPES.MemberExpression: return node;
@@ -246,10 +262,15 @@ function create$5(context) {
246
262
  const aEntries = [];
247
263
  const rEntries = [];
248
264
  const cleanupCallees = /* @__PURE__ */ new Map();
265
+ function isGlobalObject(node) {
266
+ return Check.isIdentifier(node) && (node.name === "window" || node.name === "globalThis" || node.name === "self");
267
+ }
249
268
  function isSameObject(a, b) {
250
269
  switch (true) {
251
270
  case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
252
- default: return false;
271
+ case a.type === AST_NODE_TYPES.MemberExpression: return isGlobalObject(a.object);
272
+ case b.type === AST_NODE_TYPES.MemberExpression: return isGlobalObject(b.object);
273
+ default: return true;
253
274
  }
254
275
  }
255
276
  function isInverseEntry(aEntry, rEntry) {
@@ -365,7 +386,7 @@ function getCallKind$4(node) {
365
386
  if (name != null && isMatching(P.union("fetch", "abort"))(name)) return name;
366
387
  return "other";
367
388
  }
368
- function getControllerFromSignal(context, node) {
389
+ function getControllerFromSignal(context, node, seen = /* @__PURE__ */ new Set()) {
369
390
  node = Extract.unwrap(node);
370
391
  switch (node.type) {
371
392
  case AST_NODE_TYPES.MemberExpression: return {
@@ -373,6 +394,11 @@ function getControllerFromSignal(context, node) {
373
394
  isParamSignal: false
374
395
  };
375
396
  case AST_NODE_TYPES.Identifier: {
397
+ if (seen.has(node)) return {
398
+ controller: null,
399
+ isParamSignal: false
400
+ };
401
+ seen.add(node);
376
402
  const resolved = resolveOrigin(context, node);
377
403
  const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
378
404
  if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
@@ -383,6 +409,7 @@ function getControllerFromSignal(context, node) {
383
409
  controller: node,
384
410
  isParamSignal: true
385
411
  };
412
+ if (resolvedUnwrapped != null) return getControllerFromSignal(context, resolvedUnwrapped, seen);
386
413
  return {
387
414
  controller: null,
388
415
  isParamSignal: false
@@ -487,12 +514,28 @@ function isFromObserver$1(context, node, name) {
487
514
  switch (true) {
488
515
  case Check.isIdentifier(node): {
489
516
  const initNode = resolve(context, node);
490
- return isNewObserver$1(initNode == null ? null : Extract.unwrap(initNode), name);
517
+ const unwrapped = initNode == null ? null : Extract.unwrap(initNode);
518
+ if (unwrapped == null) return false;
519
+ if (isNewObserver$1(unwrapped, name)) return true;
520
+ if (unwrapped.type === AST_NODE_TYPES.CallExpression && isUseRefLikeCall(unwrapped)) {
521
+ const arg = unwrapped.arguments.at(0);
522
+ return arg != null && isNewObserver$1(Extract.unwrap(arg), name);
523
+ }
524
+ if (unwrapped.type === AST_NODE_TYPES.MemberExpression) return isFromObserver$1(context, unwrapped, name);
525
+ return false;
491
526
  }
492
527
  case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object, name);
493
528
  default: return false;
494
529
  }
495
530
  }
531
+ function isFromRefCurrent$1(context, node, refId) {
532
+ let unwrapped = Check.isTypeExpression(node) ? Extract.unwrap(node) : node;
533
+ if (Check.isIdentifier(unwrapped)) {
534
+ const initNode = resolve(context, unwrapped);
535
+ if (initNode != null) unwrapped = Extract.unwrap(initNode);
536
+ }
537
+ return unwrapped.type === AST_NODE_TYPES.MemberExpression && !unwrapped.computed && Check.isIdentifier(Extract.unwrap(unwrapped.property), "current") && isAssignmentTargetEqual(context, unwrapped.object, refId);
538
+ }
496
539
  //#endregion
497
540
  //#region src/rules/no-leaked-intersection-observer/no-leaked-intersection-observer.ts
498
541
  const RULE_NAME$3 = "no-leaked-intersection-observer";
@@ -504,6 +547,20 @@ function getCallKind$3(context, node) {
504
547
  if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee, "IntersectionObserver")) return name;
505
548
  return "other";
506
549
  }
550
+ function isUseRefInitialValue$1(node) {
551
+ let child = node;
552
+ let parent = node.parent;
553
+ while (Check.isTypeExpression(parent)) {
554
+ child = parent;
555
+ parent = parent.parent;
556
+ }
557
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments.at(0) === child && isUseRefLikeCall(parent);
558
+ }
559
+ function resolveLocalFunction$1(context, node, id) {
560
+ const defNode = findVariable(context.sourceCode.getScope(node), id)?.defs.at(-1)?.node;
561
+ const fn = defNode == null ? null : defNode.type === AST_NODE_TYPES.FunctionDeclaration ? defNode : defNode.type === AST_NODE_TYPES.VariableDeclarator && defNode.init != null ? Extract.unwrap(defNode.init) : null;
562
+ return fn != null && Check.isFunction(fn) ? fn : null;
563
+ }
507
564
  var no_leaked_intersection_observer_default = createRule({
508
565
  meta: {
509
566
  type: "problem",
@@ -524,10 +581,25 @@ function create$3(context) {
524
581
  const oEntries = [];
525
582
  const uEntries = [];
526
583
  const dEntries = [];
584
+ const returnedCleanups = /* @__PURE__ */ new Set();
585
+ const cleanupCallees = /* @__PURE__ */ new Set();
586
+ function isInCleanupPhase(node) {
587
+ if (Traverse.findParent(node, isUseEffectCleanupCallback) != null) return true;
588
+ for (const fn of [...returnedCleanups, ...cleanupCallees]) if (Traverse.findParent(node, (n) => n === fn) != null) return true;
589
+ return false;
590
+ }
527
591
  return {
528
592
  ["CallExpression"](node) {
529
- if (Traverse.findParent(node, isUseEffectCallback$3) == null) return;
593
+ const effectCallback = Traverse.findParent(node, isUseEffectCallback$3);
594
+ if (effectCallback == null) return;
530
595
  const callee = Extract.unwrap(node.callee);
596
+ if (callee.type === AST_NODE_TYPES.Identifier) {
597
+ if (isUseEffectCleanupCallback(effectCallback)) {
598
+ const fn = resolveLocalFunction$1(context, node, callee);
599
+ if (fn != null) cleanupCallees.add(fn);
600
+ }
601
+ return;
602
+ }
531
603
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
532
604
  const { object } = callee;
533
605
  match(getCallKind$3(context, node)).with("disconnect", () => {
@@ -557,9 +629,9 @@ function create$3(context) {
557
629
  }).otherwise(() => null);
558
630
  },
559
631
  ["NewExpression"](node) {
560
- const fn = Traverse.findParent(node, and(Check.isFunction, isUseEffectCallback$3));
561
- if (fn == null) return;
562
632
  if (!isNewObserver$1(node, "IntersectionObserver")) return;
633
+ const fn = Traverse.findParent(node, and(Check.isFunction, isUseEffectCallback$3));
634
+ if (fn == null && !isUseRefInitialValue$1(node)) return;
563
635
  const id = resolveEnclosingAssignmentTarget(node);
564
636
  if (id == null) {
565
637
  context.report({
@@ -574,14 +646,27 @@ function create$3(context) {
574
646
  phaseNode: fn
575
647
  });
576
648
  },
649
+ ["ReturnStatement"](node) {
650
+ const arg = node.argument == null ? null : Extract.unwrap(node.argument);
651
+ if (arg == null || arg.type !== AST_NODE_TYPES.Identifier) return;
652
+ const setupFn = Traverse.findParent(node, Check.isFunction);
653
+ if (setupFn == null || !isUseEffectSetupCallback(setupFn)) return;
654
+ const fn = resolveLocalFunction$1(context, node, arg);
655
+ if (fn != null) returnedCleanups.add(fn);
656
+ },
577
657
  ["Program:exit"]() {
578
658
  for (const { id, node, phaseNode } of observers) {
579
- if (dEntries.some((e) => Traverse.findParent(e.node, (n) => n === node) == null && isAssignmentTargetEqual(context, e.observer, id))) continue;
580
- const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
581
- const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
659
+ const isSameObserver = phaseNode == null ? (observer) => isFromRefCurrent$1(context, observer, id) : (observer) => isAssignmentTargetEqual(context, observer, id);
660
+ const isInsideObserverCallback = (entryNode) => Traverse.findParent(entryNode, (n) => n === node) != null;
661
+ if (dEntries.some((e) => !isInsideObserverCallback(e.node) && isInCleanupPhase(e.node) && isSameObserver(e.observer))) continue;
662
+ const matchedOEntries = oEntries.filter((e) => isSameObserver(e.observer));
663
+ const matchedUEntries = uEntries.filter((e) => isSameObserver(e.observer));
582
664
  const isDynamic = or(Check.isCallExpression, Check.isConditional);
583
- const isPhaseNode = (n) => n === phaseNode;
584
- if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
665
+ if (matchedOEntries.some((e) => {
666
+ const entryPhaseNode = phaseNode ?? Traverse.findParent(e.node, isUseEffectCallback$3);
667
+ const isPhaseNode = (n) => n === entryPhaseNode;
668
+ return !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode)));
669
+ })) {
585
670
  context.report({
586
671
  messageId: "expected-disconnect-in-control-flow",
587
672
  node
@@ -589,7 +674,7 @@ function create$3(context) {
589
674
  continue;
590
675
  }
591
676
  for (const oEntry of matchedOEntries) {
592
- if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
677
+ if (matchedUEntries.some((uEntry) => !isInsideObserverCallback(uEntry.node) && isInCleanupPhase(uEntry.node) && isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
593
678
  context.report({
594
679
  messageId: "expected-disconnect-or-unobserve-in-cleanup",
595
680
  node: oEntry.node
@@ -676,12 +761,28 @@ function isFromObserver(context, node, name) {
676
761
  switch (true) {
677
762
  case Check.isIdentifier(node): {
678
763
  const initNode = resolve(context, node);
679
- return isNewObserver(initNode == null ? null : Extract.unwrap(initNode), name);
764
+ const unwrapped = initNode == null ? null : Extract.unwrap(initNode);
765
+ if (unwrapped == null) return false;
766
+ if (isNewObserver(unwrapped, name)) return true;
767
+ if (unwrapped.type === AST_NODE_TYPES.CallExpression && isUseRefLikeCall(unwrapped)) {
768
+ const arg = unwrapped.arguments.at(0);
769
+ return arg != null && isNewObserver(Extract.unwrap(arg), name);
770
+ }
771
+ if (unwrapped.type === AST_NODE_TYPES.MemberExpression) return isFromObserver(context, unwrapped, name);
772
+ return false;
680
773
  }
681
774
  case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object, name);
682
775
  default: return false;
683
776
  }
684
777
  }
778
+ function isFromRefCurrent(context, node, refId) {
779
+ let expr = Check.isTypeExpression(node) ? Extract.unwrap(node) : node;
780
+ if (Check.isIdentifier(expr)) {
781
+ const initNode = resolve(context, expr);
782
+ if (initNode != null) expr = Extract.unwrap(initNode);
783
+ }
784
+ return expr.type === AST_NODE_TYPES.MemberExpression && !expr.computed && Check.isIdentifier(Extract.unwrap(expr.property), "current") && isAssignmentTargetEqual(context, expr.object, refId);
785
+ }
685
786
  //#endregion
686
787
  //#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
687
788
  const RULE_NAME$1 = "no-leaked-resize-observer";
@@ -693,6 +794,20 @@ function getCallKind$1(context, node) {
693
794
  if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee, "ResizeObserver")) return name;
694
795
  return "other";
695
796
  }
797
+ function isUseRefInitialValue(node) {
798
+ let child = node;
799
+ let parent = node.parent;
800
+ while (Check.isTypeExpression(parent)) {
801
+ child = parent;
802
+ parent = parent.parent;
803
+ }
804
+ return parent.type === AST_NODE_TYPES.CallExpression && parent.arguments.at(0) === child && isUseRefLikeCall(parent);
805
+ }
806
+ function resolveLocalFunction(context, node, id) {
807
+ const defNode = findVariable(context.sourceCode.getScope(node), id)?.defs.at(-1)?.node;
808
+ const fn = defNode == null ? null : defNode.type === AST_NODE_TYPES.FunctionDeclaration ? defNode : defNode.type === AST_NODE_TYPES.VariableDeclarator && defNode.init != null ? Extract.unwrap(defNode.init) : null;
809
+ return fn != null && Check.isFunction(fn) ? fn : null;
810
+ }
696
811
  var no_leaked_resize_observer_default = createRule({
697
812
  meta: {
698
813
  type: "problem",
@@ -713,10 +828,25 @@ function create$1(context) {
713
828
  const oEntries = [];
714
829
  const uEntries = [];
715
830
  const dEntries = [];
831
+ const returnedCleanups = /* @__PURE__ */ new Set();
832
+ const cleanupCallees = /* @__PURE__ */ new Set();
833
+ function isInCleanupPhase(node) {
834
+ if (Traverse.findParent(node, isUseEffectCleanupCallback) != null) return true;
835
+ for (const fn of [...returnedCleanups, ...cleanupCallees]) if (Traverse.findParent(node, (n) => n === fn) != null) return true;
836
+ return false;
837
+ }
716
838
  return {
717
839
  ["CallExpression"](node) {
718
- if (Traverse.findParent(node, isUseEffectCallback$1) == null) return;
840
+ const effectCallback = Traverse.findParent(node, isUseEffectCallback$1);
841
+ if (effectCallback == null) return;
719
842
  const callee = Extract.unwrap(node.callee);
843
+ if (callee.type === AST_NODE_TYPES.Identifier) {
844
+ if (isUseEffectCleanupCallback(effectCallback)) {
845
+ const fn = resolveLocalFunction(context, node, callee);
846
+ if (fn != null) cleanupCallees.add(fn);
847
+ }
848
+ return;
849
+ }
720
850
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
721
851
  const { object } = callee;
722
852
  match(getCallKind$1(context, node)).with("disconnect", () => {
@@ -746,9 +876,9 @@ function create$1(context) {
746
876
  }).otherwise(() => null);
747
877
  },
748
878
  ["NewExpression"](node) {
749
- const fn = Traverse.findParent(node, and(Check.isFunction, isUseEffectCallback$1));
750
- if (fn == null) return;
751
879
  if (!isNewObserver(node, "ResizeObserver")) return;
880
+ const fn = Traverse.findParent(node, and(Check.isFunction, isUseEffectCallback$1));
881
+ if (fn == null && !isUseRefInitialValue(node)) return;
752
882
  const id = resolveEnclosingAssignmentTarget(node);
753
883
  if (id == null) {
754
884
  context.report({
@@ -763,15 +893,27 @@ function create$1(context) {
763
893
  phaseNode: fn
764
894
  });
765
895
  },
896
+ ["ReturnStatement"](node) {
897
+ const arg = node.argument == null ? null : Extract.unwrap(node.argument);
898
+ if (arg == null || arg.type !== AST_NODE_TYPES.Identifier) return;
899
+ const setupFn = Traverse.findParent(node, Check.isFunction);
900
+ if (setupFn == null || !isUseEffectSetupCallback(setupFn)) return;
901
+ const fn = resolveLocalFunction(context, node, arg);
902
+ if (fn != null) returnedCleanups.add(fn);
903
+ },
766
904
  ["Program:exit"]() {
767
905
  for (const { id, node, phaseNode } of observers) {
768
- const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
769
- if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
770
- const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
771
- const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
906
+ const isSameObserver = phaseNode == null ? (observer) => isFromRefCurrent(context, observer, id) : (observer) => isAssignmentTargetEqual(context, observer, id);
907
+ const isInsideObserverCallback = (entryNode) => Traverse.findParent(entryNode, (n) => n === node) != null;
908
+ if (dEntries.some((e) => !isInsideObserverCallback(e.node) && isInCleanupPhase(e.node) && isSameObserver(e.observer))) continue;
909
+ const matchedOEntries = oEntries.filter((e) => isSameObserver(e.observer));
910
+ const matchedUEntries = uEntries.filter((e) => isSameObserver(e.observer));
772
911
  const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
773
- const isPhaseNode = (node) => node === phaseNode;
774
- if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
912
+ if (matchedOEntries.some((e) => {
913
+ const entryPhaseNode = phaseNode ?? Traverse.findParent(e.node, isUseEffectCallback$1);
914
+ const isPhaseNode = (node) => node === entryPhaseNode;
915
+ return !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode)));
916
+ })) {
775
917
  context.report({
776
918
  messageId: "expected-disconnect-in-control-flow",
777
919
  node
@@ -779,7 +921,7 @@ function create$1(context) {
779
921
  continue;
780
922
  }
781
923
  for (const oEntry of matchedOEntries) {
782
- if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
924
+ if (matchedUEntries.some((uEntry) => !isInsideObserverCallback(uEntry.node) && isInCleanupPhase(uEntry.node) && isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
783
925
  context.report({
784
926
  messageId: "expected-disconnect-or-unobserve-in-cleanup",
785
927
  node: oEntry.node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-web-api",
3
- "version": "5.24.6",
3
+ "version": "5.24.8",
4
4
  "description": "ESLint React's ESLint plugin for interacting with Web APIs",
5
5
  "keywords": [
6
6
  "react",
@@ -37,13 +37,13 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@eslint-react/ast": "5.24.6",
41
- "@eslint-react/core": "5.24.6",
42
- "@eslint-react/eslint": "5.24.6",
43
- "@eslint-react/shared": "5.24.6",
44
- "@eslint-react/var": "5.24.6",
45
- "@typescript-eslint/types": "^8.71.0",
46
- "@typescript-eslint/utils": "^8.71.0",
40
+ "@eslint-react/ast": "5.24.8",
41
+ "@eslint-react/core": "5.24.8",
42
+ "@eslint-react/eslint": "5.24.8",
43
+ "@eslint-react/shared": "5.24.8",
44
+ "@eslint-react/var": "5.24.8",
45
+ "@typescript-eslint/types": "^8.71.1",
46
+ "@typescript-eslint/utils": "^8.71.1",
47
47
  "ts-pattern": "^5.9.0"
48
48
  },
49
49
  "devDependencies": {