eslint-plugin-react-web-api 5.20.3 → 5.20.4

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 +157 -250
  2. package/package.json +6 -7
package/dist/index.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
2
- import * as core from "@eslint-react/core";
3
- import birecord from "birecord";
4
- import { P, isMatching, match } from "ts-pattern";
5
2
  import { ESLintUtils } from "@typescript-eslint/utils";
6
3
  import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
4
+ import { isUseEffectCleanupCallback, isUseEffectSetupCallback } from "@eslint-react/core";
7
5
  import "@eslint-react/eslint";
8
6
  import { isAssignmentTargetEqual, isInitializedFromReactNative, isValueEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
9
7
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
8
+ import { P, isMatching, match } from "ts-pattern";
10
9
  import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
11
10
 
12
11
  //#region \0rolldown/runtime.js
@@ -28,14 +27,7 @@ var __exportAll = (all, no_symbols) => {
28
27
  //#endregion
29
28
  //#region package.json
30
29
  var name$1 = "eslint-plugin-react-web-api";
31
- var version = "5.20.3";
32
-
33
- //#endregion
34
- //#region src/types/component-phase.ts
35
- const ComponentPhaseRelevance = birecord({ setup: "cleanup" });
36
- function getPhaseKindOfFunction(node) {
37
- return match(node).when(core.isUseEffectSetupCallback, () => "setup").when(core.isUseEffectCleanupCallback, () => "cleanup").otherwise(() => null);
38
- }
30
+ var version = "5.20.4";
39
31
 
40
32
  //#endregion
41
33
  //#region src/utils/create-rule.ts
@@ -101,11 +93,13 @@ function getOptions(context, node) {
101
93
  const RULE_NAME$5 = "no-leaked-event-listener";
102
94
  function getCallKind$5(node) {
103
95
  const name = Extract.getCalleeName(node);
104
- if (name != null && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(name)) return name;
96
+ if (name != null && isMatching(P.union("addEventListener", "removeEventListener"))(name)) return name;
105
97
  return "other";
106
98
  }
107
- function getFunctionKind$2(node) {
108
- return getPhaseKindOfFunction(node) ?? "other";
99
+ function getFunctionKind$5(node) {
100
+ if (isUseEffectSetupCallback(node)) return "setup";
101
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
102
+ return "other";
109
103
  }
110
104
  var no_leaked_event_listener_default = createRule({
111
105
  meta: {
@@ -127,7 +121,6 @@ function create$5(context) {
127
121
  const fEntries = [];
128
122
  const aEntries = [];
129
123
  const rEntries = [];
130
- const abortedSignals = [];
131
124
  function isSameObject(a, b) {
132
125
  switch (true) {
133
126
  case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
@@ -137,10 +130,10 @@ function create$5(context) {
137
130
  function isInverseEntry(aEntry, rEntry) {
138
131
  const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
139
132
  const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
140
- if (ComponentPhaseRelevance.get(aPhase) !== rPhase) return false;
133
+ if (aPhase !== "setup" || rPhase !== "cleanup") return false;
141
134
  return isSameObject(aCallee, rCallee) && Compare.isEqual(aListener, rListener) && isValueEqual(context, aType, rType) && aCapture === rCapture;
142
135
  }
143
- function checkInlineFunction(node, callKind, options) {
136
+ function visitInlineFunction(node, callKind, options) {
144
137
  const listener = node.arguments.at(1);
145
138
  if (!Check.isFunction(listener)) return;
146
139
  if (options.signal != null) return;
@@ -152,26 +145,21 @@ function create$5(context) {
152
145
  }
153
146
  return {
154
147
  [":function"](node) {
155
- const kind = getFunctionKind$2(node);
156
- fEntries.push({
157
- kind,
158
- node
159
- });
148
+ fEntries.push(getFunctionKind$5(node));
160
149
  },
161
150
  [":function:exit"]() {
162
151
  fEntries.pop();
163
152
  },
164
153
  ["CallExpression"](node) {
165
- const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
154
+ const fKind = fEntries.findLast((kind) => kind !== "other");
166
155
  if (fKind == null) return;
167
- if (!ComponentPhaseRelevance.has(fKind)) return;
168
156
  const callee = Extract.unwrap(node.callee);
169
157
  match(getCallKind$5(node)).with("addEventListener", (callKind) => {
170
158
  if (callee.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(callee.object) && isInitializedFromReactNative(callee.object.name, context.sourceCode.getScope(node))) return;
171
159
  const [type, listener, options] = node.arguments;
172
160
  if (type == null || listener == null) return;
173
161
  const opts = options == null ? defaultOptions : getOptions(context, options);
174
- checkInlineFunction(node, callKind, opts);
162
+ visitInlineFunction(node, callKind, opts);
175
163
  aEntries.push({
176
164
  ...opts,
177
165
  type,
@@ -185,34 +173,27 @@ function create$5(context) {
185
173
  const [type, listener, options] = node.arguments;
186
174
  if (type == null || listener == null) return;
187
175
  const opts = options == null ? defaultOptions : getOptions(context, options);
188
- checkInlineFunction(node, callKind, opts);
176
+ visitInlineFunction(node, callKind, opts);
189
177
  rEntries.push({
190
- ...opts,
191
178
  type,
192
179
  callee,
180
+ capture: opts.capture,
193
181
  listener,
194
182
  method: "removeEventListener",
195
183
  node,
196
184
  phase: fKind
197
185
  });
198
- }).with("abort", () => {
199
- abortedSignals.push(node.callee);
200
186
  }).otherwise(() => null);
201
187
  },
202
188
  ["Program:exit"]() {
203
189
  for (const aEntry of aEntries) {
204
190
  if (aEntry.signal != null) continue;
205
191
  if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
206
- switch (aEntry.phase) {
207
- case "setup":
208
- case "cleanup":
209
- context.report({
210
- data: { effectMethodKind: "useEffect" },
211
- messageId: "expectedRemoveEventListenerInCleanup",
212
- node: aEntry.node
213
- });
214
- continue;
215
- }
192
+ context.report({
193
+ data: { effectMethodKind: "useEffect" },
194
+ messageId: "expectedRemoveEventListenerInCleanup",
195
+ node: aEntry.node
196
+ });
216
197
  }
217
198
  }
218
199
  };
@@ -220,16 +201,6 @@ function create$5(context) {
220
201
 
221
202
  //#endregion
222
203
  //#region src/rules/no-leaked-fetch/lib.ts
223
- function findProperty(node, name) {
224
- for (const property of node) {
225
- if (property.type === AST_NODE_TYPES.Property && !property.computed && Check.isIdentifier(property.key, name)) return property;
226
- if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
227
- const found = findProperty(property.argument.properties, name);
228
- if (found != null) return found;
229
- }
230
- }
231
- return null;
232
- }
233
204
  function resolveToObjectExpression(context, node) {
234
205
  node = Extract.unwrap(node);
235
206
  switch (node.type) {
@@ -248,11 +219,14 @@ function resolveToObjectExpression(context, node) {
248
219
  //#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
249
220
  const RULE_NAME$4 = "no-leaked-fetch";
250
221
  function getCallKind$4(node) {
251
- switch (Extract.getCalleeName(node)) {
252
- case "fetch": return "fetch";
253
- case "abort": return "abort";
254
- default: return "other";
255
- }
222
+ const name = Extract.getCalleeName(node);
223
+ if (name != null && isMatching(P.union("fetch", "abort"))(name)) return name;
224
+ return "other";
225
+ }
226
+ function getFunctionKind$4(node) {
227
+ if (isUseEffectSetupCallback(node)) return "setup";
228
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
229
+ return "other";
256
230
  }
257
231
  function getControllerFromSignal(context, node) {
258
232
  node = Extract.unwrap(node);
@@ -294,7 +268,7 @@ function getFetchController(context, node) {
294
268
  controller: null,
295
269
  isParamSignal: false
296
270
  };
297
- const signalProp = findProperty(options.properties, "signal");
271
+ const signalProp = Extract.findProperty(options.properties, "signal");
298
272
  if (signalProp?.type !== AST_NODE_TYPES.Property) return {
299
273
  controller: null,
300
274
  isParamSignal: false
@@ -328,42 +302,31 @@ function create$4(context) {
328
302
  const abortEntries = [];
329
303
  return {
330
304
  [":function"](node) {
331
- const kind = getPhaseKindOfFunction(node) ?? "other";
332
- fEntries.push({
333
- kind,
334
- node
335
- });
305
+ fEntries.push(getFunctionKind$4(node));
336
306
  },
337
307
  [":function:exit"]() {
338
308
  fEntries.pop();
339
309
  },
340
310
  ["CallExpression"](node) {
341
- const fEntry = fEntries.at(-1);
342
- if (fEntry == null || !ComponentPhaseRelevance.has(fEntry.kind)) return;
343
- switch (getCallKind$4(node)) {
344
- case "fetch": {
345
- if (fEntry.kind !== "setup") return;
346
- const { controller, isParamSignal } = getFetchController(context, node);
347
- fetchEntries.push({
348
- controller,
349
- isParamSignal,
350
- node,
351
- phase: fEntry.kind
352
- });
353
- break;
354
- }
355
- case "abort": {
356
- if (fEntry.kind !== "cleanup") return;
357
- const controller = getAbortController(node);
358
- if (controller == null) break;
359
- abortEntries.push({
360
- controller,
361
- node,
362
- phase: fEntry.kind
363
- });
364
- break;
365
- }
366
- }
311
+ const fKind = fEntries.at(-1);
312
+ if (fKind == null || fKind === "other") return;
313
+ match(getCallKind$4(node)).with("fetch", () => {
314
+ if (fKind !== "setup") return;
315
+ const { controller, isParamSignal } = getFetchController(context, node);
316
+ fetchEntries.push({
317
+ controller,
318
+ isParamSignal,
319
+ node
320
+ });
321
+ }).with("abort", () => {
322
+ if (fKind !== "cleanup") return;
323
+ const controller = getAbortController(node);
324
+ if (controller == null) return;
325
+ abortEntries.push({
326
+ controller,
327
+ node
328
+ });
329
+ }).otherwise(() => null);
367
330
  },
368
331
  ["Program:exit"]() {
369
332
  for (const fEntry of fetchEntries) {
@@ -864,18 +827,18 @@ const takeWhile = dual(2, (xs, pred) => {
864
827
 
865
828
  //#endregion
866
829
  //#region src/rules/no-leaked-intersection-observer/lib.ts
867
- function isNewIntersectionObserver(node) {
830
+ function isNewObserver$1(node, name) {
868
831
  if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
869
832
  const callee = Extract.unwrap(node.callee);
870
- return Check.isIdentifier(callee, "IntersectionObserver");
833
+ return Check.isIdentifier(callee, name);
871
834
  }
872
- function isFromObserver$1(context, node) {
835
+ function isFromObserver$1(context, node, name) {
873
836
  switch (true) {
874
837
  case Check.isIdentifier(node): {
875
838
  const initNode = resolve(context, node);
876
- return isNewIntersectionObserver(initNode == null ? null : Extract.unwrap(initNode));
839
+ return isNewObserver$1(initNode == null ? null : Extract.unwrap(initNode), name);
877
840
  }
878
- case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object);
841
+ case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object, name);
879
842
  default: return false;
880
843
  }
881
844
  }
@@ -885,13 +848,15 @@ function isFromObserver$1(context, node) {
885
848
  const RULE_NAME$3 = "no-leaked-intersection-observer";
886
849
  function getCallKind$3(context, node) {
887
850
  const callee = Extract.unwrap(node.callee);
888
- if (callee.type !== AST_NODE_TYPES.Identifier && callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
851
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
889
852
  const name = Extract.getCalleeName(node);
890
- if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee)) return name;
853
+ if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee, "IntersectionObserver")) return name;
891
854
  return "other";
892
855
  }
893
- function getFunctionKind$1(node) {
894
- return getPhaseKindOfFunction(node) ?? "other";
856
+ function getFunctionKind$3(node) {
857
+ if (isUseEffectSetupCallback(node)) return "setup";
858
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
859
+ return "other";
895
860
  }
896
861
  var no_leaked_intersection_observer_default = createRule({
897
862
  meta: {
@@ -917,7 +882,7 @@ function create$3(context) {
917
882
  const dEntries = [];
918
883
  return {
919
884
  [":function"](node) {
920
- const kind = getFunctionKind$1(node);
885
+ const kind = getFunctionKind$3(node);
921
886
  fEntries.push({
922
887
  kind,
923
888
  node
@@ -927,51 +892,40 @@ function create$3(context) {
927
892
  fEntries.pop();
928
893
  },
929
894
  ["CallExpression"](node) {
895
+ if (fEntries.findLast((x) => x.kind !== "other")?.kind == null) return;
930
896
  const callee = Extract.unwrap(node.callee);
931
897
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
932
- const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
933
- if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
934
898
  const { object } = callee;
935
899
  match(getCallKind$3(context, node)).with("disconnect", () => {
936
900
  dEntries.push({
937
- kind: "IntersectionObserver",
938
- callee,
939
901
  method: "disconnect",
940
902
  node,
941
- observer: object,
942
- phase: fKind
903
+ observer: object
943
904
  });
944
905
  }).with("observe", () => {
945
906
  const [element] = node.arguments;
946
907
  if (element == null) return;
947
908
  oEntries.push({
948
- kind: "IntersectionObserver",
949
- callee,
950
909
  element,
951
910
  method: "observe",
952
911
  node,
953
- observer: object,
954
- phase: fKind
912
+ observer: object
955
913
  });
956
914
  }).with("unobserve", () => {
957
915
  const [element] = node.arguments;
958
916
  if (element == null) return;
959
917
  uEntries.push({
960
- kind: "IntersectionObserver",
961
- callee,
962
918
  element,
963
919
  method: "unobserve",
964
920
  node,
965
- observer: object,
966
- phase: fKind
921
+ observer: object
967
922
  });
968
923
  }).otherwise(() => null);
969
924
  },
970
925
  ["NewExpression"](node) {
971
926
  const fEntry = fEntries.findLast((x) => x.kind !== "other");
972
927
  if (fEntry == null) return;
973
- if (!ComponentPhaseRelevance.has(fEntry.kind)) return;
974
- if (!isNewIntersectionObserver(node)) return;
928
+ if (!isNewObserver$1(node, "IntersectionObserver")) return;
975
929
  const id = resolveEnclosingAssignmentTarget(node);
976
930
  if (id == null) {
977
931
  context.report({
@@ -983,7 +937,6 @@ function create$3(context) {
983
937
  observers.push({
984
938
  id,
985
939
  node,
986
- phase: fEntry.kind,
987
940
  phaseNode: fEntry.node
988
941
  });
989
942
  },
@@ -991,19 +944,19 @@ function create$3(context) {
991
944
  for (const { id, node, phaseNode } of observers) {
992
945
  const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
993
946
  if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
994
- const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
995
- const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
947
+ const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
948
+ const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
996
949
  const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
997
950
  const isPhaseNode = (node) => node === phaseNode;
998
- if (oentries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
951
+ if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
999
952
  context.report({
1000
953
  messageId: "expectedDisconnectInControlFlow",
1001
954
  node
1002
955
  });
1003
956
  continue;
1004
957
  }
1005
- for (const oEntry of oentries) {
1006
- if (uentries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
958
+ for (const oEntry of matchedOEntries) {
959
+ if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
1007
960
  context.report({
1008
961
  messageId: "expectedDisconnectOrUnobserveInCleanup",
1009
962
  node: oEntry.node
@@ -1022,6 +975,11 @@ function getCallKind$2(node) {
1022
975
  if (name != null && isMatching(P.union("setInterval", "clearInterval"))(name)) return name;
1023
976
  return "other";
1024
977
  }
978
+ function getFunctionKind$2(node) {
979
+ if (isUseEffectSetupCallback(node)) return "setup";
980
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
981
+ return "other";
982
+ }
1025
983
  var no_leaked_interval_default = createRule({
1026
984
  meta: {
1027
985
  type: "problem",
@@ -1046,68 +1004,43 @@ function create$2(context) {
1046
1004
  }
1047
1005
  return {
1048
1006
  [":function"](node) {
1049
- const kind = getPhaseKindOfFunction(node) ?? "other";
1050
- fEntries.push({
1051
- kind,
1052
- node
1053
- });
1007
+ fEntries.push(getFunctionKind$2(node));
1054
1008
  },
1055
1009
  [":function:exit"]() {
1056
1010
  fEntries.pop();
1057
1011
  },
1058
1012
  ["CallExpression"](node) {
1059
- switch (getCallKind$2(node)) {
1060
- case "setInterval": {
1061
- const fEntry = fEntries.findLast((x) => x.kind !== "other");
1062
- if (fEntry == null) break;
1063
- if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
1064
- const intervalIdNode = resolveEnclosingAssignmentTarget(node);
1065
- if (intervalIdNode == null) {
1066
- context.report({
1067
- messageId: "expectedIntervalId",
1068
- node
1069
- });
1070
- break;
1071
- }
1072
- sEntries.push({
1073
- kind: "interval",
1074
- callee: node.callee,
1075
- node,
1076
- phase: fEntry.kind,
1077
- timerId: intervalIdNode
1078
- });
1079
- break;
1080
- }
1081
- case "clearInterval": {
1082
- const fEntry = fEntries.findLast((x) => x.kind !== "other");
1083
- if (fEntry == null) break;
1084
- if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
1085
- const [intervalIdNode] = node.arguments;
1086
- if (intervalIdNode == null) break;
1087
- cEntries.push({
1088
- kind: "interval",
1089
- callee: node.callee,
1090
- node,
1091
- phase: fEntry.kind,
1092
- timerId: intervalIdNode
1013
+ if (fEntries.findLast((kind) => kind !== "other") == null) return;
1014
+ match(getCallKind$2(node)).with("setInterval", () => {
1015
+ const intervalIdNode = resolveEnclosingAssignmentTarget(node);
1016
+ if (intervalIdNode == null) {
1017
+ context.report({
1018
+ messageId: "expectedIntervalId",
1019
+ node
1093
1020
  });
1094
- break;
1021
+ return;
1095
1022
  }
1096
- }
1023
+ sEntries.push({
1024
+ node,
1025
+ timerId: intervalIdNode
1026
+ });
1027
+ }).with("clearInterval", () => {
1028
+ const [intervalIdNode] = node.arguments;
1029
+ if (intervalIdNode == null) return;
1030
+ cEntries.push({
1031
+ node,
1032
+ timerId: intervalIdNode
1033
+ });
1034
+ }).otherwise(() => null);
1097
1035
  },
1098
1036
  ["Program:exit"]() {
1099
1037
  for (const sEntry of sEntries) {
1100
1038
  if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
1101
- switch (sEntry.phase) {
1102
- case "setup":
1103
- case "cleanup":
1104
- context.report({
1105
- data: { kind: "useEffect" },
1106
- messageId: "expectedClearIntervalInCleanup",
1107
- node: sEntry.node
1108
- });
1109
- continue;
1110
- }
1039
+ context.report({
1040
+ data: { kind: "useEffect" },
1041
+ messageId: "expectedClearIntervalInCleanup",
1042
+ node: sEntry.node
1043
+ });
1111
1044
  }
1112
1045
  }
1113
1046
  };
@@ -1115,18 +1048,18 @@ function create$2(context) {
1115
1048
 
1116
1049
  //#endregion
1117
1050
  //#region src/rules/no-leaked-resize-observer/lib.ts
1118
- function isNewResizeObserver(node) {
1051
+ function isNewObserver(node, name) {
1119
1052
  if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
1120
1053
  const callee = Extract.unwrap(node.callee);
1121
- return Check.isIdentifier(callee, "ResizeObserver");
1054
+ return Check.isIdentifier(callee, name);
1122
1055
  }
1123
- function isFromObserver(context, node) {
1056
+ function isFromObserver(context, node, name) {
1124
1057
  switch (true) {
1125
1058
  case Check.isIdentifier(node): {
1126
1059
  const initNode = resolve(context, node);
1127
- return isNewResizeObserver(initNode == null ? null : Extract.unwrap(initNode));
1060
+ return isNewObserver(initNode == null ? null : Extract.unwrap(initNode), name);
1128
1061
  }
1129
- case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
1062
+ case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object, name);
1130
1063
  default: return false;
1131
1064
  }
1132
1065
  }
@@ -1136,13 +1069,15 @@ function isFromObserver(context, node) {
1136
1069
  const RULE_NAME$1 = "no-leaked-resize-observer";
1137
1070
  function getCallKind$1(context, node) {
1138
1071
  const callee = Extract.unwrap(node.callee);
1139
- if (callee.type !== AST_NODE_TYPES.Identifier && callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
1072
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
1140
1073
  const name = Extract.getCalleeName(node);
1141
- if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee)) return name;
1074
+ if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee, "ResizeObserver")) return name;
1142
1075
  return "other";
1143
1076
  }
1144
- function getFunctionKind(node) {
1145
- return getPhaseKindOfFunction(node) ?? "other";
1077
+ function getFunctionKind$1(node) {
1078
+ if (isUseEffectSetupCallback(node)) return "setup";
1079
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
1080
+ return "other";
1146
1081
  }
1147
1082
  var no_leaked_resize_observer_default = createRule({
1148
1083
  meta: {
@@ -1168,7 +1103,7 @@ function create$1(context) {
1168
1103
  const dEntries = [];
1169
1104
  return {
1170
1105
  [":function"](node) {
1171
- const kind = getFunctionKind(node);
1106
+ const kind = getFunctionKind$1(node);
1172
1107
  fEntries.push({
1173
1108
  kind,
1174
1109
  node
@@ -1178,51 +1113,40 @@ function create$1(context) {
1178
1113
  fEntries.pop();
1179
1114
  },
1180
1115
  ["CallExpression"](node) {
1116
+ if (fEntries.findLast((x) => x.kind !== "other")?.kind == null) return;
1181
1117
  const callee = Extract.unwrap(node.callee);
1182
1118
  if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
1183
- const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
1184
- if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
1185
1119
  const { object } = callee;
1186
1120
  match(getCallKind$1(context, node)).with("disconnect", () => {
1187
1121
  dEntries.push({
1188
- kind: "ResizeObserver",
1189
- callee,
1190
1122
  method: "disconnect",
1191
1123
  node,
1192
- observer: object,
1193
- phase: fKind
1124
+ observer: object
1194
1125
  });
1195
1126
  }).with("observe", () => {
1196
1127
  const [element] = node.arguments;
1197
1128
  if (element == null) return;
1198
1129
  oEntries.push({
1199
- kind: "ResizeObserver",
1200
- callee,
1201
1130
  element,
1202
1131
  method: "observe",
1203
1132
  node,
1204
- observer: object,
1205
- phase: fKind
1133
+ observer: object
1206
1134
  });
1207
1135
  }).with("unobserve", () => {
1208
1136
  const [element] = node.arguments;
1209
1137
  if (element == null) return;
1210
1138
  uEntries.push({
1211
- kind: "ResizeObserver",
1212
- callee,
1213
1139
  element,
1214
1140
  method: "unobserve",
1215
1141
  node,
1216
- observer: object,
1217
- phase: fKind
1142
+ observer: object
1218
1143
  });
1219
1144
  }).otherwise(() => null);
1220
1145
  },
1221
1146
  ["NewExpression"](node) {
1222
1147
  const fEntry = fEntries.findLast((x) => x.kind !== "other");
1223
1148
  if (fEntry == null) return;
1224
- if (!ComponentPhaseRelevance.has(fEntry.kind)) return;
1225
- if (!isNewResizeObserver(node)) return;
1149
+ if (!isNewObserver(node, "ResizeObserver")) return;
1226
1150
  const id = resolveEnclosingAssignmentTarget(node);
1227
1151
  if (id == null) {
1228
1152
  context.report({
@@ -1234,7 +1158,6 @@ function create$1(context) {
1234
1158
  observers.push({
1235
1159
  id,
1236
1160
  node,
1237
- phase: fEntry.kind,
1238
1161
  phaseNode: fEntry.node
1239
1162
  });
1240
1163
  },
@@ -1242,19 +1165,19 @@ function create$1(context) {
1242
1165
  for (const { id, node, phaseNode } of observers) {
1243
1166
  const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
1244
1167
  if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
1245
- const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
1246
- const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
1168
+ const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
1169
+ const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
1247
1170
  const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
1248
1171
  const isPhaseNode = (node) => node === phaseNode;
1249
- if (oentries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
1172
+ if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
1250
1173
  context.report({
1251
1174
  messageId: "expectedDisconnectInControlFlow",
1252
1175
  node
1253
1176
  });
1254
1177
  continue;
1255
1178
  }
1256
- for (const oEntry of oentries) {
1257
- if (uentries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
1179
+ for (const oEntry of matchedOEntries) {
1180
+ if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
1258
1181
  context.report({
1259
1182
  messageId: "expectedDisconnectOrUnobserveInCleanup",
1260
1183
  node: oEntry.node
@@ -1273,6 +1196,11 @@ function getCallKind(node) {
1273
1196
  if (name != null && isMatching(P.union("setTimeout", "clearTimeout"))(name)) return name;
1274
1197
  return "other";
1275
1198
  }
1199
+ function getFunctionKind(node) {
1200
+ if (isUseEffectSetupCallback(node)) return "setup";
1201
+ if (isUseEffectCleanupCallback(node)) return "cleanup";
1202
+ return "other";
1203
+ }
1276
1204
  var no_leaked_timeout_default = createRule({
1277
1205
  meta: {
1278
1206
  type: "problem",
@@ -1291,70 +1219,49 @@ function create(context) {
1291
1219
  if (!context.sourceCode.text.includes("setTimeout")) return {};
1292
1220
  const fEntries = [];
1293
1221
  const sEntries = [];
1294
- const rEntries = [];
1222
+ const cEntries = [];
1295
1223
  function isInverseEntry(a, b) {
1296
1224
  return isAssignmentTargetEqual(context, a.timerId, b.timerId);
1297
1225
  }
1298
1226
  return {
1299
1227
  [":function"](node) {
1300
- const kind = getPhaseKindOfFunction(node) ?? "other";
1301
- fEntries.push({
1302
- kind,
1303
- node
1304
- });
1228
+ fEntries.push(getFunctionKind(node));
1305
1229
  },
1306
1230
  [":function:exit"]() {
1307
1231
  fEntries.pop();
1308
1232
  },
1309
1233
  ["CallExpression"](node) {
1310
- const fEntry = fEntries.findLast((f) => f.kind !== "other");
1311
- if (!ComponentPhaseRelevance.has(fEntry?.kind)) return;
1312
- switch (getCallKind(node)) {
1313
- case "setTimeout": {
1314
- const timeoutIdNode = resolveEnclosingAssignmentTarget(node);
1315
- if (timeoutIdNode == null) {
1316
- context.report({
1317
- messageId: "expectedTimeoutId",
1318
- node
1319
- });
1320
- break;
1321
- }
1322
- sEntries.push({
1323
- kind: "timeout",
1324
- callee: node.callee,
1325
- node,
1326
- phase: fEntry.kind,
1327
- timerId: timeoutIdNode
1328
- });
1329
- break;
1330
- }
1331
- case "clearTimeout": {
1332
- const [timeoutIdNode] = node.arguments;
1333
- if (timeoutIdNode == null) break;
1334
- rEntries.push({
1335
- kind: "timeout",
1336
- callee: node.callee,
1337
- node,
1338
- phase: fEntry.kind,
1339
- timerId: timeoutIdNode
1234
+ if (fEntries.findLast((kind) => kind !== "other") == null) return;
1235
+ match(getCallKind(node)).with("setTimeout", () => {
1236
+ const timeoutIdNode = resolveEnclosingAssignmentTarget(node);
1237
+ if (timeoutIdNode == null) {
1238
+ context.report({
1239
+ messageId: "expectedTimeoutId",
1240
+ node
1340
1241
  });
1341
- break;
1242
+ return;
1342
1243
  }
1343
- }
1244
+ sEntries.push({
1245
+ node,
1246
+ timerId: timeoutIdNode
1247
+ });
1248
+ }).with("clearTimeout", () => {
1249
+ const [timeoutIdNode] = node.arguments;
1250
+ if (timeoutIdNode == null) return;
1251
+ cEntries.push({
1252
+ node,
1253
+ timerId: timeoutIdNode
1254
+ });
1255
+ }).otherwise(() => null);
1344
1256
  },
1345
1257
  ["Program:exit"]() {
1346
1258
  for (const sEntry of sEntries) {
1347
- if (rEntries.some((rEntry) => isInverseEntry(sEntry, rEntry))) continue;
1348
- switch (sEntry.phase) {
1349
- case "setup":
1350
- case "cleanup":
1351
- context.report({
1352
- data: { kind: "useEffect" },
1353
- messageId: "expectedClearTimeoutInCleanup",
1354
- node: sEntry.node
1355
- });
1356
- continue;
1357
- }
1259
+ if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
1260
+ context.report({
1261
+ data: { kind: "useEffect" },
1262
+ messageId: "expectedClearTimeoutInCleanup",
1263
+ node: sEntry.node
1264
+ });
1358
1265
  }
1359
1266
  }
1360
1267
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-web-api",
3
- "version": "5.20.3",
3
+ "version": "5.20.4",
4
4
  "description": "ESLint React's ESLint plugin for interacting with Web APIs",
5
5
  "keywords": [
6
6
  "react",
@@ -37,14 +37,13 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@eslint-react/ast": "5.20.3",
41
- "@eslint-react/core": "5.20.3",
42
- "@eslint-react/eslint": "5.20.3",
43
- "@eslint-react/shared": "5.20.3",
44
- "@eslint-react/var": "5.20.3",
40
+ "@eslint-react/ast": "5.20.4",
41
+ "@eslint-react/core": "5.20.4",
42
+ "@eslint-react/eslint": "5.20.4",
43
+ "@eslint-react/shared": "5.20.4",
44
+ "@eslint-react/var": "5.20.4",
45
45
  "@typescript-eslint/types": "^8.70.0",
46
46
  "@typescript-eslint/utils": "^8.70.0",
47
- "birecord": "^0.1.2",
48
47
  "ts-pattern": "^5.9.0"
49
48
  },
50
49
  "devDependencies": {