eslint-plugin-react-web-api 5.23.3 → 5.23.5

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 +565 -486
  2. package/package.json +6 -6
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
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 { isUseEffectCleanupCallback, isUseEffectSetupCallback } from "@eslint-react/core";
4
+ import { getFunctionId, isUseEffectCleanupCallback, isUseEffectSetupCallback } 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";
8
+ import { findVariable, getStaticValue } from "@typescript-eslint/utils/ast-utils";
8
9
  import { P, isMatching, match } from "ts-pattern";
9
- import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
10
10
 
11
11
  //#region \0rolldown/runtime.js
12
12
  var __defProp = Object.defineProperty;
@@ -27,7 +27,7 @@ var __exportAll = (all, no_symbols) => {
27
27
  //#endregion
28
28
  //#region package.json
29
29
  var name$1 = "eslint-plugin-react-web-api";
30
- var version = "5.23.3";
30
+ var version = "5.23.5";
31
31
 
32
32
  //#endregion
33
33
  //#region src/utils/create-rule.ts
@@ -37,502 +37,213 @@ function getDocsUrl(ruleName) {
37
37
  const createRule = ESLintUtils.RuleCreator(getDocsUrl);
38
38
 
39
39
  //#endregion
40
- //#region src/rules/no-leaked-event-listener/lib.ts
41
- function getSignalValueExpression(context, node) {
42
- if (node == null) return null;
43
- switch (node.type) {
44
- case AST_NODE_TYPES.Identifier: {
45
- const resolved = resolveOrigin(context, node);
46
- const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
47
- if (unwrapped != null && Check.isFunction(unwrapped)) return node;
48
- return getSignalValueExpression(context, unwrapped);
40
+ //#region ../../.pkgs/eff/dist/index.js
41
+ function or(a, b) {
42
+ return (data) => a(data) || b(data);
43
+ }
44
+ /**
45
+ * Applies a `pipe` method's variadic arguments to an initial value from left
46
+ * to right.
47
+ *
48
+ * **When to use**
49
+ *
50
+ * Use to implement a custom `.pipe(...)` method from JavaScript's `arguments`
51
+ * object.
52
+ *
53
+ * **Details**
54
+ *
55
+ * This helper is intended for implementing `Pipeable.pipe` methods that
56
+ * receive JavaScript's `arguments` object. With no functions it returns the
57
+ * original value; otherwise it feeds each result into the next function.
58
+ *
59
+ * **Example** (Implementing a pipe method)
60
+ *
61
+ * ```ts
62
+ * import { Pipeable } from "effect"
63
+ *
64
+ * class NumberBox {
65
+ * constructor(readonly value: number) {}
66
+ *
67
+ * pipe(..._fns: ReadonlyArray<(value: number) => number>): number {
68
+ * return Pipeable.pipeArguments(this.value, arguments) as number
69
+ * }
70
+ * }
71
+ *
72
+ * const result = new NumberBox(5).pipe(
73
+ * (n) => n + 2,
74
+ * (n) => n * 3
75
+ * )
76
+ * console.log(result) // 21
77
+ * ```
78
+ *
79
+ * @category combinators
80
+ * @since 2.0.0
81
+ */
82
+ const pipeArguments = (self, args) => {
83
+ switch (args.length) {
84
+ case 0: return self;
85
+ case 1: return args[0](self);
86
+ case 2: return args[1](args[0](self));
87
+ case 3: return args[2](args[1](args[0](self)));
88
+ case 4: return args[3](args[2](args[1](args[0](self))));
89
+ case 5: return args[4](args[3](args[2](args[1](args[0](self)))));
90
+ case 6: return args[5](args[4](args[3](args[2](args[1](args[0](self))))));
91
+ case 7: return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))));
92
+ case 8: return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))));
93
+ case 9: return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))));
94
+ default: {
95
+ let ret = self;
96
+ for (let i = 0, len = args.length; i < len; i++) ret = args[i](ret);
97
+ return ret;
49
98
  }
50
- case AST_NODE_TYPES.MemberExpression: return node;
51
- default: return null;
52
99
  }
53
- }
54
- const defaultOptions = {
55
- capture: false,
56
- signal: null
57
100
  };
