eslint-plugin-react-web-api 5.23.4 → 5.24.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.
- package/dist/index.js +585 -506
- package/package.json +7 -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.
|
|
30
|
+
var version = "5.24.0-beta.0";
|
|
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
|
+
"expected-remove-event-listener-in-cleanup": "An 'addEventListener' in '{{effectMethodKind}}' should have a corresponding 'removeEventListener' in its cleanup function.",
|
|
637
|
+
"unexpected-inline-function": "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: "unexpected-inline-function",
|
|
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: "expected-remove-event-listener-in-cleanup",
|
|
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
|
+
"expected-abort-controller": "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
|
|
829
|
+
"expected-abort-in-cleanup": "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: "expected-abort-controller",
|
|
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: "expected-abort-in-cleanup",
|
|
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) {
|
|
@@ -835,9 +914,9 @@ var no_leaked_intersection_observer_default = createRule({
|
|
|
835
914
|
type: "problem",
|
|
836
915
|
docs: { description: "Enforces that every 'IntersectionObserver' created in a component or custom hook has a corresponding 'IntersectionObserver.disconnect()'." },
|
|
837
916
|
messages: {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
917
|
+
"expected-disconnect-in-control-flow": "Dynamically added 'IntersectionObserver.observe' should be cleared all at once using 'IntersectionObserver.disconnect' in the cleanup function.",
|
|
918
|
+
"expected-disconnect-or-unobserve-in-cleanup": "An 'IntersectionObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
|
|
919
|
+
"unexpected-floating-instance": "An 'IntersectionObserver' instance created in component or custom hook must be assigned to a variable for proper cleanup."
|
|
841
920
|
},
|
|
842
921
|
schema: []
|
|
843
922
|
},
|
|
@@ -890,7 +969,7 @@ function create$3(context) {
|
|
|
890
969
|
const id = resolveEnclosingAssignmentTarget(node);
|
|
891
970
|
if (id == null) {
|
|
892
971
|
context.report({
|
|
893
|
-
messageId: "
|
|
972
|
+
messageId: "unexpected-floating-instance",
|
|
894
973
|
node
|
|
895
974
|
});
|
|
896
975
|
return;
|
|
@@ -911,7 +990,7 @@ function create$3(context) {
|
|
|
911
990
|
const isPhaseNode = (node) => node === phaseNode;
|
|
912
991
|
if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
|
|
913
992
|
context.report({
|
|
914
|
-
messageId: "
|
|
993
|
+
messageId: "expected-disconnect-in-control-flow",
|
|
915
994
|
node
|
|
916
995
|
});
|
|
917
996
|
continue;
|
|
@@ -919,7 +998,7 @@ function create$3(context) {
|
|
|
919
998
|
for (const oEntry of matchedOEntries) {
|
|
920
999
|
if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
|
|
921
1000
|
context.report({
|
|
922
|
-
messageId: "
|
|
1001
|
+
messageId: "expected-disconnect-or-unobserve-in-cleanup",
|
|
923
1002
|
node: oEntry.node
|
|
924
1003
|
});
|
|
925
1004
|
}
|
|
@@ -941,8 +1020,8 @@ var no_leaked_interval_default = createRule({
|
|
|
941
1020
|
type: "problem",
|
|
942
1021
|
docs: { description: "Enforces that every 'setInterval' in a component or custom hook has a corresponding 'clearInterval'." },
|
|
943
1022
|
messages: {
|
|
944
|
-
|
|
945
|
-
|
|
1023
|
+
"expected-clear-interval-in-cleanup": "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the cleanup function.",
|
|
1024
|
+
"expected-interval-id": "A 'setInterval' must be assigned to a variable for proper cleanup."
|
|
946
1025
|
},
|
|
947
1026
|
schema: []
|
|
948
1027
|
},
|
|
@@ -964,7 +1043,7 @@ function create$2(context) {
|
|
|
964
1043
|
const intervalIdNode = resolveEnclosingAssignmentTarget(node);
|
|
965
1044
|
if (intervalIdNode == null) {
|
|
966
1045
|
context.report({
|
|
967
|
-
messageId: "
|
|
1046
|
+
messageId: "expected-interval-id",
|
|
968
1047
|
node
|
|
969
1048
|
});
|
|
970
1049
|
return;
|
|
@@ -987,7 +1066,7 @@ function create$2(context) {
|
|
|
987
1066
|
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
988
1067
|
context.report({
|
|
989
1068
|
data: { kind: "useEffect" },
|
|
990
|
-
messageId: "
|
|
1069
|
+
messageId: "expected-clear-interval-in-cleanup",
|
|
991
1070
|
node: sEntry.node
|
|
992
1071
|
});
|
|
993
1072
|
}
|
|
@@ -1028,9 +1107,9 @@ var no_leaked_resize_observer_default = createRule({
|
|
|
1028
1107
|
type: "problem",
|
|
1029
1108
|
docs: { description: "Enforces that every 'ResizeObserver' created in a component or custom hook has a corresponding 'ResizeObserver.disconnect()'." },
|
|
1030
1109
|
messages: {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1110
|
+
"expected-disconnect-in-control-flow": "Dynamically added 'ResizeObserver.observe' should be cleared all at once using 'ResizeObserver.disconnect' in the cleanup function.",
|
|
1111
|
+
"expected-disconnect-or-unobserve-in-cleanup": "A 'ResizeObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
|
|
1112
|
+
"unexpected-floating-instance": "A 'ResizeObserver' instance created in component or custom hook must be assigned to a variable for proper cleanup."
|
|
1034
1113
|
},
|
|
1035
1114
|
schema: []
|
|
1036
1115
|
},
|
|
@@ -1083,7 +1162,7 @@ function create$1(context) {
|
|
|
1083
1162
|
const id = resolveEnclosingAssignmentTarget(node);
|
|
1084
1163
|
if (id == null) {
|
|
1085
1164
|
context.report({
|
|
1086
|
-
messageId: "
|
|
1165
|
+
messageId: "unexpected-floating-instance",
|
|
1087
1166
|
node
|
|
1088
1167
|
});
|
|
1089
1168
|
return;
|
|
@@ -1104,7 +1183,7 @@ function create$1(context) {
|
|
|
1104
1183
|
const isPhaseNode = (node) => node === phaseNode;
|
|
1105
1184
|
if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
|
|
1106
1185
|
context.report({
|
|
1107
|
-
messageId: "
|
|
1186
|
+
messageId: "expected-disconnect-in-control-flow",
|
|
1108
1187
|
node
|
|
1109
1188
|
});
|
|
1110
1189
|
continue;
|
|
@@ -1112,7 +1191,7 @@ function create$1(context) {
|
|
|
1112
1191
|
for (const oEntry of matchedOEntries) {
|
|
1113
1192
|
if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
|
|
1114
1193
|
context.report({
|
|
1115
|
-
messageId: "
|
|
1194
|
+
messageId: "expected-disconnect-or-unobserve-in-cleanup",
|
|
1116
1195
|
node: oEntry.node
|
|
1117
1196
|
});
|
|
1118
1197
|
}
|
|
@@ -1134,8 +1213,8 @@ var no_leaked_timeout_default = createRule({
|
|
|
1134
1213
|
type: "problem",
|
|
1135
1214
|
docs: { description: "Enforces that every 'setTimeout' in a component or custom hook has a corresponding 'clearTimeout'." },
|
|
1136
1215
|
messages: {
|
|
1137
|
-
|
|
1138
|
-
|
|
1216
|
+
"expected-clear-timeout-in-cleanup": "A 'setTimeout' created in '{{ kind }}' must be cleared with 'clearTimeout' in the cleanup function.",
|
|
1217
|
+
"expected-timeout-id": "A 'setTimeout' must be assigned to a variable for proper cleanup."
|
|
1139
1218
|
},
|
|
1140
1219
|
schema: []
|
|
1141
1220
|
},
|
|
@@ -1157,7 +1236,7 @@ function create(context) {
|
|
|
1157
1236
|
const timeoutIdNode = resolveEnclosingAssignmentTarget(node);
|
|
1158
1237
|
if (timeoutIdNode == null) {
|
|
1159
1238
|
context.report({
|
|
1160
|
-
messageId: "
|
|
1239
|
+
messageId: "expected-timeout-id",
|
|
1161
1240
|
node
|
|
1162
1241
|
});
|
|
1163
1242
|
return;
|
|
@@ -1180,7 +1259,7 @@ function create(context) {
|
|
|
1180
1259
|
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
1181
1260
|
context.report({
|
|
1182
1261
|
data: { kind: "useEffect" },
|
|
1183
|
-
messageId: "
|
|
1262
|
+
messageId: "expected-clear-timeout-in-cleanup",
|
|
1184
1263
|
node: sEntry.node
|
|
1185
1264
|
});
|
|
1186
1265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-web-api",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.24.0-beta.0",
|
|
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.
|
|
41
|
-
"@eslint-react/core": "5.
|
|
42
|
-
"@eslint-react/eslint": "5.
|
|
43
|
-
"@eslint-react/shared": "5.
|
|
44
|
-
"@eslint-react/var": "5.
|
|
40
|
+
"@eslint-react/ast": "5.24.0-beta.0",
|
|
41
|
+
"@eslint-react/core": "5.24.0-beta.0",
|
|
42
|
+
"@eslint-react/eslint": "5.24.0-beta.0",
|
|
43
|
+
"@eslint-react/shared": "5.24.0-beta.0",
|
|
44
|
+
"@eslint-react/var": "5.24.0-beta.0",
|
|
45
45
|
"@typescript-eslint/types": "^8.71.0",
|
|
46
46
|
"@typescript-eslint/utils": "^8.71.0",
|
|
47
47
|
"ts-pattern": "^5.9.0"
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@local/configs": "0.0.0",
|
|
51
51
|
"@local/eff": "0.0.0",
|
|
52
|
+
"@local/testkit": "0.0.0",
|
|
52
53
|
"@types/react": "^19.3.0",
|
|
53
54
|
"@types/react-dom": "^19.3.0",
|
|
54
55
|
"dedent": "^1.7.2",
|