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.
- package/dist/index.js +565 -486
- 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.
|
|
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
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
335
|
-
* to right.
|
|
234
|
+
* Returns its input argument unchanged.
|
|
336
235
|
*
|
|
337
236
|
* **When to use**
|
|
338
237
|
*
|
|
339
|
-
* Use to
|
|
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** (
|
|
240
|
+
* **Example** (Returning the same value)
|
|
349
241
|
*
|
|
350
242
|
* ```ts
|
|
351
|
-
* import {
|
|
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
|
-
*
|
|
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
|
+
"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.
|
|
41
|
-
"@eslint-react/core": "5.23.
|
|
42
|
-
"@eslint-react/eslint": "5.23.
|
|
43
|
-
"@eslint-react/shared": "5.23.
|
|
44
|
-
"@eslint-react/var": "5.23.
|
|
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"
|