eslint-plugin-react-web-api 5.20.3 → 5.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +155 -250
- package/package.json +6 -7
package/dist/index.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
|
|
2
|
-
import * as core from "@eslint-react/core";
|
|
3
|
-
import birecord from "birecord";
|
|
4
|
-
import { P, isMatching, match } from "ts-pattern";
|
|
5
2
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
6
3
|
import { Check, Compare, Extract, Traverse } from "@eslint-react/ast";
|
|
4
|
+
import { isUseEffectCleanupCallback, isUseEffectSetupCallback } from "@eslint-react/core";
|
|
7
5
|
import "@eslint-react/eslint";
|
|
8
6
|
import { isAssignmentTargetEqual, isInitializedFromReactNative, isValueEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
|
|
9
7
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
8
|
+
import { P, isMatching, match } from "ts-pattern";
|
|
10
9
|
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
11
10
|
|
|
12
11
|
//#region \0rolldown/runtime.js
|
|
@@ -28,14 +27,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
28
27
|
//#endregion
|
|
29
28
|
//#region package.json
|
|
30
29
|
var name$1 = "eslint-plugin-react-web-api";
|
|
31
|
-
var version = "5.20.
|
|
32
|
-
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/types/component-phase.ts
|
|
35
|
-
const ComponentPhaseRelevance = birecord({ setup: "cleanup" });
|
|
36
|
-
function getPhaseKindOfFunction(node) {
|
|
37
|
-
return match(node).when(core.isUseEffectSetupCallback, () => "setup").when(core.isUseEffectCleanupCallback, () => "cleanup").otherwise(() => null);
|
|
38
|
-
}
|
|
30
|
+
var version = "5.20.5";
|
|
39
31
|
|
|
40
32
|
//#endregion
|
|
41
33
|
//#region src/utils/create-rule.ts
|
|
@@ -101,11 +93,13 @@ function getOptions(context, node) {
|
|
|
101
93
|
const RULE_NAME$5 = "no-leaked-event-listener";
|
|
102
94
|
function getCallKind$5(node) {
|
|
103
95
|
const name = Extract.getCalleeName(node);
|
|
104
|
-
if (name != null && isMatching(P.union("addEventListener", "removeEventListener"
|
|
96
|
+
if (name != null && isMatching(P.union("addEventListener", "removeEventListener"))(name)) return name;
|
|
105
97
|
return "other";
|
|
106
98
|
}
|
|
107
|
-
function getFunctionKind$
|
|
108
|
-
|
|
99
|
+
function getFunctionKind$5(node) {
|
|
100
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
101
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
102
|
+
return "other";
|
|
109
103
|
}
|
|
110
104
|
var no_leaked_event_listener_default = createRule({
|
|
111
105
|
meta: {
|
|
@@ -127,7 +121,6 @@ function create$5(context) {
|
|
|
127
121
|
const fEntries = [];
|
|
128
122
|
const aEntries = [];
|
|
129
123
|
const rEntries = [];
|
|
130
|
-
const abortedSignals = [];
|
|
131
124
|
function isSameObject(a, b) {
|
|
132
125
|
switch (true) {
|
|
133
126
|
case a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression: return Compare.isEqual(a.object, b.object);
|
|
@@ -137,10 +130,10 @@ function create$5(context) {
|
|
|
137
130
|
function isInverseEntry(aEntry, rEntry) {
|
|
138
131
|
const { type: aType, callee: aCallee, capture: aCapture, listener: aListener, phase: aPhase } = aEntry;
|
|
139
132
|
const { type: rType, callee: rCallee, capture: rCapture, listener: rListener, phase: rPhase } = rEntry;
|
|
140
|
-
if (
|
|
133
|
+
if (aPhase !== "setup" || rPhase !== "cleanup") return false;
|
|
141
134
|
return isSameObject(aCallee, rCallee) && Compare.isEqual(aListener, rListener) && isValueEqual(context, aType, rType) && aCapture === rCapture;
|
|
142
135
|
}
|
|
143
|
-
function
|
|
136
|
+
function visitInlineFunction(node, callKind, options) {
|
|
144
137
|
const listener = node.arguments.at(1);
|
|
145
138
|
if (!Check.isFunction(listener)) return;
|
|
146
139
|
if (options.signal != null) return;
|
|
@@ -152,26 +145,21 @@ function create$5(context) {
|
|
|
152
145
|
}
|
|
153
146
|
return {
|
|
154
147
|
[":function"](node) {
|
|
155
|
-
|
|
156
|
-
fEntries.push({
|
|
157
|
-
kind,
|
|
158
|
-
node
|
|
159
|
-
});
|
|
148
|
+
fEntries.push(getFunctionKind$5(node));
|
|
160
149
|
},
|
|
161
150
|
[":function:exit"]() {
|
|
162
151
|
fEntries.pop();
|
|
163
152
|
},
|
|
164
153
|
["CallExpression"](node) {
|
|
165
|
-
const fKind = fEntries.findLast((
|
|
154
|
+
const fKind = fEntries.findLast((kind) => kind !== "other");
|
|
166
155
|
if (fKind == null) return;
|
|
167
|
-
if (!ComponentPhaseRelevance.has(fKind)) return;
|
|
168
156
|
const callee = Extract.unwrap(node.callee);
|
|
169
157
|
match(getCallKind$5(node)).with("addEventListener", (callKind) => {
|
|
170
158
|
if (callee.type === AST_NODE_TYPES.MemberExpression && Check.isIdentifier(callee.object) && isInitializedFromReactNative(callee.object.name, context.sourceCode.getScope(node))) return;
|
|
171
159
|
const [type, listener, options] = node.arguments;
|
|
172
160
|
if (type == null || listener == null) return;
|
|
173
161
|
const opts = options == null ? defaultOptions : getOptions(context, options);
|
|
174
|
-
|
|
162
|
+
visitInlineFunction(node, callKind, opts);
|
|
175
163
|
aEntries.push({
|
|
176
164
|
...opts,
|
|
177
165
|
type,
|
|
@@ -185,34 +173,27 @@ function create$5(context) {
|
|
|
185
173
|
const [type, listener, options] = node.arguments;
|
|
186
174
|
if (type == null || listener == null) return;
|
|
187
175
|
const opts = options == null ? defaultOptions : getOptions(context, options);
|
|
188
|
-
|
|
176
|
+
visitInlineFunction(node, callKind, opts);
|
|
189
177
|
rEntries.push({
|
|
190
|
-
...opts,
|
|
191
178
|
type,
|
|
192
179
|
callee,
|
|
180
|
+
capture: opts.capture,
|
|
193
181
|
listener,
|
|
194
182
|
method: "removeEventListener",
|
|
195
183
|
node,
|
|
196
184
|
phase: fKind
|
|
197
185
|
});
|
|
198
|
-
}).with("abort", () => {
|
|
199
|
-
abortedSignals.push(node.callee);
|
|
200
186
|
}).otherwise(() => null);
|
|
201
187
|
},
|
|
202
188
|
["Program:exit"]() {
|
|
203
189
|
for (const aEntry of aEntries) {
|
|
204
190
|
if (aEntry.signal != null) continue;
|
|
205
191
|
if (rEntries.some((rEntry) => isInverseEntry(aEntry, rEntry))) continue;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
messageId: "expectedRemoveEventListenerInCleanup",
|
|
212
|
-
node: aEntry.node
|
|
213
|
-
});
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
192
|
+
context.report({
|
|
193
|
+
data: { effectMethodKind: "useEffect" },
|
|
194
|
+
messageId: "expectedRemoveEventListenerInCleanup",
|
|
195
|
+
node: aEntry.node
|
|
196
|
+
});
|
|
216
197
|
}
|
|
217
198
|
}
|
|
218
199
|
};
|
|
@@ -220,16 +201,6 @@ function create$5(context) {
|
|
|
220
201
|
|
|
221
202
|
//#endregion
|
|
222
203
|
//#region src/rules/no-leaked-fetch/lib.ts
|
|
223
|
-
function findProperty(node, name) {
|
|
224
|
-
for (const property of node) {
|
|
225
|
-
if (property.type === AST_NODE_TYPES.Property && !property.computed && Check.isIdentifier(property.key, name)) return property;
|
|
226
|
-
if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
|
|
227
|
-
const found = findProperty(property.argument.properties, name);
|
|
228
|
-
if (found != null) return found;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
204
|
function resolveToObjectExpression(context, node) {
|
|
234
205
|
node = Extract.unwrap(node);
|
|
235
206
|
switch (node.type) {
|
|
@@ -248,11 +219,14 @@ function resolveToObjectExpression(context, node) {
|
|
|
248
219
|
//#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
|
|
249
220
|
const RULE_NAME$4 = "no-leaked-fetch";
|
|
250
221
|
function getCallKind$4(node) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
222
|
+
const name = Extract.getCalleeName(node);
|
|
223
|
+
if (name != null && isMatching(P.union("fetch", "abort"))(name)) return name;
|
|
224
|
+
return "other";
|
|
225
|
+
}
|
|
226
|
+
function getFunctionKind$4(node) {
|
|
227
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
228
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
229
|
+
return "other";
|
|
256
230
|
}
|
|
257
231
|
function getControllerFromSignal(context, node) {
|
|
258
232
|
node = Extract.unwrap(node);
|
|
@@ -294,7 +268,7 @@ function getFetchController(context, node) {
|
|
|
294
268
|
controller: null,
|
|
295
269
|
isParamSignal: false
|
|
296
270
|
};
|
|
297
|
-
const signalProp = findProperty(options.properties, "signal");
|
|
271
|
+
const signalProp = Extract.findProperty(options.properties, "signal");
|
|
298
272
|
if (signalProp?.type !== AST_NODE_TYPES.Property) return {
|
|
299
273
|
controller: null,
|
|
300
274
|
isParamSignal: false
|
|
@@ -328,42 +302,29 @@ function create$4(context) {
|
|
|
328
302
|
const abortEntries = [];
|
|
329
303
|
return {
|
|
330
304
|
[":function"](node) {
|
|
331
|
-
|
|
332
|
-
fEntries.push({
|
|
333
|
-
kind,
|
|
334
|
-
node
|
|
335
|
-
});
|
|
305
|
+
fEntries.push(getFunctionKind$4(node));
|
|
336
306
|
},
|
|
337
307
|
[":function:exit"]() {
|
|
338
308
|
fEntries.pop();
|
|
339
309
|
},
|
|
340
310
|
["CallExpression"](node) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (controller == null) break;
|
|
359
|
-
abortEntries.push({
|
|
360
|
-
controller,
|
|
361
|
-
node,
|
|
362
|
-
phase: fEntry.kind
|
|
363
|
-
});
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
311
|
+
match(getCallKind$4(node)).with("fetch", () => {
|
|
312
|
+
if (fEntries.at(-1) !== "setup") return;
|
|
313
|
+
const { controller, isParamSignal } = getFetchController(context, node);
|
|
314
|
+
fetchEntries.push({
|
|
315
|
+
controller,
|
|
316
|
+
isParamSignal,
|
|
317
|
+
node
|
|
318
|
+
});
|
|
319
|
+
}).with("abort", () => {
|
|
320
|
+
if (fEntries.findLast((kind) => kind !== "other") !== "cleanup") return;
|
|
321
|
+
const controller = getAbortController(node);
|
|
322
|
+
if (controller == null) return;
|
|
323
|
+
abortEntries.push({
|
|
324
|
+
controller,
|
|
325
|
+
node
|
|
326
|
+
});
|
|
327
|
+
}).otherwise(() => null);
|
|
367
328
|
},
|
|
368
329
|
["Program:exit"]() {
|
|
369
330
|
for (const fEntry of fetchEntries) {
|
|
@@ -864,18 +825,18 @@ const takeWhile = dual(2, (xs, pred) => {
|
|
|
864
825
|
|
|
865
826
|
//#endregion
|
|
866
827
|
//#region src/rules/no-leaked-intersection-observer/lib.ts
|
|
867
|
-
function
|
|
828
|
+
function isNewObserver$1(node, name) {
|
|
868
829
|
if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
|
|
869
830
|
const callee = Extract.unwrap(node.callee);
|
|
870
|
-
return Check.isIdentifier(callee,
|
|
831
|
+
return Check.isIdentifier(callee, name);
|
|
871
832
|
}
|
|
872
|
-
function isFromObserver$1(context, node) {
|
|
833
|
+
function isFromObserver$1(context, node, name) {
|
|
873
834
|
switch (true) {
|
|
874
835
|
case Check.isIdentifier(node): {
|
|
875
836
|
const initNode = resolve(context, node);
|
|
876
|
-
return
|
|
837
|
+
return isNewObserver$1(initNode == null ? null : Extract.unwrap(initNode), name);
|
|
877
838
|
}
|
|
878
|
-
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object);
|
|
839
|
+
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver$1(context, node.object, name);
|
|
879
840
|
default: return false;
|
|
880
841
|
}
|
|
881
842
|
}
|
|
@@ -885,13 +846,15 @@ function isFromObserver$1(context, node) {
|
|
|
885
846
|
const RULE_NAME$3 = "no-leaked-intersection-observer";
|
|
886
847
|
function getCallKind$3(context, node) {
|
|
887
848
|
const callee = Extract.unwrap(node.callee);
|
|
888
|
-
if (callee.type !== AST_NODE_TYPES.
|
|
849
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
|
|
889
850
|
const name = Extract.getCalleeName(node);
|
|
890
|
-
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee)) return name;
|
|
851
|
+
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver$1(context, callee, "IntersectionObserver")) return name;
|
|
891
852
|
return "other";
|
|
892
853
|
}
|
|
893
|
-
function getFunctionKind$
|
|
894
|
-
|
|
854
|
+
function getFunctionKind$3(node) {
|
|
855
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
856
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
857
|
+
return "other";
|
|
895
858
|
}
|
|
896
859
|
var no_leaked_intersection_observer_default = createRule({
|
|
897
860
|
meta: {
|
|
@@ -917,7 +880,7 @@ function create$3(context) {
|
|
|
917
880
|
const dEntries = [];
|
|
918
881
|
return {
|
|
919
882
|
[":function"](node) {
|
|
920
|
-
const kind = getFunctionKind$
|
|
883
|
+
const kind = getFunctionKind$3(node);
|
|
921
884
|
fEntries.push({
|
|
922
885
|
kind,
|
|
923
886
|
node
|
|
@@ -927,51 +890,40 @@ function create$3(context) {
|
|
|
927
890
|
fEntries.pop();
|
|
928
891
|
},
|
|
929
892
|
["CallExpression"](node) {
|
|
893
|
+
if (fEntries.findLast((x) => x.kind !== "other")?.kind == null) return;
|
|
930
894
|
const callee = Extract.unwrap(node.callee);
|
|
931
895
|
if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
932
|
-
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
933
|
-
if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
|
|
934
896
|
const { object } = callee;
|
|
935
897
|
match(getCallKind$3(context, node)).with("disconnect", () => {
|
|
936
898
|
dEntries.push({
|
|
937
|
-
kind: "IntersectionObserver",
|
|
938
|
-
callee,
|
|
939
899
|
method: "disconnect",
|
|
940
900
|
node,
|
|
941
|
-
observer: object
|
|
942
|
-
phase: fKind
|
|
901
|
+
observer: object
|
|
943
902
|
});
|
|
944
903
|
}).with("observe", () => {
|
|
945
904
|
const [element] = node.arguments;
|
|
946
905
|
if (element == null) return;
|
|
947
906
|
oEntries.push({
|
|
948
|
-
kind: "IntersectionObserver",
|
|
949
|
-
callee,
|
|
950
907
|
element,
|
|
951
908
|
method: "observe",
|
|
952
909
|
node,
|
|
953
|
-
observer: object
|
|
954
|
-
phase: fKind
|
|
910
|
+
observer: object
|
|
955
911
|
});
|
|
956
912
|
}).with("unobserve", () => {
|
|
957
913
|
const [element] = node.arguments;
|
|
958
914
|
if (element == null) return;
|
|
959
915
|
uEntries.push({
|
|
960
|
-
kind: "IntersectionObserver",
|
|
961
|
-
callee,
|
|
962
916
|
element,
|
|
963
917
|
method: "unobserve",
|
|
964
918
|
node,
|
|
965
|
-
observer: object
|
|
966
|
-
phase: fKind
|
|
919
|
+
observer: object
|
|
967
920
|
});
|
|
968
921
|
}).otherwise(() => null);
|
|
969
922
|
},
|
|
970
923
|
["NewExpression"](node) {
|
|
971
924
|
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
972
925
|
if (fEntry == null) return;
|
|
973
|
-
if (!
|
|
974
|
-
if (!isNewIntersectionObserver(node)) return;
|
|
926
|
+
if (!isNewObserver$1(node, "IntersectionObserver")) return;
|
|
975
927
|
const id = resolveEnclosingAssignmentTarget(node);
|
|
976
928
|
if (id == null) {
|
|
977
929
|
context.report({
|
|
@@ -983,7 +935,6 @@ function create$3(context) {
|
|
|
983
935
|
observers.push({
|
|
984
936
|
id,
|
|
985
937
|
node,
|
|
986
|
-
phase: fEntry.kind,
|
|
987
938
|
phaseNode: fEntry.node
|
|
988
939
|
});
|
|
989
940
|
},
|
|
@@ -991,19 +942,19 @@ function create$3(context) {
|
|
|
991
942
|
for (const { id, node, phaseNode } of observers) {
|
|
992
943
|
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
|
|
993
944
|
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
|
|
994
|
-
const
|
|
995
|
-
const
|
|
945
|
+
const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
946
|
+
const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
996
947
|
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
|
|
997
948
|
const isPhaseNode = (node) => node === phaseNode;
|
|
998
|
-
if (
|
|
949
|
+
if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
|
|
999
950
|
context.report({
|
|
1000
951
|
messageId: "expectedDisconnectInControlFlow",
|
|
1001
952
|
node
|
|
1002
953
|
});
|
|
1003
954
|
continue;
|
|
1004
955
|
}
|
|
1005
|
-
for (const oEntry of
|
|
1006
|
-
if (
|
|
956
|
+
for (const oEntry of matchedOEntries) {
|
|
957
|
+
if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
|
|
1007
958
|
context.report({
|
|
1008
959
|
messageId: "expectedDisconnectOrUnobserveInCleanup",
|
|
1009
960
|
node: oEntry.node
|
|
@@ -1022,6 +973,11 @@ function getCallKind$2(node) {
|
|
|
1022
973
|
if (name != null && isMatching(P.union("setInterval", "clearInterval"))(name)) return name;
|
|
1023
974
|
return "other";
|
|
1024
975
|
}
|
|
976
|
+
function getFunctionKind$2(node) {
|
|
977
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
978
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
979
|
+
return "other";
|
|
980
|
+
}
|
|
1025
981
|
var no_leaked_interval_default = createRule({
|
|
1026
982
|
meta: {
|
|
1027
983
|
type: "problem",
|
|
@@ -1046,68 +1002,43 @@ function create$2(context) {
|
|
|
1046
1002
|
}
|
|
1047
1003
|
return {
|
|
1048
1004
|
[":function"](node) {
|
|
1049
|
-
|
|
1050
|
-
fEntries.push({
|
|
1051
|
-
kind,
|
|
1052
|
-
node
|
|
1053
|
-
});
|
|
1005
|
+
fEntries.push(getFunctionKind$2(node));
|
|
1054
1006
|
},
|
|
1055
1007
|
[":function:exit"]() {
|
|
1056
1008
|
fEntries.pop();
|
|
1057
1009
|
},
|
|
1058
1010
|
["CallExpression"](node) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
context.report({
|
|
1067
|
-
messageId: "expectedIntervalId",
|
|
1068
|
-
node
|
|
1069
|
-
});
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
|
-
sEntries.push({
|
|
1073
|
-
kind: "interval",
|
|
1074
|
-
callee: node.callee,
|
|
1075
|
-
node,
|
|
1076
|
-
phase: fEntry.kind,
|
|
1077
|
-
timerId: intervalIdNode
|
|
1078
|
-
});
|
|
1079
|
-
break;
|
|
1080
|
-
}
|
|
1081
|
-
case "clearInterval": {
|
|
1082
|
-
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
1083
|
-
if (fEntry == null) break;
|
|
1084
|
-
if (!ComponentPhaseRelevance.has(fEntry.kind)) break;
|
|
1085
|
-
const [intervalIdNode] = node.arguments;
|
|
1086
|
-
if (intervalIdNode == null) break;
|
|
1087
|
-
cEntries.push({
|
|
1088
|
-
kind: "interval",
|
|
1089
|
-
callee: node.callee,
|
|
1090
|
-
node,
|
|
1091
|
-
phase: fEntry.kind,
|
|
1092
|
-
timerId: intervalIdNode
|
|
1011
|
+
if (fEntries.findLast((kind) => kind !== "other") == null) return;
|
|
1012
|
+
match(getCallKind$2(node)).with("setInterval", () => {
|
|
1013
|
+
const intervalIdNode = resolveEnclosingAssignmentTarget(node);
|
|
1014
|
+
if (intervalIdNode == null) {
|
|
1015
|
+
context.report({
|
|
1016
|
+
messageId: "expectedIntervalId",
|
|
1017
|
+
node
|
|
1093
1018
|
});
|
|
1094
|
-
|
|
1019
|
+
return;
|
|
1095
1020
|
}
|
|
1096
|
-
|
|
1021
|
+
sEntries.push({
|
|
1022
|
+
node,
|
|
1023
|
+
timerId: intervalIdNode
|
|
1024
|
+
});
|
|
1025
|
+
}).with("clearInterval", () => {
|
|
1026
|
+
const [intervalIdNode] = node.arguments;
|
|
1027
|
+
if (intervalIdNode == null) return;
|
|
1028
|
+
cEntries.push({
|
|
1029
|
+
node,
|
|
1030
|
+
timerId: intervalIdNode
|
|
1031
|
+
});
|
|
1032
|
+
}).otherwise(() => null);
|
|
1097
1033
|
},
|
|
1098
1034
|
["Program:exit"]() {
|
|
1099
1035
|
for (const sEntry of sEntries) {
|
|
1100
1036
|
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
messageId: "expectedClearIntervalInCleanup",
|
|
1107
|
-
node: sEntry.node
|
|
1108
|
-
});
|
|
1109
|
-
continue;
|
|
1110
|
-
}
|
|
1037
|
+
context.report({
|
|
1038
|
+
data: { kind: "useEffect" },
|
|
1039
|
+
messageId: "expectedClearIntervalInCleanup",
|
|
1040
|
+
node: sEntry.node
|
|
1041
|
+
});
|
|
1111
1042
|
}
|
|
1112
1043
|
}
|
|
1113
1044
|
};
|
|
@@ -1115,18 +1046,18 @@ function create$2(context) {
|
|
|
1115
1046
|
|
|
1116
1047
|
//#endregion
|
|
1117
1048
|
//#region src/rules/no-leaked-resize-observer/lib.ts
|
|
1118
|
-
function
|
|
1049
|
+
function isNewObserver(node, name) {
|
|
1119
1050
|
if (node?.type !== AST_NODE_TYPES.NewExpression) return false;
|
|
1120
1051
|
const callee = Extract.unwrap(node.callee);
|
|
1121
|
-
return Check.isIdentifier(callee,
|
|
1052
|
+
return Check.isIdentifier(callee, name);
|
|
1122
1053
|
}
|
|
1123
|
-
function isFromObserver(context, node) {
|
|
1054
|
+
function isFromObserver(context, node, name) {
|
|
1124
1055
|
switch (true) {
|
|
1125
1056
|
case Check.isIdentifier(node): {
|
|
1126
1057
|
const initNode = resolve(context, node);
|
|
1127
|
-
return
|
|
1058
|
+
return isNewObserver(initNode == null ? null : Extract.unwrap(initNode), name);
|
|
1128
1059
|
}
|
|
1129
|
-
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
|
|
1060
|
+
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object, name);
|
|
1130
1061
|
default: return false;
|
|
1131
1062
|
}
|
|
1132
1063
|
}
|
|
@@ -1136,13 +1067,15 @@ function isFromObserver(context, node) {
|
|
|
1136
1067
|
const RULE_NAME$1 = "no-leaked-resize-observer";
|
|
1137
1068
|
function getCallKind$1(context, node) {
|
|
1138
1069
|
const callee = Extract.unwrap(node.callee);
|
|
1139
|
-
if (callee.type !== AST_NODE_TYPES.
|
|
1070
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return "other";
|
|
1140
1071
|
const name = Extract.getCalleeName(node);
|
|
1141
|
-
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee)) return name;
|
|
1072
|
+
if (name != null && isMatching(P.union("observe", "unobserve", "disconnect"))(name) && isFromObserver(context, callee, "ResizeObserver")) return name;
|
|
1142
1073
|
return "other";
|
|
1143
1074
|
}
|
|
1144
|
-
function getFunctionKind(node) {
|
|
1145
|
-
|
|
1075
|
+
function getFunctionKind$1(node) {
|
|
1076
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
1077
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
1078
|
+
return "other";
|
|
1146
1079
|
}
|
|
1147
1080
|
var no_leaked_resize_observer_default = createRule({
|
|
1148
1081
|
meta: {
|
|
@@ -1168,7 +1101,7 @@ function create$1(context) {
|
|
|
1168
1101
|
const dEntries = [];
|
|
1169
1102
|
return {
|
|
1170
1103
|
[":function"](node) {
|
|
1171
|
-
const kind = getFunctionKind(node);
|
|
1104
|
+
const kind = getFunctionKind$1(node);
|
|
1172
1105
|
fEntries.push({
|
|
1173
1106
|
kind,
|
|
1174
1107
|
node
|
|
@@ -1178,51 +1111,40 @@ function create$1(context) {
|
|
|
1178
1111
|
fEntries.pop();
|
|
1179
1112
|
},
|
|
1180
1113
|
["CallExpression"](node) {
|
|
1114
|
+
if (fEntries.findLast((x) => x.kind !== "other")?.kind == null) return;
|
|
1181
1115
|
const callee = Extract.unwrap(node.callee);
|
|
1182
1116
|
if (callee.type !== AST_NODE_TYPES.MemberExpression) return;
|
|
1183
|
-
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
1184
|
-
if (fKind == null || !ComponentPhaseRelevance.has(fKind)) return;
|
|
1185
1117
|
const { object } = callee;
|
|
1186
1118
|
match(getCallKind$1(context, node)).with("disconnect", () => {
|
|
1187
1119
|
dEntries.push({
|
|
1188
|
-
kind: "ResizeObserver",
|
|
1189
|
-
callee,
|
|
1190
1120
|
method: "disconnect",
|
|
1191
1121
|
node,
|
|
1192
|
-
observer: object
|
|
1193
|
-
phase: fKind
|
|
1122
|
+
observer: object
|
|
1194
1123
|
});
|
|
1195
1124
|
}).with("observe", () => {
|
|
1196
1125
|
const [element] = node.arguments;
|
|
1197
1126
|
if (element == null) return;
|
|
1198
1127
|
oEntries.push({
|
|
1199
|
-
kind: "ResizeObserver",
|
|
1200
|
-
callee,
|
|
1201
1128
|
element,
|
|
1202
1129
|
method: "observe",
|
|
1203
1130
|
node,
|
|
1204
|
-
observer: object
|
|
1205
|
-
phase: fKind
|
|
1131
|
+
observer: object
|
|
1206
1132
|
});
|
|
1207
1133
|
}).with("unobserve", () => {
|
|
1208
1134
|
const [element] = node.arguments;
|
|
1209
1135
|
if (element == null) return;
|
|
1210
1136
|
uEntries.push({
|
|
1211
|
-
kind: "ResizeObserver",
|
|
1212
|
-
callee,
|
|
1213
1137
|
element,
|
|
1214
1138
|
method: "unobserve",
|
|
1215
1139
|
node,
|
|
1216
|
-
observer: object
|
|
1217
|
-
phase: fKind
|
|
1140
|
+
observer: object
|
|
1218
1141
|
});
|
|
1219
1142
|
}).otherwise(() => null);
|
|
1220
1143
|
},
|
|
1221
1144
|
["NewExpression"](node) {
|
|
1222
1145
|
const fEntry = fEntries.findLast((x) => x.kind !== "other");
|
|
1223
1146
|
if (fEntry == null) return;
|
|
1224
|
-
if (!
|
|
1225
|
-
if (!isNewResizeObserver(node)) return;
|
|
1147
|
+
if (!isNewObserver(node, "ResizeObserver")) return;
|
|
1226
1148
|
const id = resolveEnclosingAssignmentTarget(node);
|
|
1227
1149
|
if (id == null) {
|
|
1228
1150
|
context.report({
|
|
@@ -1234,7 +1156,6 @@ function create$1(context) {
|
|
|
1234
1156
|
observers.push({
|
|
1235
1157
|
id,
|
|
1236
1158
|
node,
|
|
1237
|
-
phase: fEntry.kind,
|
|
1238
1159
|
phaseNode: fEntry.node
|
|
1239
1160
|
});
|
|
1240
1161
|
},
|
|
@@ -1242,19 +1163,19 @@ function create$1(context) {
|
|
|
1242
1163
|
for (const { id, node, phaseNode } of observers) {
|
|
1243
1164
|
const isInsideObserverCallback = (e) => Traverse.findParent(e.node, (n) => n === node) != null;
|
|
1244
1165
|
if (dEntries.some((e) => !isInsideObserverCallback(e) && isAssignmentTargetEqual(context, e.observer, id))) continue;
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1166
|
+
const matchedOEntries = oEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
1167
|
+
const matchedUEntries = uEntries.filter((e) => isAssignmentTargetEqual(context, e.observer, id));
|
|
1247
1168
|
const isDynamic = (node) => node?.type === AST_NODE_TYPES.CallExpression || Check.isConditional(node);
|
|
1248
1169
|
const isPhaseNode = (node) => node === phaseNode;
|
|
1249
|
-
if (
|
|
1170
|
+
if (matchedOEntries.some((e) => !isPhaseNode(Traverse.findParent(e.node, or(isDynamic, isPhaseNode))))) {
|
|
1250
1171
|
context.report({
|
|
1251
1172
|
messageId: "expectedDisconnectInControlFlow",
|
|
1252
1173
|
node
|
|
1253
1174
|
});
|
|
1254
1175
|
continue;
|
|
1255
1176
|
}
|
|
1256
|
-
for (const oEntry of
|
|
1257
|
-
if (
|
|
1177
|
+
for (const oEntry of matchedOEntries) {
|
|
1178
|
+
if (matchedUEntries.some((uEntry) => isAssignmentTargetEqual(context, uEntry.element, oEntry.element))) continue;
|
|
1258
1179
|
context.report({
|
|
1259
1180
|
messageId: "expectedDisconnectOrUnobserveInCleanup",
|
|
1260
1181
|
node: oEntry.node
|
|
@@ -1273,6 +1194,11 @@ function getCallKind(node) {
|
|
|
1273
1194
|
if (name != null && isMatching(P.union("setTimeout", "clearTimeout"))(name)) return name;
|
|
1274
1195
|
return "other";
|
|
1275
1196
|
}
|
|
1197
|
+
function getFunctionKind(node) {
|
|
1198
|
+
if (isUseEffectSetupCallback(node)) return "setup";
|
|
1199
|
+
if (isUseEffectCleanupCallback(node)) return "cleanup";
|
|
1200
|
+
return "other";
|
|
1201
|
+
}
|
|
1276
1202
|
var no_leaked_timeout_default = createRule({
|
|
1277
1203
|
meta: {
|
|
1278
1204
|
type: "problem",
|
|
@@ -1291,70 +1217,49 @@ function create(context) {
|
|
|
1291
1217
|
if (!context.sourceCode.text.includes("setTimeout")) return {};
|
|
1292
1218
|
const fEntries = [];
|
|
1293
1219
|
const sEntries = [];
|
|
1294
|
-
const
|
|
1220
|
+
const cEntries = [];
|
|
1295
1221
|
function isInverseEntry(a, b) {
|
|
1296
1222
|
return isAssignmentTargetEqual(context, a.timerId, b.timerId);
|
|
1297
1223
|
}
|
|
1298
1224
|
return {
|
|
1299
1225
|
[":function"](node) {
|
|
1300
|
-
|
|
1301
|
-
fEntries.push({
|
|
1302
|
-
kind,
|
|
1303
|
-
node
|
|
1304
|
-
});
|
|
1226
|
+
fEntries.push(getFunctionKind(node));
|
|
1305
1227
|
},
|
|
1306
1228
|
[":function:exit"]() {
|
|
1307
1229
|
fEntries.pop();
|
|
1308
1230
|
},
|
|
1309
1231
|
["CallExpression"](node) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
messageId: "expectedTimeoutId",
|
|
1318
|
-
node
|
|
1319
|
-
});
|
|
1320
|
-
break;
|
|
1321
|
-
}
|
|
1322
|
-
sEntries.push({
|
|
1323
|
-
kind: "timeout",
|
|
1324
|
-
callee: node.callee,
|
|
1325
|
-
node,
|
|
1326
|
-
phase: fEntry.kind,
|
|
1327
|
-
timerId: timeoutIdNode
|
|
1328
|
-
});
|
|
1329
|
-
break;
|
|
1330
|
-
}
|
|
1331
|
-
case "clearTimeout": {
|
|
1332
|
-
const [timeoutIdNode] = node.arguments;
|
|
1333
|
-
if (timeoutIdNode == null) break;
|
|
1334
|
-
rEntries.push({
|
|
1335
|
-
kind: "timeout",
|
|
1336
|
-
callee: node.callee,
|
|
1337
|
-
node,
|
|
1338
|
-
phase: fEntry.kind,
|
|
1339
|
-
timerId: timeoutIdNode
|
|
1232
|
+
if (fEntries.findLast((kind) => kind !== "other") == null) return;
|
|
1233
|
+
match(getCallKind(node)).with("setTimeout", () => {
|
|
1234
|
+
const timeoutIdNode = resolveEnclosingAssignmentTarget(node);
|
|
1235
|
+
if (timeoutIdNode == null) {
|
|
1236
|
+
context.report({
|
|
1237
|
+
messageId: "expectedTimeoutId",
|
|
1238
|
+
node
|
|
1340
1239
|
});
|
|
1341
|
-
|
|
1240
|
+
return;
|
|
1342
1241
|
}
|
|
1343
|
-
|
|
1242
|
+
sEntries.push({
|
|
1243
|
+
node,
|
|
1244
|
+
timerId: timeoutIdNode
|
|
1245
|
+
});
|
|
1246
|
+
}).with("clearTimeout", () => {
|
|
1247
|
+
const [timeoutIdNode] = node.arguments;
|
|
1248
|
+
if (timeoutIdNode == null) return;
|
|
1249
|
+
cEntries.push({
|
|
1250
|
+
node,
|
|
1251
|
+
timerId: timeoutIdNode
|
|
1252
|
+
});
|
|
1253
|
+
}).otherwise(() => null);
|
|
1344
1254
|
},
|
|
1345
1255
|
["Program:exit"]() {
|
|
1346
1256
|
for (const sEntry of sEntries) {
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
messageId: "expectedClearTimeoutInCleanup",
|
|
1354
|
-
node: sEntry.node
|
|
1355
|
-
});
|
|
1356
|
-
continue;
|
|
1357
|
-
}
|
|
1257
|
+
if (cEntries.some((cEntry) => isInverseEntry(sEntry, cEntry))) continue;
|
|
1258
|
+
context.report({
|
|
1259
|
+
data: { kind: "useEffect" },
|
|
1260
|
+
messageId: "expectedClearTimeoutInCleanup",
|
|
1261
|
+
node: sEntry.node
|
|
1262
|
+
});
|
|
1358
1263
|
}
|
|
1359
1264
|
}
|
|
1360
1265
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-web-api",
|
|
3
|
-
"version": "5.20.
|
|
3
|
+
"version": "5.20.5",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for interacting with Web APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -37,14 +37,13 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@eslint-react/ast": "5.20.
|
|
41
|
-
"@eslint-react/core": "5.20.
|
|
42
|
-
"@eslint-react/eslint": "5.20.
|
|
43
|
-
"@eslint-react/shared": "5.20.
|
|
44
|
-
"@eslint-react/var": "5.20.
|
|
40
|
+
"@eslint-react/ast": "5.20.5",
|
|
41
|
+
"@eslint-react/core": "5.20.5",
|
|
42
|
+
"@eslint-react/eslint": "5.20.5",
|
|
43
|
+
"@eslint-react/shared": "5.20.5",
|
|
44
|
+
"@eslint-react/var": "5.20.5",
|
|
45
45
|
"@typescript-eslint/types": "^8.70.0",
|
|
46
46
|
"@typescript-eslint/utils": "^8.70.0",
|
|
47
|
-
"birecord": "^0.1.2",
|
|
48
47
|
"ts-pattern": "^5.9.0"
|
|
49
48
|
},
|
|
50
49
|
"devDependencies": {
|