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