eslint-plugin-react-web-api 5.8.18 → 5.9.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/README.md +0 -1
- package/dist/index.js +283 -123
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -45,7 +45,6 @@ export default defineConfig(
|
|
|
45
45
|
| `no-leaked-idle-callback` | Prevents leaked `requestIdleCallback` |
|
|
46
46
|
| `no-leaked-animation-frame` | Prevents leaked `requestAnimationFrame` |
|
|
47
47
|
| `no-leaked-event-source` | Prevents leaked `EventSource` |
|
|
48
|
-
| `no-leaked-intersection-observer` | Prevents leaked `IntersectionObserver` |
|
|
49
48
|
| `no-leaked-mutation-observer` | Prevents leaked `MutationObserver` |
|
|
50
49
|
| `no-leaked-performance-observer` | Prevents leaked `PerformanceObserver` |
|
|
51
50
|
| `no-leaked-websocket` | Prevents leaked `WebSocket` |
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region package.json
|
|
30
30
|
var name$1 = "eslint-plugin-react-web-api";
|
|
31
|
-
var version = "5.
|
|
31
|
+
var version = "5.9.0";
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/types/component-phase.ts
|
|
@@ -108,8 +108,8 @@ function getOptions(context, node) {
|
|
|
108
108
|
|
|
109
109
|
//#endregion
|
|
110
110
|
//#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
|
|
111
|
-
const RULE_NAME$
|
|
112
|
-
function getCallKind$
|
|
111
|
+
const RULE_NAME$5 = "no-leaked-event-listener";
|
|
112
|
+
function getCallKind$5(node) {
|
|
113
113
|
const callee = Extract.unwrap(node.callee);
|
|
114
114
|
switch (true) {
|
|
115
115
|
case callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(callee.name): return callee.name;
|
|
@@ -117,7 +117,7 @@ function getCallKind$4(node) {
|
|
|
117
117
|
default: return "other";
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
function getFunctionKind$
|
|
120
|
+
function getFunctionKind$2(node) {
|
|
121
121
|
return getPhaseKindOfFunction(node) ?? "other";
|
|
122
122
|
}
|
|
123
123
|
var no_leaked_event_listener_default = createRule({
|
|
@@ -130,11 +130,11 @@ var no_leaked_event_listener_default = createRule({
|
|
|
130
130
|
},
|
|
131
131
|
schema: []
|
|
132
132
|
},
|
|
133
|
-
name: RULE_NAME$
|
|
134
|
-
create: create$
|
|
133
|
+
name: RULE_NAME$5,
|
|
134
|
+
create: create$5,
|
|
135
135
|
defaultOptions: []
|
|
136
136
|
});
|
|
137
|
-
function create$
|
|
137
|
+
function create$5(context) {
|
|
138
138
|
if (!context.sourceCode.text.includes("addEventListener")) return {};
|
|
139
139
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
140
140
|
const fEntries = [];
|
|
@@ -165,7 +165,7 @@ function create$4(context) {
|
|
|
165
165
|
}
|
|
166
166
|
return {
|
|
167
167
|
[":function"](node) {
|
|
168
|
-
const kind = getFunctionKind$
|
|
168
|
+
const kind = getFunctionKind$2(node);
|
|
169
169
|
fEntries.push({
|
|
170
170
|
kind,
|
|
171
171
|
node
|
|
@@ -179,7 +179,7 @@ function create$4(context) {
|
|
|
179
179
|
if (fKind == null) return;
|
|
180
180
|
if (!ComponentPhaseRelevance.has(fKind)) return;
|
|
181
181
|
const unwrappedCallee = Extract.unwrap(node.callee);
|
|
182
|
-
match(getCallKind$
|
|
182
|
+
match(getCallKind$5(node)).with("addEventListener", (callKind) => {
|
|
183
183
|
if (unwrappedCallee.type === AST_NODE_TYPES.MemberExpression && unwrappedCallee.object.type === AST_NODE_TYPES.Identifier && core.isAPIFromReactNative(unwrappedCallee.object.name, context.sourceCode.getScope(node))) return;
|
|
184
184
|
const [type, listener, options] = node.arguments;
|
|
185
185
|
if (type == null || listener == null) return;
|
|
@@ -261,8 +261,8 @@ function resolveToObjectExpression(context, node) {
|
|
|
261
261
|
|
|
262
262
|
//#endregion
|
|
263
263
|
//#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
|
|
264
|
-
const RULE_NAME$
|
|
265
|
-
function getCallKind$
|
|
264
|
+
const RULE_NAME$4 = "no-leaked-fetch";
|
|
265
|
+
function getCallKind$4(node) {
|
|
266
266
|
const callee = Extract.unwrap(node.callee);
|
|
267
267
|
switch (true) {
|
|
268
268
|
case callee.type === AST_NODE_TYPES.Identifier && callee.name === "fetch": return "fetch";
|
|
@@ -334,11 +334,11 @@ var no_leaked_fetch_default = createRule({
|
|
|
334
334
|
},
|
|
335
335
|
schema: []
|
|
336
336
|
},
|
|
337
|
-
name: RULE_NAME$
|
|
338
|
-
create: create$
|
|
337
|
+
name: RULE_NAME$4,
|
|
338
|
+
create: create$4,
|
|
339
339
|
defaultOptions: []
|
|
340
340
|
});
|
|
341
|
-
function create$
|
|
341
|
+
function create$4(context) {
|
|
342
342
|
if (!context.sourceCode.text.includes("fetch")) return {};
|
|
343
343
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
344
344
|
const fEntries = [];
|
|
@@ -358,7 +358,7 @@ function create$3(context) {
|
|
|
358
358
|
["CallExpression"](node) {
|
|
359
359
|
const fEntry = fEntries.at(-1);
|
|
360
360
|
if (fEntry == null || !ComponentPhaseRelevance.has(fEntry.kind)) return;
|
|
361
|
-
switch (getCallKind$
|
|
361
|
+
switch (getCallKind$4(node)) {
|
|
362
362
|
case "fetch": {
|
|
363
363
|
if (fEntry.kind !== "setup") return;
|
|
364
364
|
const { controller, isParamSignal } = getFetchController(context, node);
|
|
@@ -403,108 +403,6 @@ function create$3(context) {
|
|
|
403
403
|
};
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
//#endregion
|
|
407
|
-
//#region src/rules/no-leaked-interval/no-leaked-interval.ts
|
|
408
|
-
const RULE_NAME$2 = "no-leaked-interval";
|
|
409
|
-
function getCallKind$2(node) {
|
|
410
|
-
const callee = Extract.unwrap(node.callee);
|
|
411
|
-
switch (true) {
|
|
412
|
-
case callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(callee.name): return callee.name;
|
|
413
|
-
case callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(callee.property.name): return callee.property.name;
|
|
414
|
-
default: return "other";
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
var no_leaked_interval_default = createRule({
|
|
418
|
-
meta: {
|
|
419
|
-
type: "problem",
|
|
420
|
-
docs: { description: "Enforces that every 'setInterval' in a component or custom hook has a corresponding 'clearInterval'." },
|
|
421
|
-
messages: {
|
|
422
|
-
expectedClearIntervalInCleanup: "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the cleanup function.",
|
|
423
|
-
expectedIntervalId: "A 'setInterval' must be assigned to a variable for proper cleanup."
|
|
424
|
-
},
|
|
425
|
-
schema: []
|
|
426
|
-
},
|
|
427
|
-
name: RULE_NAME$2,
|
|
428
|
-
create: create$2,
|
|
429
|
-
defaultOptions: []
|
|
430
|
-
});
|
|
431
|
-
function create$2(context) {
|
|
432
|
-
if (!context.sourceCode.text.includes("setInterval")) return {};
|
|
433
|
-
const fEntries = [];
|
|
434
|
-
const sEntries = [];
|
|
435
|
-
const cEntries = [];
|
|
436
|
-
function isInverseEntry(a, b) {
|
|
437
|
-
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
|
|
438
|
-
}
|
|
439
|
-
return {
|
|
440
|
-
[":function"](node) {
|
|
441
|
-
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
442
|
-
fEntries.push({
|
|
443
|
-
kind,
|
|
444
|
-
node
|
|
445
|
-
});
|
|
446
|
-
},
|
|
447
|
-
[":function:exit"]() {
|
|
448
|
-
fEntries.pop();
|
|
449
|
-
},
|
|
450
|
-
["CallExpression"](node) {
|
|
451
|
-
switch (getCallKind$2(node)) {
|
|
452
|
-
case "setInterval": {
|
|
453
|
-
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
454
|
-
if (fEntry == null) break;
|
|
455
|
-
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
456
|
-
const intervalIdNode = resolveEnclosingAssignmentTarget(node);
|
|
457
|
-
if (intervalIdNode == null) {
|
|
458
|
-
context.report({
|
|
459
|
-
messageId: "expectedIntervalId",
|
|
460
|
-
node
|
|
461
|
-
});
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
sEntries.push({
|
|
465
|
-
kind: "interval",
|
|
466
|
-
callee: node.callee,
|
|
467
|
-
node,
|
|
468
|
-
phase: fEntry.kind,
|
|
469
|
-
timerId: intervalIdNode
|
|
470
|
-
});
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
case "clearInterval": {
|
|
474
|
-
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
475
|
-
if (fEntry == null) break;
|
|
476
|
-
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
477
|
-
const [intervalIdNode] = node.arguments;
|
|
478
|
-
if (intervalIdNode == null) break;
|
|
479
|
-
cEntries.push({
|
|
480
|
-
kind: "interval",
|
|
481
|
-
callee: node.callee,
|
|
482
|
-
node,
|
|
483
|
-
phase: fEntry.kind,
|
|
484
|
-
timerId: intervalIdNode
|
|
485
|
-
});
|
|
486
|
-
break;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
["Program:exit"]() {
|
|
491
|
-
for (const sEntry of sEntries) {
|
|
492
|
-
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
493
|
-
switch (sEntry.phase) {
|
|
494
|
-
case "setup":
|
|
495
|
-
case "cleanup":
|
|
496
|
-
context.report({
|
|
497
|
-
data: { kind: "useEffect" },
|
|
498
|
-
messageId: "expectedClearIntervalInCleanup",
|
|
499
|
-
node: sEntry.node
|
|
500
|
-
});
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
406
|
//#endregion
|
|
509
407
|
//#region ../../.pkgs/eff/dist/index.js
|
|
510
408
|
function or(a, b) {
|
|
@@ -627,19 +525,278 @@ const dual = function(arity, body) {
|
|
|
627
525
|
const compose = dual(2, (ab, bc) => (a) => bc(ab(a)));
|
|
628
526
|
|
|
629
527
|
//#endregion
|
|
630
|
-
//#region src/rules/no-leaked-
|
|
528
|
+
//#region src/rules/no-leaked-intersection-observer/lib.ts
|
|
631
529
|
/**
|
|
632
|
-
* Check if a node is a
|
|
530
|
+
* Check if a node is a conditional expression or control flow statement
|
|
633
531
|
* @param node The node to check
|
|
634
|
-
* @returns True if the node is
|
|
532
|
+
* @returns True if the node is conditional
|
|
635
533
|
*/
|
|
636
|
-
const
|
|
534
|
+
const isConditional$1 = isOneOf([
|
|
637
535
|
AST_NODE_TYPES.DoWhileStatement,
|
|
638
536
|
AST_NODE_TYPES.ForInStatement,
|
|
639
537
|
AST_NODE_TYPES.ForOfStatement,
|
|
640
538
|
AST_NODE_TYPES.ForStatement,
|
|
641
|
-
AST_NODE_TYPES.WhileStatement
|
|
539
|
+
AST_NODE_TYPES.WhileStatement,
|
|
540
|
+
AST_NODE_TYPES.IfStatement,
|
|
541
|
+
AST_NODE_TYPES.SwitchStatement,
|
|
542
|
+
AST_NODE_TYPES.LogicalExpression,
|
|
543
|
+
AST_NODE_TYPES.ConditionalExpression
|
|
642
544
|
]);
|
|
545
|
+
function isNewIntersectionObserver(node) {
|
|
546
|
+
if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
|
|
547
|
+
const callee = Extract.unwrap(node.callee);
|
|
548
|
+
return callee.type === AST_NODE_TYPES.Identifier && callee.name === "IntersectionObserver";
|
|
549
|
+
}
|
|
550
|
+
function isFromObserver$1(context, node) {
|
|
551
|
+
switch (true) {
|
|
552
|
+
case node.type === AST_NODE_TYPES.Identifier: {
|
|
553
|
+
const initNode = resolve(context, node);
|
|
554
|
+
return isNewIntersectionObserver(initNode == null ? null : Extract.unwrap(initNode));
|
|
555
|
+
}
|
|
556
|
+
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object);
|
|
557
|
+
default: return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/rules/no-leaked-intersection-observer/no-leaked-intersection-observer.ts
|
|
563
|
+
const RULE_NAME$3 = "no-leaked-intersection-observer";
|
|
564
|
+
function getCallKind$3(context, node) {
|
|
565
|
+
const callee = Extract.unwrap(node.callee);
|
|
566
|
+
switch (true) {
|
|
567
|
+
case callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(callee.name) && isFromObserver$1(context, callee): return callee.name;
|
|
568
|
+
case callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(callee.property.name) && isFromObserver$1(context, callee): return callee.property.name;
|
|
569
|
+
default: return "other";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function getFunctionKind$1(node) {
|
|
573
|
+
return getPhaseKindOfFunction(node) ?? "other";
|
|
574
|
+
}
|
|
575
|
+
var no_leaked_intersection_observer_default = createRule({
|
|
576
|
+
meta: {
|
|
577
|
+
type: "problem",
|
|
578
|
+
docs: { description: "Enforces that every 'IntersectionObserver' created in a component or custom hook has a corresponding 'IntersectionObserver.disconnect()'." },
|
|
579
|
+
messages: {
|
|
580
|
+
expectedDisconnectInControlFlow: "Dynamically added 'IntersectionObserver.observe' should be cleared all at once using 'IntersectionObserver.disconnect' in the cleanup function.",
|
|
581
|
+
expectedDisconnectOrUnobserveInCleanup: "An 'IntersectionObserver' instance created in 'useEffect' must be disconnected in the cleanup function.",
|
|
582
|
+
unexpectedFloatingInstance: "An 'IntersectionObserver' instance created in component or custom hook must be assigned to a variable for proper cleanup."
|
|
583
|
+
},
|
|
584
|
+
schema: []
|
|
585
|
+
},
|
|
586
|
+
name: RULE_NAME$3,
|
|
587
|
+
create: create$3,
|
|
588
|
+
defaultOptions: []
|
|
589
|
+
});
|
|
590
|
+
function create$3(context) {
|
|
591
|
+
if (!context.sourceCode.text.includes("IntersectionObserver")) return {};
|
|
592
|
+
const fEntries = [];
|
|
593
|
+
const observers = [];
|
|
594
|
+
const oEntries = [];
|
|
595
|
+
const uEntries = [];
|
|
596
|
+
const dEntries = [];
|
|
597
|
+
return {
|
|
598
|
+
[":function"](node) {
|
|
599
|
+
const kind = getFunctionKind$1(node);
|
|
600
|
+
fEntries.push({
|
|
601
|
+
kind,
|
|
602
|
+
node
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
[":function:exit"]() {
|
|
606
|
+
fEntries.pop();
|
|
607
|
+
},
|
|
608
|
+
["CallExpression"](node) {
|
|
609
|
+
const unwrappedCallee = Extract.unwrap(node.callee);
|
|
610
|
+
if (unwrappedCallee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
611
|
+
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
612
|
+
if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
|
|
613
|
+
const { object } = unwrappedCallee;
|
|
614
|
+
match(getCallKind$3(context, node)).with("disconnect", () => {
|
|
615
|
+
dEntries.push({
|
|
616
|
+
kind: "IntersectionObserver",
|
|
617
|
+
callee: node.callee,
|
|
618
|
+
method: "disconnect",
|
|
619
|
+
node,
|
|
620
|
+
observer: object,
|
|
621
|
+
phase: fKind
|
|
622
|
+
});
|
|
623
|
+
}).with("observe", () => {
|
|
624
|
+
const [element] = node.arguments;
|
|
625
|
+
if (element == null) return;
|
|
626
|
+
oEntries.push({
|
|
627
|
+
kind: "IntersectionObserver",
|
|
628
|
+
callee: node.callee,
|
|
629
|
+
element,
|
|
630
|
+
method: "observe",
|
|
631
|
+
node,
|
|
632
|
+
observer: object,
|
|
633
|
+
phase: fKind
|
|
634
|
+
});
|
|
635
|
+
}).with("unobserve", () => {
|
|
636
|
+
const [element] = node.arguments;
|
|
637
|
+
if (element == null) return;
|
|
638
|
+
uEntries.push({
|
|
639
|
+
kind: "IntersectionObserver",
|
|
640
|
+
callee: node.callee,
|
|
641
|
+
element,
|
|
642
|
+
method: "unobserve",
|
|
643
|
+
node,
|
|
644
|
+
observer: object,
|
|
645
|
+
phase: fKind
|
|
646
|
+
});
|
|
647
|
+
}).otherwise(() => null);
|
|
648
|
+
},
|
|
649
|
+
["NewExpression"](node) {
|
|
650
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
651
|
+
if (fEntry == null) return;
|
|
652
|
+
if (!ComponentPhaseRelevance.has(fEntry.kind)) return;
|
|
653
|
+
if (!isNewIntersectionObserver(node)) return;
|
|
654
|
+
const id = resolveEnclosingAssignmentTarget(node);
|
|
655
|
+
if (id == null) {
|
|
656
|
+
context.report({
|
|
657
|
+
messageId: "unexpectedFloatingInstance",
|
|
658
|
+
node
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
observers.push({
|
|
663
|
+
id,
|
|
664
|
+
node,
|
|
665
|
+
phase: fEntry.kind,
|
|
666
|
+
phaseNode: fEntry.node
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
["Program:exit"]() {
|
|
670
|
+
for (const { id, node, phaseNode } of observers) {
|
|
671
|
+
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
|
|
672
|
+
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
|
|
673
|
+
const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
674
|
+
const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
675
|
+
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || isConditional$1(node);
|
|
676
|
+
const isPhaseNode = (node) => node === phaseNode;
|
|
677
|
+
if (oentries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
|
|
678
|
+
context.report({
|
|
679
|
+
messageId: "expectedDisconnectInControlFlow",
|
|
680
|
+
node
|
|
681
|
+
});
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
for (const oEntry of oentries) {
|
|
685
|
+
if (uentries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
|
|
686
|
+
context.report({
|
|
687
|
+
messageId: "expectedDisconnectOrUnobserveInCleanup",
|
|
688
|
+
node: oEntry.node
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/rules/no-leaked-interval/no-leaked-interval.ts
|
|
698
|
+
const RULE_NAME$2 = "no-leaked-interval";
|
|
699
|
+
function getCallKind$2(node) {
|
|
700
|
+
const callee = Extract.unwrap(node.callee);
|
|
701
|
+
switch (true) {
|
|
702
|
+
case callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(callee.name): return callee.name;
|
|
703
|
+
case callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("setInterval", "clearInterval"))(callee.property.name): return callee.property.name;
|
|
704
|
+
default: return "other";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
var no_leaked_interval_default = createRule({
|
|
708
|
+
meta: {
|
|
709
|
+
type: "problem",
|
|
710
|
+
docs: { description: "Enforces that every 'setInterval' in a component or custom hook has a corresponding 'clearInterval'." },
|
|
711
|
+
messages: {
|
|
712
|
+
expectedClearIntervalInCleanup: "A 'setInterval' created in '{{ kind }}' must be cleared with 'clearInterval' in the cleanup function.",
|
|
713
|
+
expectedIntervalId: "A 'setInterval' must be assigned to a variable for proper cleanup."
|
|
714
|
+
},
|
|
715
|
+
schema: []
|
|
716
|
+
},
|
|
717
|
+
name: RULE_NAME$2,
|
|
718
|
+
create: create$2,
|
|
719
|
+
defaultOptions: []
|
|
720
|
+
});
|
|
721
|
+
function create$2(context) {
|
|
722
|
+
if (!context.sourceCode.text.includes("setInterval")) return {};
|
|
723
|
+
const fEntries = [];
|
|
724
|
+
const sEntries = [];
|
|
725
|
+
const cEntries = [];
|
|
726
|
+
function isInverseEntry(a, b) {
|
|
727
|
+
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
[":function"](node) {
|
|
731
|
+
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
732
|
+
fEntries.push({
|
|
733
|
+
kind,
|
|
734
|
+
node
|
|
735
|
+
});
|
|
736
|
+
},
|
|
737
|
+
[":function:exit"]() {
|
|
738
|
+
fEntries.pop();
|
|
739
|
+
},
|
|
740
|
+
["CallExpression"](node) {
|
|
741
|
+
switch (getCallKind$2(node)) {
|
|
742
|
+
case "setInterval": {
|
|
743
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
744
|
+
if (fEntry == null) break;
|
|
745
|
+
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
746
|
+
const intervalIdNode = resolveEnclosingAssignmentTarget(node);
|
|
747
|
+
if (intervalIdNode == null) {
|
|
748
|
+
context.report({
|
|
749
|
+
messageId: "expectedIntervalId",
|
|
750
|
+
node
|
|
751
|
+
});
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
sEntries.push({
|
|
755
|
+
kind: "interval",
|
|
756
|
+
callee: node.callee,
|
|
757
|
+
node,
|
|
758
|
+
phase: fEntry.kind,
|
|
759
|
+
timerId: intervalIdNode
|
|
760
|
+
});
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "clearInterval": {
|
|
764
|
+
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
765
|
+
if (fEntry == null) break;
|
|
766
|
+
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
767
|
+
const [intervalIdNode] = node.arguments;
|
|
768
|
+
if (intervalIdNode == null) break;
|
|
769
|
+
cEntries.push({
|
|
770
|
+
kind: "interval",
|
|
771
|
+
callee: node.callee,
|
|
772
|
+
node,
|
|
773
|
+
phase: fEntry.kind,
|
|
774
|
+
timerId: intervalIdNode
|
|
775
|
+
});
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
["Program:exit"]() {
|
|
781
|
+
for (const sEntry of sEntries) {
|
|
782
|
+
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
783
|
+
switch (sEntry.phase) {
|
|
784
|
+
case "setup":
|
|
785
|
+
case "cleanup":
|
|
786
|
+
context.report({
|
|
787
|
+
data: { kind: "useEffect" },
|
|
788
|
+
messageId: "expectedClearIntervalInCleanup",
|
|
789
|
+
node: sEntry.node
|
|
790
|
+
});
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/rules/no-leaked-resize-observer/lib.ts
|
|
643
800
|
/**
|
|
644
801
|
* Check if a node is a conditional expression or control flow statement
|
|
645
802
|
* @param node The node to check
|
|
@@ -782,7 +939,8 @@ function create$1(context) {
|
|
|
782
939
|
},
|
|
783
940
|
["Program:exit"]() {
|
|
784
941
|
for (const { id, node, phaseNode } of observers) {
|
|
785
|
-
|
|
942
|
+
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
|
|
943
|
+
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
|
|
786
944
|
const oentries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
787
945
|
const uentries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
788
946
|
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || isConditional(node);
|
|
@@ -914,6 +1072,7 @@ const plugin = {
|
|
|
914
1072
|
rules: {
|
|
915
1073
|
"no-leaked-event-listener": no_leaked_event_listener_default,
|
|
916
1074
|
"no-leaked-fetch": no_leaked_fetch_default,
|
|
1075
|
+
"no-leaked-intersection-observer": no_leaked_intersection_observer_default,
|
|
917
1076
|
"no-leaked-interval": no_leaked_interval_default,
|
|
918
1077
|
"no-leaked-resize-observer": no_leaked_resize_observer_default,
|
|
919
1078
|
"no-leaked-timeout": no_leaked_timeout_default
|
|
@@ -932,6 +1091,7 @@ const name = "react-web-api/recommended";
|
|
|
932
1091
|
const rules = {
|
|
933
1092
|
"react-web-api/no-leaked-event-listener": "warn",
|
|
934
1093
|
"react-web-api/no-leaked-fetch": "warn",
|
|
1094
|
+
"react-web-api/no-leaked-intersection-observer": "warn",
|
|
935
1095
|
"react-web-api/no-leaked-interval": "warn",
|
|
936
1096
|
"react-web-api/no-leaked-resize-observer": "warn",
|
|
937
1097
|
"react-web-api/no-leaked-timeout": "warn"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-web-api",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for interacting with Web APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"@typescript-eslint/utils": "^8.61.0",
|
|
42
42
|
"birecord": "^0.1.1",
|
|
43
43
|
"ts-pattern": "^5.9.0",
|
|
44
|
-
"@eslint-react/
|
|
45
|
-
"@eslint-react/
|
|
46
|
-
"@eslint-react/
|
|
47
|
-
"@eslint-react/
|
|
48
|
-
"@eslint-react/
|
|
44
|
+
"@eslint-react/core": "5.9.0",
|
|
45
|
+
"@eslint-react/ast": "5.9.0",
|
|
46
|
+
"@eslint-react/eslint": "5.9.0",
|
|
47
|
+
"@eslint-react/var": "5.9.0",
|
|
48
|
+
"@eslint-react/shared": "5.9.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/react": "^19.2.17",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"eslint": "^10.4.1",
|
|
55
55
|
"tsdown": "^0.22.2",
|
|
56
56
|
"typescript": "6.0.3",
|
|
57
|
-
"@local/
|
|
58
|
-
"@local/
|
|
57
|
+
"@local/eff": "0.0.0",
|
|
58
|
+
"@local/configs": "0.0.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"eslint": "*",
|