58
- function getOptions(context, node) {
59
- function visit(node) {
60
- switch (node.type) {
61
- case AST_NODE_TYPES.Identifier: {
62
- const initNode = resolve(context, node);
63
- if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return visit(initNode);
64
- return defaultOptions;
65
- }
66
- case AST_NODE_TYPES.Literal: return {
67
- ...defaultOptions,
68
- capture: Boolean(node.value)
101
+ /**
102
+ * Reusable prototype that implements `Pipeable.pipe`.
103
+ *
104
+ * **When to use**
105
+ *
106
+ * Use when classes or object prototypes can reuse this value when they need the
107
+ * standard pipe implementation backed by `pipeArguments`.
108
+ *
109
+ * @category prototypes
110
+ * @since 3.15.0
111
+ */
112
+ const Prototype = { pipe() {
113
+ return pipeArguments(this, arguments);
114
+ } };
115
+ /**
116
+ * Provides a base constructor whose instances implement the standard `Pipeable.pipe`
117
+ * method.
118
+ *
119
+ * **When to use**
120
+ *
121
+ * Use when you need to define a class that supports Effect-style method
122
+ * chaining through `.pipe(...)`.
123
+ *
124
+ * @category constructors
125
+ * @since 3.15.0
126
+ */
127
+ const Class = (function() {
128
+ function PipeableBase() {}
129
+ PipeableBase.prototype = Prototype;
130
+ return PipeableBase;
131
+ })();
132
+ /**
133
+ * Provides small helpers for defining and reusing TypeScript functions.
134
+ *
135
+ * The main helpers are `pipe` and `flow` for left-to-right composition and
136
+ * `dual` for APIs that support both direct and pipe-friendly call styles. The
137
+ * module also contains small identity, constant, tuple, type-level, and
138
+ * memoization helpers used across the library.
139
+ *
140
+ * @since 2.0.0
141
+ */
142
+ /**
143
+ * Creates a function that can be called in data-first style or data-last
144
+ * (`pipe`-friendly) style.
145
+ *
146
+ * **When to use**
147
+ *
148
+ * Use to expose one implementation through both direct and `pipe`-friendly
149
+ * call styles.
150
+ *
151
+ * **Details**
152
+ *
153
+ * Pass either the arity of the uncurried function or a predicate that decides
154
+ * whether the current call is data-first. Arity is the common case. Use a
155
+ * predicate when optional arguments make arity ambiguous.
156
+ *
157
+ * **Example** (Selecting data-first or data-last style by arity)
158
+ *
159
+ * ```ts
160
+ * import { Function, pipe } from "effect"
161
+ *
162
+ * const sum = Function.dual<
163
+ * (that: number) => (self: number) => number,
164
+ * (self: number, that: number) => number
165
+ * >(2, (self, that) => self + that)
166
+ *
167
+ * console.log(sum(2, 3)) // 5
168
+ * console.log(pipe(2, sum(3))) // 5
169
+ * ```
170
+ *
171
+ * **Example** (Defining overloads with call signatures)
172
+ *
173
+ * ```ts
174
+ * import { Function, pipe } from "effect"
175
+ *
176
+ * const sum: {
177
+ * (that: number): (self: number) => number
178
+ * (self: number, that: number): number
179
+ * } = Function.dual(2, (self: number, that: number): number => self + that)
180
+ *
181
+ * console.log(sum(2, 3)) // 5
182
+ * console.log(pipe(2, sum(3))) // 5
183
+ * ```
184
+ *
185
+ * **Example** (Selecting data-first or data-last style with a predicate)
186
+ *
187
+ * ```ts
188
+ * import { Function, pipe } from "effect"
189
+ *
190
+ * const sum = Function.dual<
191
+ * (that: number) => (self: number) => number,
192
+ * (self: number, that: number) => number
193
+ * >(
194
+ * (args) => args.length === 2,
195
+ * (self, that) => self + that
196
+ * )
197
+ *
198
+ * console.log(sum(2, 3)) // 5
199
+ * console.log(pipe(2, sum(3))) // 5
200
+ * ```
201
+ *
202
+ * @category combinators
203
+ * @since 2.0.0
204
+ */
205
+ const dual = function(arity, body) {
206
+ if (typeof arity === "function") return function() {
207
+ return arity(arguments) ? body.apply(this, arguments) : ((self) => body(self, ...arguments));
208
+ };
209
+ switch (arity) {
210
+ case 0:
211
+ case 1: throw new RangeError(`Invalid arity ${arity}`);
212
+ case 2: return function(a, b) {
213
+ if (arguments.length >= 2) return body(a, b);
214
+ return function(self) {
215
+ return body(self, a);
69
216
  };
70
- case AST_NODE_TYPES.ObjectExpression: {
71
- const pCapture = Extract.findProperty(node.properties, "capture");
72
- const vCapture = match(pCapture).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
73
- const value = prop.value;
74
- switch (value.type) {
75
- case AST_NODE_TYPES.Literal: return Boolean(value.value);
76
- default: return Boolean(getStaticValue(value, context.sourceCode.getScope(node))?.value);
77
- }
78
- }).otherwise(() => false);
79
- const pSignal = Extract.findProperty(node.properties, "signal");
80
- return {
81
- capture: vCapture,
82
- signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(context, pSignal.value) : null
83
- };
84
- }
85
- default: return defaultOptions;
86
- }
217
+ };
218
+ case 3: return function(a, b, c) {
219
+ if (arguments.length >= 3) return body(a, b, c);
220
+ return function(self) {
221
+ return body(self, a, b);
222
+ };
223
+ };
224
+ default: return function() {
225
+ if (arguments.length >= arity) return body.apply(this, arguments);
226
+ const args = arguments;
227
+ return function(self) {
228
+ return body(self, ...args);
229
+ };
230
+ };
87
231
  }
88
- return visit(node);
89
- }
90
-
91
- //#endregion
92
- //#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
93
- const RULE_NAME$5 = "no-leaked-event-listener";
94
- function getCallKind$5(node) {
95
- const name = Extract.getCalleeName(node);
96
- if (name != null && isMatching(P.union("addEventListener", "removeEventListener"))(name)) return name;
97
- return "other";
98
- }
99
- var no_leaked_event_listener_default = createRule({
100
- meta: {
101
- type: "problem",
102
- docs: { description: "Enforces that every 'addEventListener' in a component or custom hook has a corresponding 'removeEventListener'." },
103
- messages: {
104
- expectedRemoveEventListenerInCleanup: "An 'addEventListener' in '{{effectMethodKind}}' should have a corresponding 'removeEventListener' in its cleanup function.",
105
- unexpectedInlineFunction: "A/an '{{eventMethodKind}}' should not have an inline listener function."
106
- },
107
- schema: []
108
- },
109
- name: RULE_NAME$5,
110
- create: create$5,
111
- defaultOptions: []
112
- });
113
- function create$5(context) {
114
- if (!context.sourceCode.text.includes("addEventListener")) return {};
115
- if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
116
- const aEntries = [];
117
- const rEntries = [];
118
- function isSameObject(a, b) {
119
- switch (true) {
120
- case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
121
- default: return false;
122
- }
123
- }
124
- function isInverseEntry(aEntry, rEntry) {
125
- const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
126
- const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
127
- if (aPhase !== "setup" || rPhase !== "cleanup") return false;
128
- return isSameObject(aCallee, rCallee) && Compare.isEqual(aListener, rListener) && isValueEqual(context, aType, rType) && aCapture === rCapture;
129
- }
130
- function visitInlineFunction(node, callKind, options) {
131
- const listener = node.arguments.at(1);
132
- if (!Check.isFunction(listener)) return;
133
- if (options.signal != null) return;
134
- context.report({
135
- data: { eventMethodKind: callKind },
136
- messageId: "unexpectedInlineFunction",
137
- node: listener
138
- });
139
- }
140
- return {
141
- ["CallExpression"](node) {
142
- const fn = Traverse.findParent(node, (n) => isUseEffectSetupCallback(n) || isUseEffectCleanupCallback(n));
143
- if (fn == null) return;
144
- const fKind = isUseEffectSetupCallback(fn) ? "setup" : "cleanup";
145
- const callee = Extract.unwrap(node.callee);
146
- match(getCallKind$5(node)).with("addEventListener", (callKind) => {
147
- if (callee.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(callee.object) && isInitializedFromReactNative(callee.object.name, context.sourceCode.getScope(node))) return;
148
- const [type, listener, options] = node.arguments;
149
- if (type == null || listener == null) return;
150
- const opts = options == null ? defaultOptions : getOptions(context, options);
151
- visitInlineFunction(node, callKind, opts);
152
- aEntries.push({
153
- ...opts,
154
- type,
155
- callee,
156
- listener,
157
- method: "addEventListener",
158
- node,
159
- phase: fKind
160
- });
161
- }).with("removeEventListener", (callKind) => {
162
- const [type, listener, options] = node.arguments;
163
- if (type == null || listener == null) return;
164
- const opts = options == null ? defaultOptions : getOptions(context, options);
165
- visitInlineFunction(node, callKind, opts);
166
- rEntries.push({
167
- type,
168
- callee,
169
- capture: opts.capture,
170
- listener,
171
- method: "removeEventListener",
172
- node,
173
- phase: fKind
174
- });
175
- }).otherwise(() => null);
176
- },
177
- ["Program:exit"]() {
178
- for (const aEntry of aEntries) {
179
- if (aEntry.signal != null) continue;
180
- if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
181
- context.report({
182
- data: { effectMethodKind: "useEffect" },
183
- messageId: "expectedRemoveEventListenerInCleanup",
184
- node: aEntry.node
185
- });
186
- }
187
- }
188
- };
189
- }
190
-
191
- //#endregion
192
- //#region src/rules/no-leaked-fetch/lib.ts
193
- function resolveToObjectExpression(context, node) {
194
- node = Extract.unwrap(node);
195
- switch (node.type) {
196
- case AST_NODE_TYPES.ObjectExpression: return node;
197
- case AST_NODE_TYPES.Identifier: {
198
- let resolved = resolve(context, node);
199
- resolved = resolved == null ? null : Extract.unwrap(resolved);
200
- if (resolved?.type === AST_NODE_TYPES.ObjectExpression) return resolved;
201
- return null;
202
- }
203
- default: return null;
204
- }
205
- }
206
-
207
- //#endregion
208
- //#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
209
- const RULE_NAME$4 = "no-leaked-fetch";
210
- function getCallKind$4(node) {
211
- const name = Extract.getCalleeName(node);
212
- if (name != null && isMatching(P.union("fetch", "abort"))(name)) return name;
213
- return "other";
214
- }
215
- function getControllerFromSignal(context, node) {
216
- node = Extract.unwrap(node);
217
- switch (node.type) {
218
- case AST_NODE_TYPES.MemberExpression: return {
219
- controller: node.object,
220
- isParamSignal: false
221
- };
222
- case AST_NODE_TYPES.Identifier: {
223
- const resolved = resolveOrigin(context, node);
224
- const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
225
- if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
226
- controller: resolvedUnwrapped.object,
227
- isParamSignal: false
228
- };
229
- if (resolved != null && Check.isFunction(resolved)) return {
230
- controller: node,
231
- isParamSignal: true
232
- };
233
- return {
234
- controller: null,
235
- isParamSignal: false
236
- };
237
- }
238
- default: return {
239
- controller: null,
240
- isParamSignal: false
241
- };
242
- }
243
- }
244
- function getFetchController(context, node) {
245
- const [, optionsArg] = node.arguments;
246
- if (optionsArg == null) return {
247
- controller: null,
248
- isParamSignal: false
249
- };
250
- const options = resolveToObjectExpression(context, optionsArg);
251
- if (options == null) return {
252
- controller: null,
253
- isParamSignal: false
254
- };
255
- const signalProp = Extract.findProperty(options.properties, "signal");
256
- if (signalProp?.type !== AST_NODE_TYPES.Property) return {
257
- controller: null,
258
- isParamSignal: false
259
- };
260
- return getControllerFromSignal(context, signalProp.value);
261
- }
262
- function getAbortController(node) {
263
- const callee = Extract.unwrap(node.callee);
264
- if (callee.type === AST_NODE_TYPES.MemberExpression) return callee.object;
265
- return null;
266
- }
267
- var no_leaked_fetch_default = createRule({
268
- meta: {
269
- type: "problem",
270
- docs: { description: "Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function." },
271
- messages: {
272
- expectedAbortController: "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
273
- expectedAbortInCleanup: "A 'fetch' started in effect must be aborted with 'AbortController.abort' in the cleanup function."
274
- },
275
- schema: []
276
- },
277
- name: RULE_NAME$4,
278
- create: create$4,
279
- defaultOptions: []
280
- });
281
- function create$4(context) {
282
- if (!context.sourceCode.text.includes("fetch")) return {};
283
- if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
284
- const fetchEntries = [];
285
- const abortEntries = [];
286
- return {
287
- ["CallExpression"](node) {
288
- match(getCallKind$4(node)).with("fetch", () => {
289
- const fn = Traverse.findParent(node, Check.isFunction);
290
- if (fn == null || !isUseEffectSetupCallback(fn)) return;
291
- const { controller, isParamSignal } = getFetchController(context, node);
292
- fetchEntries.push({
293
- controller,
294
- isParamSignal,
295
- node
296
- });
297
- }).with("abort", () => {
298
- const fn = Traverse.findParent(node, (n) => isUseEffectSetupCallback(n) || isUseEffectCleanupCallback(n));
299
- if (fn == null || !isUseEffectCleanupCallback(fn)) return;
300
- const controller = getAbortController(node);
301
- if (controller == null) return;
302
- abortEntries.push({
303
- controller,
304
- node
305
- });
306
- }).otherwise(() => null);
307
- },
308
- ["Program:exit"]() {
309
- for (const fEntry of fetchEntries) {
310
- const controller = fEntry.controller;
311
- if (controller == null) {
312
- context.report({
313
- messageId: "expectedAbortController",
314
- node: fEntry.node
315
- });
316
- continue;
317
- }
318
- if (fEntry.isParamSignal) continue;
319
- if (!abortEntries.some((aEntry) => isAssignmentTargetEqual(context, aEntry.controller, controller))) context.report({
320
- messageId: "expectedAbortInCleanup",
321
- node: fEntry.node
322
- });
323
- }
324
- }
325
- };
326
- }
327
-
328
- //#endregion
329
- //#region ../../.pkgs/eff/dist/index.js
330
- function or(a, b) {
331
- return (data) => a(data) || b(data);
332
- }
232
+ };
333
233
  /**
334
- * Applies a `pipe` method's variadic arguments to an initial value from left
335
- * to right.
234
+ * Returns its input argument unchanged.
336
235
  *
337
236
  * **When to use**
338
237
  *
339
- * Use to implement a custom `.pipe(...)` method from JavaScript's `arguments`
340
- * object.
341
- *
342
- * **Details**
343
- *
344
- * This helper is intended for implementing `Pipeable.pipe` methods that
345
- * receive JavaScript's `arguments` object. With no functions it returns the
346
- * original value; otherwise it feeds each result into the next function.
238
+ * Use to return a value unchanged where a function is required.
347
239
  *
348
- * **Example** (Implementing a pipe method)
240
+ * **Example** (Returning the same value)
349
241
  *
350
242
  * ```ts
351
- * import { Pipeable } from "effect"
352
- *
353
- * class NumberBox {
354
- * constructor(readonly value: number) {}
355
- *
356
- * pipe(..._fns: ReadonlyArray<(value: number) => number>): number {
357
- * return Pipeable.pipeArguments(this.value, arguments) as number
358
- * }
359
- * }
243
+ * import { identity } from "effect"
244
+ * import * as assert from "node:assert"
360
245
  *
361
- * const result = new NumberBox(5).pipe(
362
- * (n) => n + 2,
363
- * (n) => n * 3
364
- * )
365
- * console.log(result) // 21
366
- * ```
367
- *
368
- * @category combinators
369
- * @since 2.0.0
370
- */
371
- const pipeArguments = (self, args) => {
372
- switch (args.length) {
373
- case 0: return self;
374
- case 1: return args[0](self);
375
- case 2: return args[1](args[0](self));
376
- case 3: return args[2](args[1](args[0](self)));
377
- case 4: return args[3](args[2](args[1](args[0](self))));
378
- case 5: return args[4](args[3](args[2](args[1](args[0](self)))));
379
- case 6: return args[5](args[4](args[3](args[2](args[1](args[0](self))))));
380
- case 7: return args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))));
381
- case 8: return args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self))))))));
382
- case 9: return args[8](args[7](args[6](args[5](args[4](args[3](args[2](args[1](args[0](self)))))))));
383
- default: {
384
- let ret = self;
385
- for (let i = 0, len = args.length; i < len; i++) ret = args[i](ret);
386
- return ret;
387
- }
388
- }
389
- };
390
- /**
391
- * Reusable prototype that implements `Pipeable.pipe`.
392
- *
393
- * **When to use**
394
- *
395
- * Use when classes or object prototypes can reuse this value when they need the
396
- * standard pipe implementation backed by `pipeArguments`.
397
- *
398
- * @category prototypes
399
- * @since 3.15.0
400
- */
401
- const Prototype = { pipe() {
402
- return pipeArguments(this, arguments);
403
- } };
404
- /**
405
- * Provides a base constructor whose instances implement the standard `Pipeable.pipe`
406
- * method.
407
- *
408
- * **When to use**
409
- *
410
- * Use when you need to define a class that supports Effect-style method
411
- * chaining through `.pipe(...)`.
412
- *
413
- * @category constructors
414
- * @since 3.15.0
415
- */
416
- const Class = (function() {
417
- function PipeableBase() {}
418
- PipeableBase.prototype = Prototype;
419
- return PipeableBase;
420
- })();
421
- /**
422
- * Provides small helpers for defining and reusing TypeScript functions.
423
- *
424
- * The main helpers are `pipe` and `flow` for left-to-right composition and
425
- * `dual` for APIs that support both direct and pipe-friendly call styles. The
426
- * module also contains small identity, constant, tuple, type-level, and
427
- * memoization helpers used across the library.
428
- *
429
- * @since 2.0.0
430
- */
431
- /**
432
- * Creates a function that can be called in data-first style or data-last
433
- * (`pipe`-friendly) style.
434
- *
435
- * **When to use**
436
- *
437
- * Use to expose one implementation through both direct and `pipe`-friendly
438
- * call styles.
439
- *
440
- * **Details**
441
- *
442
- * Pass either the arity of the uncurried function or a predicate that decides
443
- * whether the current call is data-first. Arity is the common case. Use a
444
- * predicate when optional arguments make arity ambiguous.
445
- *
446
- * **Example** (Selecting data-first or data-last style by arity)
447
- *
448
- * ```ts
449
- * import { Function, pipe } from "effect"
450
- *
451
- * const sum = Function.dual<
452
- * (that: number) => (self: number) => number,
453
- * (self: number, that: number) => number
454
- * >(2, (self, that) => self + that)
455
- *
456
- * console.log(sum(2, 3)) // 5
457
- * console.log(pipe(2, sum(3))) // 5
458
- * ```
459
- *
460
- * **Example** (Defining overloads with call signatures)
461
- *
462
- * ```ts
463
- * import { Function, pipe } from "effect"
464
- *
465
- * const sum: {
466
- * (that: number): (self: number) => number
467
- * (self: number, that: number): number
468
- * } = Function.dual(2, (self: number, that: number): number => self + that)
469
- *
470
- * console.log(sum(2, 3)) // 5
471
- * console.log(pipe(2, sum(3))) // 5
472
- * ```
473
- *
474
- * **Example** (Selecting data-first or data-last style with a predicate)
475
- *
476
- * ```ts
477
- * import { Function, pipe } from "effect"
478
- *
479
- * const sum = Function.dual<
480
- * (that: number) => (self: number) => number,
481
- * (self: number, that: number) => number
482
- * >(
483
- * (args) => args.length === 2,
484
- * (self, that) => self + that
485
- * )
486
- *
487
- * console.log(sum(2, 3)) // 5
488
- * console.log(pipe(2, sum(3))) // 5
489
- * ```
490
- *
491
- * @category combinators
492
- * @since 2.0.0
493
- */
494
- const dual = function(arity, body) {
495
- if (typeof arity === "function") return function() {
496
- return arity(arguments) ? body.apply(this, arguments) : ((self) => body(self, ...arguments));
497
- };
498
- switch (arity) {
499
- case 0:
500
- case 1: throw new RangeError(`Invalid arity ${arity}`);
501
- case 2: return function(a, b) {
502
- if (arguments.length >= 2) return body(a, b);
503
- return function(self) {
504
- return body(self, a);
505
- };
506
- };
507
- case 3: return function(a, b, c) {
508
- if (arguments.length >= 3) return body(a, b, c);
509
- return function(self) {
510
- return body(self, a, b);
511
- };
512
- };
513
- default: return function() {
514
- if (arguments.length >= arity) return body.apply(this, arguments);
515
- const args = arguments;
516
- return function(self) {
517
- return body(self, ...args);
518
- };
519
- };
520
- }
521
- };
522
- /**
523
- * Returns its input argument unchanged.
524
- *
525
- * **When to use**
526
- *
527
- * Use to return a value unchanged where a function is required.
528
- *
529
- * **Example** (Returning the same value)
530
- *
531
- * ```ts
532
- * import { identity } from "effect"
533
- * import * as assert from "node:assert"
534
- *
535
- * assert.deepStrictEqual(identity(5), 5)
246
+ * assert.deepStrictEqual(identity(5), 5)
536
247
  * ```
537
248
  *
538
249
  * @category combinators
@@ -749,6 +460,12 @@ const absurd = (_) => {
749
460
  * @since 2.0.0
750
461
  */
