eslint-plugin-react-web-api 2.0.0-next.58 → 2.0.0-next.61
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.d.ts +19 -44
- package/dist/index.js +621 -684
- package/package.json +16 -17
package/dist/index.js
CHANGED
|
@@ -1,737 +1,674 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as AST from
|
|
3
|
-
import * as
|
|
4
|
-
import {
|
|
5
|
-
import * as VAR from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from
|
|
1
|
+
import { DEFAULT_ESLINT_REACT_SETTINGS, getConfigAdapters, getDocsUrl } from "@eslint-react/shared";
|
|
2
|
+
import * as AST from "@eslint-react/ast";
|
|
3
|
+
import * as ER from "@eslint-react/core";
|
|
4
|
+
import { or, unit } from "@eslint-react/eff";
|
|
5
|
+
import * as VAR from "@eslint-react/var";
|
|
6
|
+
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
|
|
7
|
+
import { P, isMatching, match } from "ts-pattern";
|
|
8
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES$1 } from "@typescript-eslint/types";
|
|
9
9
|
|
|
10
|
+
//#region rolldown:runtime
|
|
10
11
|
var __defProp = Object.defineProperty;
|
|
11
12
|
var __export = (target, all) => {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
for (var name$2 in all) __defProp(target, name$2, {
|
|
14
|
+
get: all[name$2],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
14
17
|
};
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/configs/recommended.ts
|
|
17
21
|
var recommended_exports = {};
|
|
18
22
|
__export(recommended_exports, {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
name: () => name$1,
|
|
24
|
+
rules: () => rules,
|
|
25
|
+
settings: () => settings
|
|
22
26
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
var settings = {
|
|
31
|
-
"react-x": DEFAULT_ESLINT_REACT_SETTINGS
|
|
27
|
+
const name$1 = "react-web-api/recommended";
|
|
28
|
+
const rules = {
|
|
29
|
+
"react-web-api/no-leaked-event-listener": "warn",
|
|
30
|
+
"react-web-api/no-leaked-interval": "warn",
|
|
31
|
+
"react-web-api/no-leaked-resize-observer": "warn",
|
|
32
|
+
"react-web-api/no-leaked-timeout": "warn"
|
|
32
33
|
};
|
|
34
|
+
const settings = { "react-x": DEFAULT_ESLINT_REACT_SETTINGS };
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region package.json
|
|
38
|
+
var name = "eslint-plugin-react-web-api";
|
|
39
|
+
var version = "2.0.0-next.61";
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/utils/create-rule.ts
|
|
43
|
+
const createRule = ESLintUtils.RuleCreator(getDocsUrl("web-api"));
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/utils/get-phase-kind-of-function.ts
|
|
38
47
|
function getPhaseKindOfFunction(node) {
|
|
39
|
-
|
|
48
|
+
return match(node).when(isFunctionOfUseEffectSetup, () => "setup").when(isFunctionOfUseEffectCleanup, () => "cleanup").when(isFunctionOfComponentDidMount, () => "mount").when(isFunctionOfComponentWillUnmount, () => "unmount").otherwise(() => null);
|
|
40
49
|
}
|
|
41
50
|
function isFunctionOfUseEffectSetup(node) {
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
if (node == null) return false;
|
|
52
|
+
return node.parent?.type === AST_NODE_TYPES$1.CallExpression && node.parent.callee !== node && node.parent.callee.type === AST_NODE_TYPES$1.Identifier && node.parent.arguments.at(0) === node && ER.isUseEffectCallLoose(node.parent);
|
|
44
53
|
}
|
|
45
54
|
function isFunctionOfUseEffectCleanup(node) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
if (node == null) return false;
|
|
56
|
+
const pReturn = AST.findParentNode(node, AST.is(AST_NODE_TYPES$1.ReturnStatement));
|
|
57
|
+
const pFunction = AST.findParentNode(node, AST.isFunction);
|
|
58
|
+
const pFunctionOfReturn = AST.findParentNode(pReturn, AST.isFunction);
|
|
59
|
+
if (pFunction !== pFunctionOfReturn) return false;
|
|
60
|
+
return isFunctionOfUseEffectSetup(pFunction);
|
|
52
61
|
}
|
|
53
62
|
function isFunctionOfComponentDidMount(node) {
|
|
54
|
-
|
|
63
|
+
return AST.isFunction(node) && ER.isComponentDidMount(node.parent) && node.parent.value === node;
|
|
55
64
|
}
|
|
56
65
|
function isFunctionOfComponentWillUnmount(node) {
|
|
57
|
-
|
|
66
|
+
return AST.isFunction(node) && ER.isComponentWillUnmount(node.parent) && node.parent.value === node;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/utils/is-conditional.ts
|
|
71
|
+
const isControlFlow = or(AST.isLoop, AST.isOneOf([AST_NODE_TYPES$1.IfStatement, AST_NODE_TYPES$1.SwitchStatement]));
|
|
72
|
+
const isConditional = or(isControlFlow, AST.isOneOf([AST_NODE_TYPES$1.LogicalExpression, AST_NODE_TYPES$1.ConditionalExpression]));
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/rules/no-leaked-event-listener.ts
|
|
76
|
+
const RULE_NAME$3 = "no-leaked-event-listener";
|
|
77
|
+
const RULE_FEATURES$3 = [];
|
|
78
|
+
const defaultOptions = {
|
|
79
|
+
capture: false,
|
|
80
|
+
signal: unit
|
|
67
81
|
};
|
|
68
|
-
function getCallKind(node) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
default:
|
|
75
|
-
return "other";
|
|
76
|
-
}
|
|
82
|
+
function getCallKind$3(node) {
|
|
83
|
+
switch (true) {
|
|
84
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.name): return node.callee.name;
|
|
85
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.property.name): return node.callee.property.name;
|
|
86
|
+
default: return "other";
|
|
87
|
+
}
|
|
77
88
|
}
|
|
78
|
-
function getFunctionKind(node) {
|
|
79
|
-
|
|
89
|
+
function getFunctionKind$1(node) {
|
|
90
|
+
return getPhaseKindOfFunction(node) ?? "other";
|
|
80
91
|
}
|
|
81
92
|
function getSignalValueExpression(node, initialScope) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return node;
|
|
89
|
-
default:
|
|
90
|
-
return unit;
|
|
91
|
-
}
|
|
93
|
+
if (node == null) return unit;
|
|
94
|
+
switch (node.type) {
|
|
95
|
+
case AST_NODE_TYPES.Identifier: return getSignalValueExpression(VAR.getVariableDefinitionNode(VAR.findVariable(node, initialScope), 0), initialScope);
|
|
96
|
+
case AST_NODE_TYPES.MemberExpression: return node;
|
|
97
|
+
default: return unit;
|
|
98
|
+
}
|
|
92
99
|
}
|
|
93
100
|
function getOptions(node, initialScope) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
101
|
+
function findProp(properties, propName) {
|
|
102
|
+
return VAR.findProperty(propName, properties, initialScope);
|
|
103
|
+
}
|
|
104
|
+
function getPropValue(prop, filter = (a) => true) {
|
|
105
|
+
if (prop?.type !== AST_NODE_TYPES.Property) return unit;
|
|
106
|
+
const { value } = prop;
|
|
107
|
+
let v = value;
|
|
108
|
+
switch (value.type) {
|
|
109
|
+
case AST_NODE_TYPES.Literal:
|
|
110
|
+
v = value.value;
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
v = VAR.toStaticValue({
|
|
114
|
+
kind: "lazy",
|
|
115
|
+
node: value,
|
|
116
|
+
initialScope
|
|
117
|
+
}).value;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
return filter(v) ? v : unit;
|
|
121
|
+
}
|
|
122
|
+
function getOpts(node$1) {
|
|
123
|
+
switch (node$1.type) {
|
|
124
|
+
case AST_NODE_TYPES.Identifier: {
|
|
125
|
+
const variable = VAR.findVariable(node$1, initialScope);
|
|
126
|
+
const variableNode = VAR.getVariableDefinitionNode(variable, 0);
|
|
127
|
+
if (variableNode?.type === AST_NODE_TYPES.ObjectExpression) return getOpts(variableNode);
|
|
128
|
+
return defaultOptions;
|
|
129
|
+
}
|
|
130
|
+
case AST_NODE_TYPES.Literal: return {
|
|
131
|
+
...defaultOptions,
|
|
132
|
+
capture: Boolean(node$1.value)
|
|
133
|
+
};
|
|
134
|
+
case AST_NODE_TYPES.ObjectExpression: {
|
|
135
|
+
const pCapture = findProp(node$1.properties, "capture");
|
|
136
|
+
const vCapture = !!getPropValue(pCapture);
|
|
137
|
+
const pSignal = findProp(node$1.properties, "signal");
|
|
138
|
+
const vSignal = pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(pSignal.value, initialScope) : unit;
|
|
139
|
+
return {
|
|
140
|
+
capture: vCapture,
|
|
141
|
+
signal: vSignal
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
default: return defaultOptions;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return getOpts(node);
|
|
139
148
|
}
|
|
140
149
|
var no_leaked_event_listener_default = createRule({
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
150
|
+
meta: {
|
|
151
|
+
type: "problem",
|
|
152
|
+
docs: {
|
|
153
|
+
description: "Prevents leaked `addEventListener` in a component or custom Hook.",
|
|
154
|
+
[Symbol.for("rule_features")]: RULE_FEATURES$3
|
|
155
|
+
},
|
|
156
|
+
messages: {
|
|
157
|
+
expectedRemoveEventListenerInCleanup: "An 'addEventListener' in '{{effectMethodKind}}' should have a corresponding 'removeEventListener' in its cleanup function.",
|
|
158
|
+
expectedRemoveEventListenerInUnmount: "An 'addEventListener' in 'componentDidMount' should have a corresponding 'removeEventListener' in 'componentWillUnmount' method.",
|
|
159
|
+
unexpectedInlineFunction: "A/an '{{eventMethodKind}}' should not have an inline listener function."
|
|
160
|
+
},
|
|
161
|
+
schema: []
|
|
162
|
+
},
|
|
163
|
+
name: RULE_NAME$3,
|
|
164
|
+
create: create$3,
|
|
165
|
+
defaultOptions: []
|
|
157
166
|
});
|
|
158
|
-
function create(context) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
switch (aEntry.phase) {
|
|
267
|
-
case "setup":
|
|
268
|
-
case "cleanup":
|
|
269
|
-
context.report({
|
|
270
|
-
messageId: "expectedRemoveEventListenerInCleanup",
|
|
271
|
-
node: aEntry.node,
|
|
272
|
-
data: {
|
|
273
|
-
effectMethodKind: "useEffect"
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
continue;
|
|
277
|
-
case "mount":
|
|
278
|
-
case "unmount":
|
|
279
|
-
context.report({
|
|
280
|
-
messageId: "expectedRemoveEventListenerInUnmount",
|
|
281
|
-
node: aEntry.node
|
|
282
|
-
});
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
};
|
|
167
|
+
function create$3(context) {
|
|
168
|
+
if (!context.sourceCode.text.includes("addEventListener")) return {};
|
|
169
|
+
if (!/use\w*Effect|componentDidMount|componentWillUnmount/u.test(context.sourceCode.text)) return {};
|
|
170
|
+
const fEntries = [];
|
|
171
|
+
const aEntries = [];
|
|
172
|
+
const rEntries = [];
|
|
173
|
+
const abortedSignals = [];
|
|
174
|
+
function isSameObject(a, b) {
|
|
175
|
+
switch (true) {
|
|
176
|
+
case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return AST.isNodeEqual(a.object, b.object);
|
|
177
|
+
default: return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function isInverseEntry(aEntry, rEntry) {
|
|
181
|
+
const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
|
|
182
|
+
const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
|
|
183
|
+
if (!ER.isInversePhase(aPhase, rPhase)) return false;
|
|
184
|
+
return isSameObject(aCallee, rCallee) && AST.isNodeEqual(aListener, rListener) && VAR.isNodeValueEqual(aType, rType, [context.sourceCode.getScope(aType), context.sourceCode.getScope(rType)]) && aCapture === rCapture;
|
|
185
|
+
}
|
|
186
|
+
function checkInlineFunction(node, callKind, options) {
|
|
187
|
+
const listener = node.arguments.at(1);
|
|
188
|
+
if (!AST.isFunction(listener)) return;
|
|
189
|
+
if (options.signal != null) return;
|
|
190
|
+
context.report({
|
|
191
|
+
messageId: "unexpectedInlineFunction",
|
|
192
|
+
node: listener,
|
|
193
|
+
data: { eventMethodKind: callKind }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
[":function"](node) {
|
|
198
|
+
const kind = getFunctionKind$1(node);
|
|
199
|
+
fEntries.push({
|
|
200
|
+
kind,
|
|
201
|
+
node
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
[":function:exit"]() {
|
|
205
|
+
fEntries.pop();
|
|
206
|
+
},
|
|
207
|
+
["CallExpression"](node) {
|
|
208
|
+
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
209
|
+
if (fKind == null) return;
|
|
210
|
+
if (!ER.ComponentPhaseRelevance.has(fKind)) return;
|
|
211
|
+
match(getCallKind$3(node)).with("addEventListener", (callKind) => {
|
|
212
|
+
const [type, listener, options] = node.arguments;
|
|
213
|
+
if (type == null || listener == null) return;
|
|
214
|
+
const opts = options == null ? defaultOptions : getOptions(options, context.sourceCode.getScope(options));
|
|
215
|
+
const { callee } = node;
|
|
216
|
+
checkInlineFunction(node, callKind, opts);
|
|
217
|
+
aEntries.push({
|
|
218
|
+
...opts,
|
|
219
|
+
kind: "addEventListener",
|
|
220
|
+
type,
|
|
221
|
+
node,
|
|
222
|
+
callee,
|
|
223
|
+
listener,
|
|
224
|
+
phase: fKind
|
|
225
|
+
});
|
|
226
|
+
}).with("removeEventListener", (callKind) => {
|
|
227
|
+
const [type, listener, options] = node.arguments;
|
|
228
|
+
if (type == null || listener == null) return;
|
|
229
|
+
const opts = options == null ? defaultOptions : getOptions(options, context.sourceCode.getScope(options));
|
|
230
|
+
const { callee } = node;
|
|
231
|
+
checkInlineFunction(node, callKind, opts);
|
|
232
|
+
rEntries.push({
|
|
233
|
+
...opts,
|
|
234
|
+
kind: "removeEventListener",
|
|
235
|
+
type,
|
|
236
|
+
node,
|
|
237
|
+
callee,
|
|
238
|
+
listener,
|
|
239
|
+
phase: fKind
|
|
240
|
+
});
|
|
241
|
+
}).with("abort", () => {
|
|
242
|
+
abortedSignals.push(node.callee);
|
|
243
|
+
}).otherwise(() => null);
|
|
244
|
+
},
|
|
245
|
+
["Program:exit"]() {
|
|
246
|
+
for (const aEntry of aEntries) {
|
|
247
|
+
const signal = aEntry.signal;
|
|
248
|
+
if (signal != null && abortedSignals.some((a) => isSameObject(a, signal))) continue;
|
|
249
|
+
if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
|
|
250
|
+
switch (aEntry.phase) {
|
|
251
|
+
case "setup":
|
|
252
|
+
case "cleanup":
|
|
253
|
+
context.report({
|
|
254
|
+
messageId: "expectedRemoveEventListenerInCleanup",
|
|
255
|
+
node: aEntry.node,
|
|
256
|
+
data: { effectMethodKind: "useEffect" }
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
case "mount":
|
|
260
|
+
case "unmount":
|
|
261
|
+
context.report({
|
|
262
|
+
messageId: "expectedRemoveEventListenerInUnmount",
|
|
263
|
+
node: aEntry.node
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
288
270
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/rules/no-leaked-interval.ts
|
|
274
|
+
const RULE_NAME$2 = "no-leaked-interval";
|
|
275
|
+
const RULE_FEATURES$2 = [];
|
|
276
|
+
function getCallKind$2(node) {
|
|
277
|
+
switch (true) {
|
|
278
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(node.callee.name): return node.callee.name;
|
|
279
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(node.callee.property.name): return node.callee.property.name;
|
|
280
|
+
default: return "other";
|
|
281
|
+
}
|
|
300
282
|
}
|
|
301
283
|
var no_leaked_interval_default = createRule({
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
284
|
+
meta: {
|
|
285
|
+
type: "problem",
|
|
286
|
+
docs: {
|
|
287
|
+
description: "Prevents leaked `setInterval` in a component or custom Hook.",
|
|
288
|
+
[Symbol.for("rule_features")]: RULE_FEATURES$2
|
|
289
|
+
},
|
|
290
|
+
messages: {
|
|
291
|
+
expectedClearIntervalInCleanup: "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the cleanup function.",
|
|
292
|
+
expectedClearIntervalInUnmount: "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the 'componentWillUnmount' method.",
|
|
293
|
+
expectedIntervalId: "A 'setInterval' must be assigned to a variable for proper cleanup."
|
|
294
|
+
},
|
|
295
|
+
schema: []
|
|
296
|
+
},
|
|
297
|
+
name: RULE_NAME$2,
|
|
298
|
+
create: create$2,
|
|
299
|
+
defaultOptions: []
|
|
318
300
|
});
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
continue;
|
|
403
|
-
case "mount":
|
|
404
|
-
case "unmount":
|
|
405
|
-
context.report({
|
|
406
|
-
messageId: "expectedClearIntervalInUnmount",
|
|
407
|
-
node: sEntry.node,
|
|
408
|
-
data: {
|
|
409
|
-
kind: "componentDidMount"
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
};
|
|
301
|
+
function create$2(context) {
|
|
302
|
+
if (!context.sourceCode.text.includes("setInterval")) return {};
|
|
303
|
+
const fEntries = [];
|
|
304
|
+
const sEntries = [];
|
|
305
|
+
const cEntries = [];
|
|
306
|
+
function isInverseEntry(a, b) {
|
|
307
|
+
return ER.isInstanceIdEqual(context, a.timerId, b.timerId);
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
[":function"](node) {
|
|
311
|
+
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
312
|
+
fEntries.push({
|
|
313
|
+
kind,
|
|
314
|
+
node
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
[":function:exit"]() {
|
|
318
|
+
fEntries.pop();
|
|
319
|
+
},
|
|
320
|
+
["CallExpression"](node) {
|
|
321
|
+
switch (getCallKind$2(node)) {
|
|
322
|
+
case "setInterval": {
|
|
323
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
324
|
+
if (fEntry == null) break;
|
|
325
|
+
if (!ER.ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
326
|
+
const intervalIdNode = VAR.findAssignmentTarget(node);
|
|
327
|
+
if (intervalIdNode == null) {
|
|
328
|
+
context.report({
|
|
329
|
+
messageId: "expectedIntervalId",
|
|
330
|
+
node
|
|
331
|
+
});
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
sEntries.push({
|
|
335
|
+
kind: "interval",
|
|
336
|
+
node,
|
|
337
|
+
callee: node.callee,
|
|
338
|
+
phase: fEntry.kind,
|
|
339
|
+
timerId: intervalIdNode
|
|
340
|
+
});
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "clearInterval": {
|
|
344
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
345
|
+
if (fEntry == null) break;
|
|
346
|
+
if (!ER.ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
347
|
+
const [intervalIdNode] = node.arguments;
|
|
348
|
+
if (intervalIdNode == null) break;
|
|
349
|
+
cEntries.push({
|
|
350
|
+
kind: "interval",
|
|
351
|
+
node,
|
|
352
|
+
callee: node.callee,
|
|
353
|
+
phase: fEntry.kind,
|
|
354
|
+
timerId: intervalIdNode
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
["Program:exit"]() {
|
|
361
|
+
for (const sEntry of sEntries) {
|
|
362
|
+
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
363
|
+
switch (sEntry.phase) {
|
|
364
|
+
case "setup":
|
|
365
|
+
case "cleanup":
|
|
366
|
+
context.report({
|
|
367
|
+
messageId: "expectedClearIntervalInCleanup",
|
|
368
|
+
node: sEntry.node,
|
|
369
|
+
data: { kind: "useEffect" }
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
case "mount":
|
|
373
|
+
case "unmount":
|
|
374
|
+
context.report({
|
|
375
|
+
messageId: "expectedClearIntervalInUnmount",
|
|
376
|
+
node: sEntry.node,
|
|
377
|
+
data: { kind: "componentDidMount" }
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
417
384
|
}
|
|
418
|
-
|
|
419
|
-
|
|
385
|
+
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/rules/no-leaked-resize-observer.ts
|
|
388
|
+
const RULE_NAME$1 = "no-leaked-resize-observer";
|
|
389
|
+
const RULE_FEATURES$1 = [];
|
|
420
390
|
function isNewResizeObserver(node) {
|
|
421
|
-
|
|
391
|
+
return node?.type === AST_NODE_TYPES.NewExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "ResizeObserver";
|
|
422
392
|
}
|
|
423
393
|
function isFromObserver(context, node) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
394
|
+
switch (true) {
|
|
395
|
+
case node.type === AST_NODE_TYPES.Identifier: {
|
|
396
|
+
const initialScope = context.sourceCode.getScope(node);
|
|
397
|
+
const object = VAR.getVariableDefinitionNode(VAR.findVariable(node, initialScope), 0);
|
|
398
|
+
return isNewResizeObserver(object);
|
|
399
|
+
}
|
|
400
|
+
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
|
|
401
|
+
default: return false;
|
|
402
|
+
}
|
|
435
403
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
default:
|
|
443
|
-
return "other";
|
|
444
|
-
}
|
|
404
|
+
function getCallKind$1(context, node) {
|
|
405
|
+
switch (true) {
|
|
406
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(node.callee.name) && isFromObserver(context, node.callee): return node.callee.name;
|
|
407
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(node.callee.property.name) && isFromObserver(context, node.callee): return node.callee.property.name;
|
|
408
|
+
default: return "other";
|
|
409
|
+
}
|
|
445
410
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
411
|
+
function getFunctionKind(node) {
|
|
412
|
+
return getPhaseKindOfFunction(node) ?? "other";
|
|
448
413
|
}
|
|
449
414
|
var no_leaked_resize_observer_default = createRule({
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
415
|
+
meta: {
|
|
416
|
+
type: "problem",
|
|
417
|
+
docs: {
|
|
418
|
+
description: "Prevents leaked `ResizeObserver` in a component or custom Hook.",
|
|
419
|
+
[Symbol.for("rule_features")]: RULE_FEATURES$1
|
|
420
|
+
},
|
|
421
|
+
messages: {
|
|
422
|
+
expectedDisconnectInControlFlow: "Dynamically added 'ResizeObserver.observe' should be cleared all at once using 'ResizeObserver.disconnect' in the cleanup function.",
|
|
423
|
+
expectedDisconnectOrUnobserveInCleanup: "A 'ResizeObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
|
|
424
|
+
unexpectedFloatingInstance: "A 'ResizeObserver' instance created in component or custom Hook must be assigned to a variable for proper cleanup."
|
|
425
|
+
},
|
|
426
|
+
schema: []
|
|
427
|
+
},
|
|
428
|
+
name: RULE_NAME$1,
|
|
429
|
+
create: create$1,
|
|
430
|
+
defaultOptions: []
|
|
466
431
|
});
|
|
467
|
-
function
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
for (const oEntry of oentries) {
|
|
571
|
-
if (uentries.some((uEntry) => ER4.isInstanceIdEqual(context, uEntry.element, oEntry.element))) {
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
context.report({ messageId: "expectedDisconnectOrUnobserveInCleanup", node: oEntry.node });
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
};
|
|
432
|
+
function create$1(context) {
|
|
433
|
+
if (!context.sourceCode.text.includes("ResizeObserver")) return {};
|
|
434
|
+
const fEntries = [];
|
|
435
|
+
const observers = [];
|
|
436
|
+
const oEntries = [];
|
|
437
|
+
const uEntries = [];
|
|
438
|
+
const dEntries = [];
|
|
439
|
+
return {
|
|
440
|
+
[":function"](node) {
|
|
441
|
+
const kind = getFunctionKind(node);
|
|
442
|
+
fEntries.push({
|
|
443
|
+
kind,
|
|
444
|
+
node
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
[":function:exit"]() {
|
|
448
|
+
fEntries.pop();
|
|
449
|
+
},
|
|
450
|
+
["CallExpression"](node) {
|
|
451
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
452
|
+
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
453
|
+
if (fKind == null || !ER.ComponentPhaseRelevance.has(fKind)) return;
|
|
454
|
+
const { object } = node.callee;
|
|
455
|
+
match(getCallKind$1(context, node)).with("disconnect", () => {
|
|
456
|
+
dEntries.push({
|
|
457
|
+
kind: "disconnect",
|
|
458
|
+
node,
|
|
459
|
+
callee: node.callee,
|
|
460
|
+
observer: object,
|
|
461
|
+
observerKind: "ResizeObserver",
|
|
462
|
+
phase: fKind
|
|
463
|
+
});
|
|
464
|
+
}).with("observe", () => {
|
|
465
|
+
const [element] = node.arguments;
|
|
466
|
+
if (element == null) return;
|
|
467
|
+
oEntries.push({
|
|
468
|
+
kind: "observe",
|
|
469
|
+
node,
|
|
470
|
+
callee: node.callee,
|
|
471
|
+
element,
|
|
472
|
+
observer: object,
|
|
473
|
+
observerKind: "ResizeObserver",
|
|
474
|
+
phase: fKind
|
|
475
|
+
});
|
|
476
|
+
}).with("unobserve", () => {
|
|
477
|
+
const [element] = node.arguments;
|
|
478
|
+
if (element == null) return;
|
|
479
|
+
uEntries.push({
|
|
480
|
+
kind: "unobserve",
|
|
481
|
+
node,
|
|
482
|
+
callee: node.callee,
|
|
483
|
+
element,
|
|
484
|
+
observer: object,
|
|
485
|
+
observerKind: "ResizeObserver",
|
|
486
|
+
phase: fKind
|
|
487
|
+
});
|
|
488
|
+
}).otherwise(() => null);
|
|
489
|
+
},
|
|
490
|
+
["NewExpression"](node) {
|
|
491
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
492
|
+
if (fEntry == null) return;
|
|
493
|
+
if (!ER.ComponentPhaseRelevance.has(fEntry.kind)) return;
|
|
494
|
+
if (!isNewResizeObserver(node)) return;
|
|
495
|
+
const id = ER.getInstanceId(node);
|
|
496
|
+
if (id == null) {
|
|
497
|
+
context.report({
|
|
498
|
+
messageId: "unexpectedFloatingInstance",
|
|
499
|
+
node
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
observers.push({
|
|
504
|
+
id,
|
|
505
|
+
node,
|
|
506
|
+
phase: fEntry.kind,
|
|
507
|
+
phaseNode: fEntry.node
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
["Program:exit"]() {
|
|
511
|
+
for (const { id, node, phaseNode } of observers) {
|
|
512
|
+
if (dEntries.some((e) => ER.isInstanceIdEqual(context, e.observer, id))) continue;
|
|
513
|
+
const oentries = oEntries.filter((e) => ER.isInstanceIdEqual(context, e.observer, id));
|
|
514
|
+
const uentries = uEntries.filter((e) => ER.isInstanceIdEqual(context, e.observer, id));
|
|
515
|
+
const isDynamic = (node$1) => node$1?.type === AST_NODE_TYPES.CallExpression || isConditional(node$1);
|
|
516
|
+
const isPhaseNode = (node$1) => node$1 === phaseNode;
|
|
517
|
+
const hasDynamicallyAdded = oentries.some((e) => !isPhaseNode(AST.findParentNode(e.node, or(isDynamic, isPhaseNode))));
|
|
518
|
+
if (hasDynamicallyAdded) {
|
|
519
|
+
context.report({
|
|
520
|
+
messageId: "expectedDisconnectInControlFlow",
|
|
521
|
+
node
|
|
522
|
+
});
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
for (const oEntry of oentries) {
|
|
526
|
+
if (uentries.some((uEntry) => ER.isInstanceIdEqual(context, uEntry.element, oEntry.element))) continue;
|
|
527
|
+
context.report({
|
|
528
|
+
messageId: "expectedDisconnectOrUnobserveInCleanup",
|
|
529
|
+
node: oEntry.node
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
579
535
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
536
|
+
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/rules/no-leaked-timeout.ts
|
|
539
|
+
const RULE_NAME = "no-leaked-timeout";
|
|
540
|
+
const RULE_FEATURES = [];
|
|
541
|
+
function getCallKind(node) {
|
|
542
|
+
switch (true) {
|
|
543
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setTimeout", "clearTimeout"))(node.callee.name): return node.callee.name;
|
|
544
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setTimeout", "clearTimeout"))(node.callee.property.name): return node.callee.property.name;
|
|
545
|
+
default: return "other";
|
|
546
|
+
}
|
|
591
547
|
}
|
|
592
548
|
var no_leaked_timeout_default = createRule({
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
549
|
+
meta: {
|
|
550
|
+
type: "problem",
|
|
551
|
+
docs: {
|
|
552
|
+
description: "Prevents leaked `setTimeout` in a component or custom Hook.",
|
|
553
|
+
[Symbol.for("rule_features")]: RULE_FEATURES
|
|
554
|
+
},
|
|
555
|
+
messages: {
|
|
556
|
+
expectedClearTimeoutInCleanup: "A 'setTimeout' created in '{{ kind }}' must be cleared with 'clearTimeout' in the cleanup function.",
|
|
557
|
+
expectedClearTimeoutInUnmount: "A 'setTimeout' created in '{{ kind }}' must be cleared with 'clearTimeout' in the 'componentWillUnmount' method.",
|
|
558
|
+
expectedTimeoutId: "A 'setTimeout' must be assigned to a variable for proper cleanup."
|
|
559
|
+
},
|
|
560
|
+
schema: []
|
|
561
|
+
},
|
|
562
|
+
name: RULE_NAME,
|
|
563
|
+
create,
|
|
564
|
+
defaultOptions: []
|
|
609
565
|
});
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
data: {
|
|
690
|
-
kind: "componentDidMount"
|
|
691
|
-
}
|
|
692
|
-
});
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
};
|
|
566
|
+
function create(context) {
|
|
567
|
+
if (!context.sourceCode.text.includes("setTimeout")) return {};
|
|
568
|
+
const fEntries = [];
|
|
569
|
+
const sEntries = [];
|
|
570
|
+
const rEntries = [];
|
|
571
|
+
function isInverseEntry(a, b) {
|
|
572
|
+
return ER.isInstanceIdEqual(context, a.timerId, b.timerId);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
[":function"](node) {
|
|
576
|
+
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
577
|
+
fEntries.push({
|
|
578
|
+
kind,
|
|
579
|
+
node
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
[":function:exit"]() {
|
|
583
|
+
fEntries.pop();
|
|
584
|
+
},
|
|
585
|
+
["CallExpression"](node) {
|
|
586
|
+
const fEntry = fEntries.findLast((f) => f.kind !== "other");
|
|
587
|
+
if (!ER.ComponentPhaseRelevance.has(fEntry?.kind)) return;
|
|
588
|
+
switch (getCallKind(node)) {
|
|
589
|
+
case "setTimeout": {
|
|
590
|
+
const timeoutIdNode = VAR.findAssignmentTarget(node);
|
|
591
|
+
if (timeoutIdNode == null) {
|
|
592
|
+
context.report({
|
|
593
|
+
messageId: "expectedTimeoutId",
|
|
594
|
+
node
|
|
595
|
+
});
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
sEntries.push({
|
|
599
|
+
kind: "timeout",
|
|
600
|
+
node,
|
|
601
|
+
callee: node.callee,
|
|
602
|
+
phase: fEntry.kind,
|
|
603
|
+
timerId: timeoutIdNode
|
|
604
|
+
});
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
case "clearTimeout": {
|
|
608
|
+
const [timeoutIdNode] = node.arguments;
|
|
609
|
+
if (timeoutIdNode == null) break;
|
|
610
|
+
rEntries.push({
|
|
611
|
+
kind: "timeout",
|
|
612
|
+
node,
|
|
613
|
+
callee: node.callee,
|
|
614
|
+
phase: fEntry.kind,
|
|
615
|
+
timerId: timeoutIdNode
|
|
616
|
+
});
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
["Program:exit"]() {
|
|
622
|
+
for (const sEntry of sEntries) {
|
|
623
|
+
if (rEntries.some((rEntry) => isInverseEntry(sEntry, rEntry))) continue;
|
|
624
|
+
switch (sEntry.phase) {
|
|
625
|
+
case "setup":
|
|
626
|
+
case "cleanup":
|
|
627
|
+
context.report({
|
|
628
|
+
messageId: "expectedClearTimeoutInCleanup",
|
|
629
|
+
node: sEntry.node,
|
|
630
|
+
data: { kind: "useEffect" }
|
|
631
|
+
});
|
|
632
|
+
continue;
|
|
633
|
+
case "mount":
|
|
634
|
+
case "unmount":
|
|
635
|
+
context.report({
|
|
636
|
+
messageId: "expectedClearTimeoutInUnmount",
|
|
637
|
+
node: sEntry.node,
|
|
638
|
+
data: { kind: "componentDidMount" }
|
|
639
|
+
});
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
698
645
|
}
|
|
699
646
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/plugin.ts
|
|
649
|
+
const plugin = {
|
|
650
|
+
meta: {
|
|
651
|
+
name,
|
|
652
|
+
version
|
|
653
|
+
},
|
|
654
|
+
rules: {
|
|
655
|
+
"no-leaked-event-listener": no_leaked_event_listener_default,
|
|
656
|
+
"no-leaked-interval": no_leaked_interval_default,
|
|
657
|
+
"no-leaked-resize-observer": no_leaked_resize_observer_default,
|
|
658
|
+
"no-leaked-timeout": no_leaked_timeout_default
|
|
659
|
+
}
|
|
712
660
|
};
|
|
713
661
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
function makeLegacyConfig({ rules: rules2 }) {
|
|
724
|
-
return {
|
|
725
|
-
plugins: ["react-web-api"],
|
|
726
|
-
rules: rules2
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
var index_default = {
|
|
730
|
-
...plugin,
|
|
731
|
-
configs: {
|
|
732
|
-
["recommended"]: makeConfig(recommended_exports),
|
|
733
|
-
["recommended-legacy"]: makeLegacyConfig(recommended_exports)
|
|
734
|
-
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/index.ts
|
|
664
|
+
const { toFlatConfig, toLegacyConfig } = getConfigAdapters("react-web-api", plugin);
|
|
665
|
+
var src_default = {
|
|
666
|
+
...plugin,
|
|
667
|
+
configs: {
|
|
668
|
+
["recommended"]: toFlatConfig(recommended_exports),
|
|
669
|
+
["recommended-legacy"]: toLegacyConfig(recommended_exports)
|
|
670
|
+
}
|
|
735
671
|
};
|
|
736
672
|
|
|
737
|
-
|
|
673
|
+
//#endregion
|
|
674
|
+
export { src_default as default };
|