eslint-plugin-react-web-api 5.2.5-next.1 → 5.3.0-beta.0

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 +209 -35
  2. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -4,10 +4,10 @@ import * as core from "@eslint-react/core";
4
4
  import { merge } from "@eslint-react/eslint";
5
5
  import { isAssignmentTargetEqual, isValueEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
6
6
  import { AST_NODE_TYPES } from "@typescript-eslint/types";
7
- import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
8
7
  import { P, isMatching, match } from "ts-pattern";
9
8
  import birecord from "birecord";
10
9
  import { ESLintUtils } from "@typescript-eslint/utils";
10
+ import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
11
11
 
12
12
  //#region \0rolldown/runtime.js
13
13
  var __defProp = Object.defineProperty;
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
28
28
  //#endregion
29
29
  //#region package.json
30
30
  var name$1 = "eslint-plugin-react-web-api";
31
- var version = "5.2.5-next.1";
31
+ var version = "5.3.0-beta.0";
32
32
 
33
33
  //#endregion
34
34
  //#region src/types/component-phase.ts
@@ -46,46 +46,33 @@ const createRule = ESLintUtils.RuleCreator(getDocsUrl);
46
46
 
47
47
  //#endregion
48
48
  //#region src/rules/no-leaked-event-listener/lib.ts
49
- function findProperty(of, named) {
49
+ function findProperty$1(of, named) {
50
50
  for (const property of of) {
51
51
  if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === named) return property;
52
52
  if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
53
- const found = findProperty(property.argument.properties, named);
53
+ const found = findProperty$1(property.argument.properties, named);
54
54
  if (found != null) return found;
55
55
  }
56
56
  }
57
57
  return null;
58
58
  }
59
-
60
- //#endregion
61
- //#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
62
- const RULE_NAME$3 = "no-leaked-event-listener";
63
- const defaultOptions = {
64
- capture: false,
65
- signal: null
66
- };
67
- function getCallKind$3(node) {
68
- switch (true) {
69
- case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.name): return node.callee.name;
70
- case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.property.name): return node.callee.property.name;
71
- default: return "other";
72
- }
73
- }
74
- function getFunctionKind$1(node) {
75
- return getPhaseKindOfFunction(node) ?? "other";
76
- }
77
59
  function getSignalValueExpression(context, node) {
78
60
  if (node == null) return null;
79
61
  switch (node.type) {
80
62
  case AST_NODE_TYPES.Identifier: {
81
63
  const resolved = resolve(context, node);
82
- if (resolved != null && Check.isFunction(resolved)) return node;
83
- return getSignalValueExpression(context, resolved);
64
+ const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
65
+ if (unwrapped != null && Check.isFunction(unwrapped)) return node;
66
+ return getSignalValueExpression(context, unwrapped);
84
67
  }
85
68
  case AST_NODE_TYPES.MemberExpression: return node;
86
69
  default: return null;
87
70
  }
88
71
  }