751
462
  const hole = cast(absurd);
463
+ function getOrInsertComputed(map, key, callback) {
464
+ if (map.has(key)) return map.get(key);
465
+ const value = callback(key);
466
+ map.set(key, value);
467
+ return value;
468
+ }
752
469
  /**
753
470
  * Drops the longest prefix of elements from an array that satisfy the given predicate.
754
471
  *
@@ -802,6 +519,368 @@ const takeWhile = dual(2, (xs, pred) => {
802
519
  return xs.slice(0, idx);
803
520
  });
804
521
 
522
+ //#endregion
523
+ //#region src/rules/no-leaked-event-listener/lib.ts
524
+ function getSignalValueExpression(context, node) {
525
+ if (node == null) return null;
526
+ switch (node.type) {
527
+ case AST_NODE_TYPES.Identifier: {
528
+ const resolved = resolveOrigin(context, node);
529
+ const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
530
+ if (unwrapped != null && Check.isFunction(unwrapped)) return node;
531
+ return getSignalValueExpression(context, unwrapped);
532
+ }
533
+ case AST_NODE_TYPES.MemberExpression: return node;
534
+ default: return null;
535
+ }
536
+ }
537
+ const defaultOptions = {
538
+ capture: false,
539
+ signal: null
540
+ };
541
+ function getOptions(context, node) {
542
+ function visit(node) {
543
+ switch (node.type) {
544
+ case AST_NODE_TYPES.Identifier: {
545
+ const initNode = resolve(context, node);
546
+ if (initNode?.type === AST_NODE_TYPES.ObjectExpression) return visit(initNode);
547
+ return defaultOptions;
548
+ }
549
+ case AST_NODE_TYPES.Literal: return {
550
+ ...defaultOptions,
551
+ capture: Boolean(node.value)
552
+ };
553
+ case AST_NODE_TYPES.ObjectExpression: {
554
+ const pCapture = Extract.findProperty(node.properties, "capture");
555
+ const vCapture = match(pCapture).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
556
+ const value = prop.value;
557
+ switch (value.type) {
558
+ case AST_NODE_TYPES.Literal: return Boolean(value.value);
559
+ default: return Boolean(getStaticValue(value, context.sourceCode.getScope(node))?.value);
560
+ }
561
+ }).otherwise(() => false);
562
+ const pSignal = Extract.findProperty(node.properties, "signal");
563
+ return {
564
+ capture: vCapture,
565
+ signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(context, pSignal.value) : null
566
+ };
567
+ }
568
+ default: return defaultOptions;
569
+ }
570
+ }
571
+ return visit(node);
572
+ }
573
+
574
+ //#endregion
575
+ //#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
576
+ const RULE_NAME$5 = "no-leaked-event-listener";
577
+ function getCallKind$5(node) {
578
+ const name = Extract.getCalleeName(node);
579
+ if (name != null && isMatching(P.union("addEventListener", "removeEventListener"))(name)) return name;
580
+ return "other";
581
+ }
582
+ /**
583
+ * Resolve the callee of a call inside an effect cleanup to the function it refers to.
584
+ * A bare call (`stop()`) resolves through the scope to the local function; a member call
585
+ * (`handlers.stop()`) only counts when the receiver resolves to a local object literal
586
+ * whose matching property is a function — an unresolvable member call says nothing about
587
+ * a same-named local function. Falls back to the callee name when a bare call cannot be
588
+ * resolved locally.
589
+ * @param context The ESLint rule context
590
+ * @param node The call expression to resolve
591
+ * @returns The resolved function or the callee name, or null when neither applies
592
+ */
593
+ function resolveCleanupCallee(context, node) {
594
+ const callee = Extract.unwrap(node.callee);
595
+ switch (callee.type) {
596
+ case AST_NODE_TYPES.Identifier: {
597
+ const defNode = findVariable(context.sourceCode.getScope(node), callee)?.defs.at(-1)?.node;
598
+ 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;
599
+ return fn != null && Check.isFunction(fn) ? {
600
+ kind: "function",
601
+ node: fn
602
+ } : {
603
+ kind: "name",
604
+ name: callee.name
605
+ };
606
+ }
607
+ case AST_NODE_TYPES.MemberExpression: {
608
+ if (callee.computed) return null;
609
+ const property = Extract.unwrap(callee.property);
610
+ const object = Extract.unwrap(callee.object);
611
+ if (property.type !== AST_NODE_TYPES.Identifier || object.type !== AST_NODE_TYPES.Identifier) return null;
612
+ const defNode = findVariable(context.sourceCode.getScope(node), object)?.defs.at(-1)?.node;
613
+ if (defNode?.type !== AST_NODE_TYPES.VariableDeclarator || defNode.init == null) return null;
614
+ const init = Extract.unwrap(defNode.init);
615
+ if (init.type !== AST_NODE_TYPES.ObjectExpression) return null;
616
+ for (const prop of init.properties) {
617
+ if (prop.type !== AST_NODE_TYPES.Property || prop.computed) continue;
618
+ const key = Extract.unwrap(prop.key);
619
+ if ((key.type === AST_NODE_TYPES.Identifier ? key.name : key.type === AST_NODE_TYPES.Literal && typeof key.value === "string" ? key.value : null) !== property.name) continue;
620
+ const value = Extract.unwrap(prop.value);
621
+ if (Check.isFunction(value)) return {
622
+ kind: "function",
623
+ node: value
624
+ };
625
+ }
626
+ return null;
627
+ }
628
+ default: return null;
629
+ }
630
+ }
631
+ var no_leaked_event_listener_default = createRule({
632
+ meta: {
633
+ type: "problem",
634
+ docs: { description: "Enforces that every 'addEventListener' in a component or custom hook has a corresponding 'removeEventListener'." },
635
+ messages: {
636
+ expectedRemoveEventListenerInCleanup: "An 'addEventListener' in '{{effectMethodKind}}' should have a corresponding 'removeEventListener' in its cleanup function.",
637
+ unexpectedInlineFunction: "A/an '{{eventMethodKind}}' should not have an inline listener function."
638
+ },
639
+ schema: []
640
+ },
641
+ name: RULE_NAME$5,
642
+ create: create$5,
643
+ defaultOptions: []
644
+ });
645
+ function create$5(context) {
646
+ if (!context.sourceCode.text.includes("addEventListener")) return {};
647
+ if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
648
+ const aEntries = [];
649
+ const rEntries = [];
650
+ const cleanupCallees = /* @__PURE__ */ new Map();
651
+ function isSameObject(a, b) {
652
+ switch (true) {
653
+ case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
654
+ default: return false;
655
+ }
656
+ }
657
+ function isInverseEntry(aEntry, rEntry) {
658
+ const { type: aType, callee: aCallee, capture: aCapture, effect: aEffect, listener: aListener, phase: aPhase } = aEntry;
659
+ const { type: rType, callee: rCallee, capture: rCapture, effect: rEffect, listener: rListener, phase: rPhase } = rEntry;
660
+ if (aPhase !== "setup") return false;
661
+ if (!isSameObject(aCallee, rCallee) || !Compare.isEqual(aListener, rListener) || !isValueEqual(context, aType, rType) || aCapture !== rCapture) return false;
662
+ if (rPhase === "cleanup") return true;
663
+ if (aEffect == null || aEffect !== rEffect) return false;
664
+ const enclosingFunction = Traverse.findParent(rEntry.node, Check.isFunction);
665
+ if (enclosingFunction == null) return false;
666
+ const callees = cleanupCallees.get(aEffect);
667
+ if (callees == null) return false;
668
+ if (callees.functions.has(enclosingFunction)) return true;
669
+ const id = getFunctionId(enclosingFunction);
670
+ return id != null && id.type === AST_NODE_TYPES.Identifier && callees.names.has(id.name);
671
+ }
672
+ function visitInlineFunction(node, callKind, options) {
673
+ const listener = node.arguments.at(1);
674
+ if (!Check.isFunction(listener)) return;
675
+ if (options.signal != null) return;
676
+ context.report({
677
+ data: { eventMethodKind: callKind },
678
+ messageId: "unexpectedInlineFunction",
679
+ node: listener
680
+ });
681
+ }
682
+ return {
683
+ ["CallExpression"](node) {
684
+ const fn = Traverse.findParent(node, (n) => isUseEffectSetupCallback(n) || isUseEffectCleanupCallback(n));
685
+ if (fn == null) return;
686
+ const fKind = isUseEffectSetupCallback(fn) ? "setup" : "cleanup";
687
+ const setupFn = fKind === "setup" ? fn : Traverse.findParent(fn, Check.isFunction);
688
+ const effect = setupFn == null ? null : Extract.unwrap(setupFn).parent ?? null;
689
+ const callee = Extract.unwrap(node.callee);
690
+ match(getCallKind$5(node)).with("addEventListener", (callKind) => {
691
+ if (callee.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(callee.object) && isInitializedFromReactNative(callee.object.name, context.sourceCode.getScope(node))) return;
692
+ const [type, listener, options] = node.arguments;
693
+ if (type == null || listener == null) return;
694
+ const opts = options == null ? defaultOptions : getOptions(context, options);
695
+ visitInlineFunction(node, callKind, opts);
696
+ aEntries.push({
697
+ ...opts,
698
+ type,
699
+ callee,
700
+ effect,
701
+ listener,
702
+ method: "addEventListener",
703
+ node,
704
+ phase: fKind
705
+ });
706
+ }).with("removeEventListener", (callKind) => {
707
+ const [type, listener, options] = node.arguments;
708
+ if (type == null || listener == null) return;
709
+ const opts = options == null ? defaultOptions : getOptions(context, options);
710
+ visitInlineFunction(node, callKind, opts);
711
+ rEntries.push({
712
+ type,
713
+ callee,
714
+ capture: opts.capture,
715
+ effect,
716
+ listener,
717
+ method: "removeEventListener",
718
+ node,
719
+ phase: fKind
720
+ });
721
+ }).with("other", () => {
722
+ if (fKind !== "cleanup" || effect == null) return;
723
+ const callee = resolveCleanupCallee(context, node);
724
+ if (callee == null) return;
725
+ const callees = getOrInsertComputed(cleanupCallees, effect, () => ({
726
+ functions: /* @__PURE__ */ new Set(),
727
+ names: /* @__PURE__ */ new Set()
728
+ }));
729
+ if (callee.kind === "function") callees.functions.add(callee.node);
730
+ else callees.names.add(callee.name);
731
+ }).otherwise(() => null);
732
+ },
733
+ ["Program:exit"]() {
734
+ for (const aEntry of aEntries) {
735
+ if (aEntry.signal != null) continue;
736
+ if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
737
+ context.report({
738
+ data: { effectMethodKind: "useEffect" },
739
+ messageId: "expectedRemoveEventListenerInCleanup",
740
+ node: aEntry.node
741
+ });
742
+ }
743
+ }
744
+ };
745
+ }
746
+
747
+ //#endregion
748
+ //#region src/rules/no-leaked-fetch/lib.ts
749
+ function resolveToObjectExpression(context, node) {
750
+ node = Extract.unwrap(node);
751
+ switch (node.type) {
752
+ case AST_NODE_TYPES.ObjectExpression: return node;
753
+ case AST_NODE_TYPES.Identifier: {
754
+ let resolved = resolve(context, node);
755
+ resolved = resolved == null ? null : Extract.unwrap(resolved);
756
+ if (resolved?.type === AST_NODE_TYPES.ObjectExpression) return resolved;
757
+ return null;
758
+ }
759
+ default: return null;
760
+ }
761
+ }
762
+
763
+ //#endregion
764
+ //#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
765
+ const RULE_NAME$4 = "no-leaked-fetch";
766
+ function getCallKind$4(node) {
767
+ const name = Extract.getCalleeName(node);
768
+ if (name != null && isMatching(P.union("fetch", "abort"))(name)) return name;
769
+ return "other";
770
+ }
771
+ function getControllerFromSignal(context, node) {
772
+ node = Extract.unwrap(node);
773
+ switch (node.type) {
774
+ case AST_NODE_TYPES.MemberExpression: return {
775
+ controller: node.object,
776
+ isParamSignal: false
777
+ };
778
+ case AST_NODE_TYPES.Identifier: {
779
+ const resolved = resolveOrigin(context, node);
780
+ const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
781
+ if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
782
+ controller: resolvedUnwrapped.object,
783
+ isParamSignal: false
784
+ };
785
+ if (resolved != null && Check.isFunction(resolved)) return {
786
+ controller: node,
787
+ isParamSignal: true
788
+ };
789
+ return {
790
+ controller: null,
791
+ isParamSignal: false
792
+ };
793
+ }
794
+ default: return {
795
+ controller: null,
796
+ isParamSignal: false
797
+ };
798
+ }
799
+ }
800
+ function getFetchController(context, node) {
801
+ const [, optionsArg] = node.arguments;
802
+ if (optionsArg == null) return {
803
+ controller: null,
804
+ isParamSignal: false
805
+ };
806
+ const options = resolveToObjectExpression(context, optionsArg);
807
+ if (options == null) return {
808
+ controller: null,
809
+ isParamSignal: false
810
+ };
811
+ const signalProp = Extract.findProperty(options.properties, "signal");
812
+ if (signalProp?.type !== AST_NODE_TYPES.Property) return {
813
+ controller: null,
814
+ isParamSignal: false
815
+ };
816
+ return getControllerFromSignal(context, signalProp.value);
817
+ }
818
+ function getAbortController(node) {
819
+ const callee = Extract.unwrap(node.callee);
820
+ if (callee.type === AST_NODE_TYPES.MemberExpression) return callee.object;
821
+ return null;
822
+ }
823
+ var no_leaked_fetch_default = createRule({
824
+ meta: {
825
+ type: "problem",
826
+ docs: { description: "Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function." },
827
+ messages: {
828
+ expectedAbortController: "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
829
+ expectedAbortInCleanup: "A 'fetch' started in effect must be aborted with 'AbortController.abort' in the cleanup function."
830
+ },
831
+ schema: []
832
+ },
833
+ name: RULE_NAME$4,
834
+ create: create$4,
835
+ defaultOptions: []
836
+ });
837
+ function create$4(context) {
838
+ if (!context.sourceCode.text.includes("fetch")) return {};
839
+ if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
840
+ const fetchEntries = [];
841
+ const abortEntries = [];
842
+ return {
843
+ ["CallExpression"](node) {
844
+ match(getCallKind$4(node)).with("fetch", () => {
845
+ const fn = Traverse.findParent(node, Check.isFunction);
846
+ if (fn == null || !isUseEffectSetupCallback(fn)) return;
847
+ const { controller, isParamSignal } = getFetchController(context, node);
848
+ fetchEntries.push({
849
+ controller,
850
+ isParamSignal,
851
+ node
852
+ });
853
+ }).with("abort", () => {
854
+ const fn = Traverse.findParent(node, (n) => isUseEffectSetupCallback(n) || isUseEffectCleanupCallback(n));
855
+ if (fn == null || !isUseEffectCleanupCallback(fn)) return;
856
+ const controller = getAbortController(node);
857
+ if (controller == null) return;
858
+ abortEntries.push({
859
+ controller,
860
+ node
861
+ });
862
+ }).otherwise(() => null);
863
+ },
864
+ ["Program:exit"]() {
865
+ for (const fEntry of fetchEntries) {
866
+ const controller = fEntry.controller;
867
+ if (controller == null) {
868
+ context.report({
869
+ messageId: "expectedAbortController",
870
+ node: fEntry.node
871
+ });
872
+ continue;
873
+ }
874
+ if (fEntry.isParamSignal) continue;
875
+ if (!abortEntries.some((aEntry) => isAssignmentTargetEqual(context, aEntry.controller, controller))) context.report({
876
+ messageId: "expectedAbortInCleanup",
877
+ node: fEntry.node
878
+ });
879
+ }
880
+ }
881
+ };
882
+ }
883
+
805
884
  //#endregion
806
885
  //#region src/rules/no-leaked-intersection-observer/lib.ts
807
886
  function isNewObserver$1(node, name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-react-web-api",
3
- "version": "5.23.3",
3
+ "version": "5.23.5",
4
4
  "description": "ESLint React's ESLint plugin for interacting with Web APIs",
5
5
  "keywords": [
6
6
  "react",
@@ -37,11 +37,11 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
- "@eslint-react/ast": "5.23.3",
41
- "@eslint-react/core": "5.23.3",
42
- "@eslint-react/eslint": "5.23.3",
43
- "@eslint-react/shared": "5.23.3",
44
- "@eslint-react/var": "5.23.3",
40
+ "@eslint-react/ast": "5.23.5",
41
+ "@eslint-react/core": "5.23.5",
42
+ "@eslint-react/eslint": "5.23.5",
43
+ "@eslint-react/shared": "5.23.5",
44
+ "@eslint-react/var": "5.23.5",
45
45
  "@typescript-eslint/types": "^8.71.0",
46
46
  "@typescript-eslint/utils": "^8.71.0",
47
47
  "ts-pattern": "^5.9.0"