72
+ const defaultOptions = {
73
+ capture: false,
74
+ signal: null
75
+ };
89
76
  function getOptions(context, node) {
90
77
  const initialScope = context.sourceCode.getScope(node);
91
78
  function getOpts(node) {
@@ -100,14 +87,14 @@ function getOptions(context, node) {
100
87
  capture: Boolean(node.value)
101
88
  };
102
89
  case AST_NODE_TYPES.ObjectExpression: {
103
- const vCapture = match(findProperty(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
90
+ const vCapture = match(findProperty$1(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
104
91
  const value = prop.value;
105
92
  switch (value.type) {
106
93
  case AST_NODE_TYPES.Literal: return Boolean(value.value);
107
94
  default: return Boolean(getStaticValue(value, initialScope)?.value);
108
95
  }
109
96
  }).otherwise(() => false);
110
- const pSignal = findProperty(node.properties, "signal");
97
+ const pSignal = findProperty$1(node.properties, "signal");
111
98
  return {
112
99
  capture: vCapture,
113
100
  signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(context, pSignal.value) : null
@@ -118,6 +105,20 @@ function getOptions(context, node) {
118
105
  }
119
106
  return getOpts(node);
120
107
  }
108
+
109
+ //#endregion
110
+ //#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
111
+ const RULE_NAME$4 = "no-leaked-event-listener";
112
+ function getCallKind$4(node) {
113
+ switch (true) {
114
+ case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.name): return node.callee.name;
115
+ case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.property.name): return node.callee.property.name;
116
+ default: return "other";
117
+ }
118
+ }
119
+ function getFunctionKind$1(node) {
120
+ return getPhaseKindOfFunction(node) ?? "other";
121
+ }
121
122
  var no_leaked_event_listener_default = createRule({
122
123
  meta: {
123
124
  type: "problem",
@@ -128,11 +129,11 @@ var no_leaked_event_listener_default = createRule({
128
129
  },
129
130
  schema: []
130
131
  },
131
- name: RULE_NAME$3,
132
- create: create$3,
132
+ name: RULE_NAME$4,
133
+ create: create$4,
133
134
  defaultOptions: []
134
135
  });
135
- function create$3(context) {
136
+ function create$4(context) {
136
137
  if (!context.sourceCode.text.includes("addEventListener")) return {};
137
138
  if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
138
139
  const fEntries = [];
@@ -176,7 +177,7 @@ function create$3(context) {
176
177
  const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
177
178
  if (fKind == null) return;
178
179
  if (!ComponentPhaseRelevance.has(fKind)) return;
179
- match(getCallKind$3(node)).with("addEventListener", (callKind) => {
180
+ match(getCallKind$4(node)).with("addEventListener", (callKind) => {
180
181
  if (node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.object.type === AST_NODE_TYPES.Identifier && core.isAPIFromReactNative(node.callee.object.name, context.sourceCode.getScope(node))) return;
181
182
  const [type, listener, options] = node.arguments;
182
183
  if (type == null || listener == null) return;
@@ -230,6 +231,174 @@ function create$3(context) {
230
231
  });
231
232
  }
232
233
 
234
+ //#endregion
235
+ //#region src/rules/no-leaked-fetch/lib.ts
236
+ function findProperty(of, named) {
237
+ for (const property of of) {
238
+ if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === named) return property;
239
+ if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
240
+ const found = findProperty(property.argument.properties, named);
241
+ if (found != null) return found;
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function resolveToObjectExpression(context, node) {
247
+ node = Extract.unwrap(node);
248
+ switch (node.type) {
249
+ case AST_NODE_TYPES.ObjectExpression: return node;
250
+ case AST_NODE_TYPES.Identifier: {
251
+ let resolved = resolve(context, node);
252
+ resolved = resolved == null ? null : Extract.unwrap(resolved);
253
+ if (resolved?.type === AST_NODE_TYPES.ObjectExpression) return resolved;
254
+ return null;
255
+ }
256
+ default: return null;
257
+ }
258
+ }
259
+
260
+ //#endregion
261
+ //#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
262
+ const RULE_NAME$3 = "no-leaked-fetch";
263
+ function getCallKind$3(node) {
264
+ switch (true) {
265
+ case node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "fetch": return "fetch";
266
+ case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "fetch": return "fetch";
267
+ case node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "abort": return "abort";
268
+ case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "abort": return "abort";
269
+ default: return "other";
270
+ }
271
+ }
272
+ function getControllerFromSignal(context, node) {
273
+ node = Extract.unwrap(node);
274
+ switch (node.type) {
275
+ case AST_NODE_TYPES.MemberExpression: return {
276
+ controller: node.object,
277
+ isParamSignal: false
278
+ };
279
+ case AST_NODE_TYPES.Identifier: {
280
+ const resolved = resolve(context, node);
281
+ const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
282
+ if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
283
+ controller: resolvedUnwrapped.object,
284
+ isParamSignal: false
285
+ };
286
+ if (resolved != null && Check.isFunction(resolved)) return {
287
+ controller: node,
288
+ isParamSignal: true
289
+ };
290
+ return {
291
+ controller: null,
292
+ isParamSignal: false
293
+ };
294
+ }
295
+ default: return {
296
+ controller: null,
297
+ isParamSignal: false
298
+ };
299
+ }
300
+ }
301
+ function getFetchController(context, node) {
302
+ const [, optionsArg] = node.arguments;
303
+ if (optionsArg == null) return {
304
+ controller: null,
305
+ isParamSignal: false
306
+ };
307
+ const options = resolveToObjectExpression(context, optionsArg);
308
+ if (options == null) return {
309
+ controller: null,
310
+ isParamSignal: false
311
+ };
312
+ const signalProp = findProperty(options.properties, "signal");
313
+ if (signalProp?.type !== AST_NODE_TYPES.Property) return {
314
+ controller: null,
315
+ isParamSignal: false
316
+ };
317
+ return getControllerFromSignal(context, signalProp.value);
318
+ }
319
+ function getAbortController(node) {
320
+ if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.object;
321
+ return null;
322
+ }
323
+ var no_leaked_fetch_default = createRule({
324
+ meta: {
325
+ type: "problem",
326
+ docs: { description: "Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function." },
327
+ messages: {
328
+ expectedAbortController: "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
329
+ expectedAbortInCleanup: "A 'fetch' started in effect must be aborted with 'AbortController.abort' in the cleanup function."
330
+ },
331
+ schema: []
332
+ },
333
+ name: RULE_NAME$3,
334
+ create: create$3,
335
+ defaultOptions: []
336
+ });
337
+ function create$3(context) {
338
+ if (!context.sourceCode.text.includes("fetch")) return {};
339
+ if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
340
+ const fEntries = [];
341
+ const fetchEntries = [];
342
+ const abortEntries = [];
343
+ return merge({
344
+ [":function"](node) {
345
+ const kind = getPhaseKindOfFunction(node) ?? "other";
346
+ fEntries.push({
347
+ kind,
348
+ node
349
+ });
350
+ },
351
+ [":function:exit"]() {
352
+ fEntries.pop();
353
+ },
354
+ ["CallExpression"](node) {
355
+ const fEntry = fEntries.at(-1);
356
+ if (fEntry == null || !ComponentPhaseRelevance.has(fEntry.kind)) return;
357
+ switch (getCallKind$3(node)) {
358
+ case "fetch": {
359
+ if (fEntry.kind !== "setup") return;
360
+ const { controller, isParamSignal } = getFetchController(context, node);
361
+ fetchEntries.push({
362
+ controller,
363
+ isParamSignal,
364
+ node,
365
+ phase: fEntry.kind
366
+ });
367
+ break;
368
+ }
369
+ case "abort": {
370
+ if (fEntry.kind !== "cleanup") return;
371
+ const controller = getAbortController(node);
372
+ if (controller == null) break;
373
+ abortEntries.push({
374
+ controller,
375
+ node,
376
+ phase: fEntry.kind
377
+ });
378
+ break;
379
+ }
380
+ }
381
+ },
382
+ ["Program:exit"]() {
383
+ for (const fEntry of fetchEntries) {
384
+ const controller = fEntry.controller;
385
+ if (controller == null) {
386
+ context.report({
387
+ messageId: "expectedAbortController",
388
+ node: fEntry.node
389
+ });
390
+ continue;
391
+ }
392
+ if (fEntry.isParamSignal) continue;
393
+ if (!abortEntries.some((aEntry) => isAssignmentTargetEqual(context, aEntry.controller, controller))) context.report({
394
+ messageId: "expectedAbortInCleanup",
395
+ node: fEntry.node
396
+ });
397
+ }
398
+ }
399
+ });
400
+ }
401
+
233
402
  //#endregion
234
403
  //#region src/rules/no-leaked-interval/no-leaked-interval.ts
235
404
  const RULE_NAME$2 = "no-leaked-interval";
@@ -482,20 +651,23 @@ const isConditional = isOneOf([
482
651
  AST_NODE_TYPES.LogicalExpression,
483
652
  AST_NODE_TYPES.ConditionalExpression
484
653
  ]);
485
-
486
- //#endregion
487
- //#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
488
- const RULE_NAME$1 = "no-leaked-resize-observer";
489
654
  function isNewResizeObserver(node) {
490
655
  return node?.type === AST_NODE_TYPES.NewExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "ResizeObserver";
491
656
  }
492
657
  function isFromObserver(context, node) {
493
658
  switch (true) {
494
- case node.type === AST_NODE_TYPES.Identifier: return isNewResizeObserver(resolve(context, node));
659
+ case node.type === AST_NODE_TYPES.Identifier: {
660
+ const initNode = resolve(context, node);
661
+ return isNewResizeObserver(initNode == null ? null : Extract.unwrap(initNode));
662
+ }
495
663
  case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
496
664
  default: return false;
497
665
  }
498
666
  }
667
+
668
+ //#endregion
669
+ //#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
670
+ const RULE_NAME$1 = "no-leaked-resize-observer";
499
671
  function getCallKind$1(context, node) {
500
672
  switch (true) {
501
673
  case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(node.callee.name) && isFromObserver(context, node.callee): return node.callee.name;
@@ -731,6 +903,7 @@ const plugin = {
731
903
  },
732
904
  rules: {
733
905
  "no-leaked-event-listener": no_leaked_event_listener_default,
906
+ "no-leaked-fetch": no_leaked_fetch_default,
734
907
  "no-leaked-interval": no_leaked_interval_default,
735
908
  "no-leaked-resize-observer": no_leaked_resize_observer_default,
736
909
  "no-leaked-timeout": no_leaked_timeout_default
@@ -748,6 +921,7 @@ var recommended_exports = /* @__PURE__ */ __exportAll({
748
921
  const name = "react-web-api/recommended";
749
922
  const rules = {
750
923
  "react-web-api/no-leaked-event-listener": "warn",
924
+ "react-web-api/no-leaked-fetch": "warn",
751
925
  "react-web-api/no-leaked-interval": "warn",
752
926
  "react-web-api/no-leaked-resize-observer": "warn",
753
927
  "react-web-api/no-leaked-timeout": "warn"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-web-api",
3
- "version": "5.2.5-next.1",
3
+ "version": "5.3.0-beta.0",
4
4
  "description": "ESLint React's ESLint plugin for interacting with Web APIs",
5
5
  "keywords": [
6
6
  "react",
@@ -43,22 +43,22 @@
43
43
  "@typescript-eslint/utils": "^8.58.2",
44
44
  "birecord": "^0.1.1",
45
45
  "ts-pattern": "^5.9.0",
46
- "@eslint-react/ast": "5.2.5-next.1",
47
- "@eslint-react/eslint": "5.2.5-next.1",
48
- "@eslint-react/core": "5.2.5-next.1",
49
- "@eslint-react/shared": "5.2.5-next.1",
50
- "@eslint-react/var": "5.2.5-next.1"
46
+ "@eslint-react/core": "5.3.0-beta.0",
47
+ "@eslint-react/eslint": "5.3.0-beta.0",
48
+ "@eslint-react/shared": "5.3.0-beta.0",
49
+ "@eslint-react/var": "5.3.0-beta.0",
50
+ "@eslint-react/ast": "5.3.0-beta.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/react": "^19.2.14",
54
54
  "@types/react-dom": "^19.2.3",
55
- "eslint": "^10.2.0",
55
+ "eslint": "^10.2.1",
56
56
  "tsdown": "^0.21.9",
57
- "@local/eff": "3.0.0-beta.72",
58
- "@local/configs": "0.0.0"
57
+ "@local/configs": "0.0.0",
58
+ "@local/eff": "3.0.0-beta.72"
59
59
  },
60
60
  "peerDependencies": {
61
- "eslint": "^10.2.0",
61
+ "eslint": "^10.2.1",
62
62
  "typescript": "*"
63
63
  },
64
64
  "engines": {