eslint-plugin-react-web-api 5.2.5-next.1 → 5.3.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +209 -35
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -4,10 +4,10 @@ import * as core from "@eslint-react/core";
|
|
|
4
4
|
import { merge } from "@eslint-react/eslint";
|
|
5
5
|
import { isAssignmentTargetEqual, isValueEqual, resolve, resolveEnclosingAssignmentTarget } from "@eslint-react/var";
|
|
6
6
|
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
7
|
-
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
8
7
|
import { P, isMatching, match } from "ts-pattern";
|
|
9
8
|
import birecord from "birecord";
|
|
10
9
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
10
|
+
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
|
|
11
11
|
|
|
12
12
|
//#region \0rolldown/runtime.js
|
|
13
13
|
var __defProp = Object.defineProperty;
|
|
@@ -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.3.0-next.0";
|
|
32
32
|
|
|
33
33
|
//#endregion
|
|
34
34
|
//#region src/types/component-phase.ts
|
|
@@ -46,46 +46,33 @@ const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
|
46
46
|
|
|
47
47
|
//#endregion
|
|
48
48
|
//#region src/rules/no-leaked-event-listener/lib.ts
|
|
49
|
-
function findProperty(of, named) {
|
|
49
|
+
function findProperty$1(of, named) {
|
|
50
50
|
for (const property of of) {
|
|
51
51
|
if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === named) return property;
|
|
52
52
|
if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
|
|
53
|
-
const found = findProperty(property.argument.properties, named);
|
|
53
|
+
const found = findProperty$1(property.argument.properties, named);
|
|
54
54
|
if (found != null) return found;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
|
|
62
|
-
const RULE_NAME$3 = "no-leaked-event-listener";
|
|
63
|
-
const defaultOptions = {
|
|
64
|
-
capture: false,
|
|
65
|
-
signal: null
|
|
66
|
-
};
|
|
67
|
-
function getCallKind$3(node) {
|
|
68
|
-
switch (true) {
|
|
69
|
-
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.name): return node.callee.name;
|
|
70
|
-
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.property.name): return node.callee.property.name;
|
|
71
|
-
default: return "other";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function getFunctionKind$1(node) {
|
|
75
|
-
return getPhaseKindOfFunction(node) ?? "other";
|
|
76
|
-
}
|
|
77
59
|
function getSignalValueExpression(context, node) {
|
|
78
60
|
if (node == null) return null;
|
|
79
61
|
switch (node.type) {
|
|
80
62
|
case AST_NODE_TYPES.Identifier: {
|
|
81
63
|
const resolved = resolve(context, node);
|
|
82
|
-
|
|
83
|
-
|
|
64
|
+
const unwrapped = resolved == null ? null : Extract.unwrap(resolved);
|
|
65
|
+
if (unwrapped != null && Check.isFunction(unwrapped)) return node;
|
|
66
|
+
return getSignalValueExpression(context, unwrapped);
|
|
84
67
|
}
|
|
85
68
|
case AST_NODE_TYPES.MemberExpression: return node;
|
|
86
69
|
default: return null;
|
|
87
70
|
}
|
|
88
71
|
}
|
|
72
|
+
const defaultOptions = {
|
|
73
|
+
capture: false,
|
|
74
|
+
signal: null
|
|
75
|
+
};
|
|
89
76
|
function getOptions(context, node) {
|
|
90
77
|
const initialScope = context.sourceCode.getScope(node);
|
|
91
78
|
function getOpts(node) {
|
|
@@ -100,14 +87,14 @@ function getOptions(context, node) {
|
|
|
100
87
|
capture: Boolean(node.value)
|
|
101
88
|
};
|
|
102
89
|
case AST_NODE_TYPES.ObjectExpression: {
|
|
103
|
-
const vCapture = match(findProperty(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
|
|
90
|
+
const vCapture = match(findProperty$1(node.properties, "capture")).with(P.nullish, () => false).with({ type: AST_NODE_TYPES.Property }, (prop) => {
|
|
104
91
|
const value = prop.value;
|
|
105
92
|
switch (value.type) {
|
|
106
93
|
case AST_NODE_TYPES.Literal: return Boolean(value.value);
|
|
107
94
|
default: return Boolean(getStaticValue(value, initialScope)?.value);
|
|
108
95
|
}
|
|
109
96
|
}).otherwise(() => false);
|
|
110
|
-
const pSignal = findProperty(node.properties, "signal");
|
|
97
|
+
const pSignal = findProperty$1(node.properties, "signal");
|
|
111
98
|
return {
|
|
112
99
|
capture: vCapture,
|
|
113
100
|
signal: pSignal?.type === AST_NODE_TYPES.Property ? getSignalValueExpression(context, pSignal.value) : null
|
|
@@ -118,6 +105,20 @@ function getOptions(context, node) {
|
|
|
118
105
|
}
|
|
119
106
|
return getOpts(node);
|
|
120
107
|
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/rules/no-leaked-event-listener/no-leaked-event-listener.ts
|
|
111
|
+
const RULE_NAME$4 = "no-leaked-event-listener";
|
|
112
|
+
function getCallKind$4(node) {
|
|
113
|
+
switch (true) {
|
|
114
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.name): return node.callee.name;
|
|
115
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && isMatching(P.union("addEventListener", "removeEventListener", "abort"))(node.callee.property.name): return node.callee.property.name;
|
|
116
|
+
default: return "other";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getFunctionKind$1(node) {
|
|
120
|
+
return getPhaseKindOfFunction(node) ?? "other";
|
|
121
|
+
}
|
|
121
122
|
var no_leaked_event_listener_default = createRule({
|
|
122
123
|
meta: {
|
|
123
124
|
type: "problem",
|
|
@@ -128,11 +129,11 @@ var no_leaked_event_listener_default = createRule({
|
|
|
128
129
|
},
|
|
129
130
|
schema: []
|
|
130
131
|
},
|
|
131
|
-
name: RULE_NAME$
|
|
132
|
-
create: create$
|
|
132
|
+
name: RULE_NAME$4,
|
|
133
|
+
create: create$4,
|
|
133
134
|
defaultOptions: []
|
|
134
135
|
});
|
|
135
|
-
function create$
|
|
136
|
+
function create$4(context) {
|
|
136
137
|
if (!context.sourceCode.text.includes("addEventListener")) return {};
|
|
137
138
|
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
138
139
|
const fEntries = [];
|
|
@@ -176,7 +177,7 @@ function create$3(context) {
|
|
|
176
177
|
const fKind = fEntries.findLast((x) => x.kind !== "other")?.kind;
|
|
177
178
|
if (fKind == null) return;
|
|
178
179
|
if (!ComponentPhaseRelevance.has(fKind)) return;
|
|
179
|
-
match(getCallKind$
|
|
180
|
+
match(getCallKind$4(node)).with("addEventListener", (callKind) => {
|
|
180
181
|
if (node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.object.type === AST_NODE_TYPES.Identifier && core.isAPIFromReactNative(node.callee.object.name, context.sourceCode.getScope(node))) return;
|
|
181
182
|
const [type, listener, options] = node.arguments;
|
|
182
183
|
if (type == null || listener == null) return;
|
|
@@ -230,6 +231,174 @@ function create$3(context) {
|
|
|
230
231
|
});
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/rules/no-leaked-fetch/lib.ts
|
|
236
|
+
function findProperty(of, named) {
|
|
237
|
+
for (const property of of) {
|
|
238
|
+
if (property.type === AST_NODE_TYPES.Property && Extract.getPropertyName(property.key) === named) return property;
|
|
239
|
+
if (property.type === AST_NODE_TYPES.SpreadElement && property.argument.type === AST_NODE_TYPES.ObjectExpression) {
|
|
240
|
+
const found = findProperty(property.argument.properties, named);
|
|
241
|
+
if (found != null) return found;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function resolveToObjectExpression(context, node) {
|
|
247
|
+
node = Extract.unwrap(node);
|
|
248
|
+
switch (node.type) {
|
|
249
|
+
case AST_NODE_TYPES.ObjectExpression: return node;
|
|
250
|
+
case AST_NODE_TYPES.Identifier: {
|
|
251
|
+
let resolved = resolve(context, node);
|
|
252
|
+
resolved = resolved == null ? null : Extract.unwrap(resolved);
|
|
253
|
+
if (resolved?.type === AST_NODE_TYPES.ObjectExpression) return resolved;
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
default: return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/rules/no-leaked-fetch/no-leaked-fetch.ts
|
|
262
|
+
const RULE_NAME$3 = "no-leaked-fetch";
|
|
263
|
+
function getCallKind$3(node) {
|
|
264
|
+
switch (true) {
|
|
265
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "fetch": return "fetch";
|
|
266
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "fetch": return "fetch";
|
|
267
|
+
case node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "abort": return "abort";
|
|
268
|
+
case node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "abort": return "abort";
|
|
269
|
+
default: return "other";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function getControllerFromSignal(context, node) {
|
|
273
|
+
node = Extract.unwrap(node);
|
|
274
|
+
switch (node.type) {
|
|
275
|
+
case AST_NODE_TYPES.MemberExpression: return {
|
|
276
|
+
controller: node.object,
|
|
277
|
+
isParamSignal: false
|
|
278
|
+
};
|
|
279
|
+
case AST_NODE_TYPES.Identifier: {
|
|
280
|
+
const resolved = resolve(context, node);
|
|
281
|
+
const resolvedUnwrapped = resolved == null ? null : Extract.unwrap(resolved);
|
|
282
|
+
if (resolvedUnwrapped?.type === AST_NODE_TYPES.MemberExpression) return {
|
|
283
|
+
controller: resolvedUnwrapped.object,
|
|
284
|
+
isParamSignal: false
|
|
285
|
+
};
|
|
286
|
+
if (resolved != null && Check.isFunction(resolved)) return {
|
|
287
|
+
controller: node,
|
|
288
|
+
isParamSignal: true
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
controller: null,
|
|
292
|
+
isParamSignal: false
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
default: return {
|
|
296
|
+
controller: null,
|
|
297
|
+
isParamSignal: false
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function getFetchController(context, node) {
|
|
302
|
+
const [, optionsArg] = node.arguments;
|
|
303
|
+
if (optionsArg == null) return {
|
|
304
|
+
controller: null,
|
|
305
|
+
isParamSignal: false
|
|
306
|
+
};
|
|
307
|
+
const options = resolveToObjectExpression(context, optionsArg);
|
|
308
|
+
if (options == null) return {
|
|
309
|
+
controller: null,
|
|
310
|
+
isParamSignal: false
|
|
311
|
+
};
|
|
312
|
+
const signalProp = findProperty(options.properties, "signal");
|
|
313
|
+
if (signalProp?.type !== AST_NODE_TYPES.Property) return {
|
|
314
|
+
controller: null,
|
|
315
|
+
isParamSignal: false
|
|
316
|
+
};
|
|
317
|
+
return getControllerFromSignal(context, signalProp.value);
|
|
318
|
+
}
|
|
319
|
+
function getAbortController(node) {
|
|
320
|
+
if (node.callee.type === AST_NODE_TYPES.MemberExpression) return node.callee.object;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
var no_leaked_fetch_default = createRule({
|
|
324
|
+
meta: {
|
|
325
|
+
type: "problem",
|
|
326
|
+
docs: { description: "Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function." },
|
|
327
|
+
messages: {
|
|
328
|
+
expectedAbortController: "A 'fetch' must be provided with an 'AbortController' for proper cleanup.",
|
|
329
|
+
expectedAbortInCleanup: "A 'fetch' started in effect must be aborted with 'AbortController.abort' in the cleanup function."
|
|
330
|
+
},
|
|
331
|
+
schema: []
|
|
332
|
+
},
|
|
333
|
+
name: RULE_NAME$3,
|
|
334
|
+
create: create$3,
|
|
335
|
+
defaultOptions: []
|
|
336
|
+
});
|
|
337
|
+
function create$3(context) {
|
|
338
|
+
if (!context.sourceCode.text.includes("fetch")) return {};
|
|
339
|
+
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
|
|
340
|
+
const fEntries = [];
|
|
341
|
+
const fetchEntries = [];
|
|
342
|
+
const abortEntries = [];
|
|
343
|
+
return merge({
|
|
344
|
+
[":function"](node) {
|
|
345
|
+
const kind = getPhaseKindOfFunction(node) ?? "other";
|
|
346
|
+
fEntries.push({
|
|
347
|
+
kind,
|
|
348
|
+
node
|
|
349
|
+
});
|
|
350
|
+
},
|
|
351
|
+
[":function:exit"]() {
|
|
352
|
+
fEntries.pop();
|
|
353
|
+
},
|
|
354
|
+
["CallExpression"](node) {
|
|
355
|
+
const fEntry = fEntries.at(-1);
|
|
356
|
+
if (fEntry == null || !ComponentPhaseRelevance.has(fEntry.kind)) return;
|
|
357
|
+
switch (getCallKind$3(node)) {
|
|
358
|
+
case "fetch": {
|
|
359
|
+
if (fEntry.kind !== "setup") return;
|
|
360
|
+
const { controller, isParamSignal } = getFetchController(context, node);
|
|
361
|
+
fetchEntries.push({
|
|
362
|
+
controller,
|
|
363
|
+
isParamSignal,
|
|
364
|
+
node,
|
|
365
|
+
phase: fEntry.kind
|
|
366
|
+
});
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "abort": {
|
|
370
|
+
if (fEntry.kind !== "cleanup") return;
|
|
371
|
+
const controller = getAbortController(node);
|
|
372
|
+
if (controller == null) break;
|
|
373
|
+
abortEntries.push({
|
|
374
|
+
controller,
|
|
375
|
+
node,
|
|
376
|
+
phase: fEntry.kind
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
["Program:exit"]() {
|
|
383
|
+
for (const fEntry of fetchEntries) {
|
|
384
|
+
const controller = fEntry.controller;
|
|
385
|
+
if (controller == null) {
|
|
386
|
+
context.report({
|
|
387
|
+
messageId: "expectedAbortController",
|
|
388
|
+
node: fEntry.node
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (fEntry.isParamSignal) continue;
|
|
393
|
+
if (!abortEntries.some((aEntry) => isAssignmentTargetEqual(context, aEntry.controller, controller))) context.report({
|
|
394
|
+
messageId: "expectedAbortInCleanup",
|
|
395
|
+
node: fEntry.node
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
233
402
|
//#endregion
|
|
234
403
|
//#region src/rules/no-leaked-interval/no-leaked-interval.ts
|
|
235
404
|
const RULE_NAME$2 = "no-leaked-interval";
|
|
@@ -482,20 +651,23 @@ const isConditional = isOneOf([
|
|
|
482
651
|
AST_NODE_TYPES.LogicalExpression,
|
|
483
652
|
AST_NODE_TYPES.ConditionalExpression
|
|
484
653
|
]);
|
|
485
|
-
|
|
486
|
-
//#endregion
|
|
487
|
-
//#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
|
|
488
|
-
const RULE_NAME$1 = "no-leaked-resize-observer";
|
|
489
654
|
function isNewResizeObserver(node) {
|
|
490
655
|
return node?.type === AST_NODE_TYPES.NewExpression && node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "ResizeObserver";
|
|
491
656
|
}
|
|
492
657
|
function isFromObserver(context, node) {
|
|
493
658
|
switch (true) {
|
|
494
|
-
case node.type === AST_NODE_TYPES.Identifier:
|
|
659
|
+
case node.type === AST_NODE_TYPES.Identifier: {
|
|
660
|
+
const initNode = resolve(context, node);
|
|
661
|
+
return isNewResizeObserver(initNode == null ? null : Extract.unwrap(initNode));
|
|
662
|
+
}
|
|
495
663
|
case node.type === AST_NODE_TYPES.MemberExpression: return isFromObserver(context, node.object);
|
|
496
664
|
default: return false;
|
|
497
665
|
}
|
|
498
666
|
}
|
|
667
|
+
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/rules/no-leaked-resize-observer/no-leaked-resize-observer.ts
|
|
670
|
+
const RULE_NAME$1 = "no-leaked-resize-observer";
|
|
499
671
|
function getCallKind$1(context, node) {
|
|
500
672
|
switch (true) {
|
|
501
673
|
case node.callee.type === AST_NODE_TYPES.Identifier && isMatching(P.union("observe", "unobserve", "disconnect"))(node.callee.name) && isFromObserver(context, node.callee): return node.callee.name;
|
|
@@ -731,6 +903,7 @@ const plugin = {
|
|
|
731
903
|
},
|
|
732
904
|
rules: {
|
|
733
905
|
"no-leaked-event-listener": no_leaked_event_listener_default,
|
|
906
|
+
"no-leaked-fetch": no_leaked_fetch_default,
|
|
734
907
|
"no-leaked-interval": no_leaked_interval_default,
|
|
735
908
|
"no-leaked-resize-observer": no_leaked_resize_observer_default,
|
|
736
909
|
"no-leaked-timeout": no_leaked_timeout_default
|
|
@@ -748,6 +921,7 @@ var recommended_exports = /* @__PURE__ */ __exportAll({
|
|
|
748
921
|
const name = "react-web-api/recommended";
|
|
749
922
|
const rules = {
|
|
750
923
|
"react-web-api/no-leaked-event-listener": "warn",
|
|
924
|
+
"react-web-api/no-leaked-fetch": "warn",
|
|
751
925
|
"react-web-api/no-leaked-interval": "warn",
|
|
752
926
|
"react-web-api/no-leaked-resize-observer": "warn",
|
|
753
927
|
"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.3.0-next.0",
|
|
4
4
|
"description": "ESLint React's ESLint plugin for interacting with Web APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -43,22 +43,22 @@
|
|
|
43
43
|
"@typescript-eslint/utils": "^8.58.2",
|
|
44
44
|
"birecord": "^0.1.1",
|
|
45
45
|
"ts-pattern": "^5.9.0",
|
|
46
|
-
"@eslint-react/ast": "5.
|
|
47
|
-
"@eslint-react/
|
|
48
|
-
"@eslint-react/
|
|
49
|
-
"@eslint-react/
|
|
50
|
-
"@eslint-react/
|
|
46
|
+
"@eslint-react/ast": "5.3.0-next.0",
|
|
47
|
+
"@eslint-react/core": "5.3.0-next.0",
|
|
48
|
+
"@eslint-react/shared": "5.3.0-next.0",
|
|
49
|
+
"@eslint-react/var": "5.3.0-next.0",
|
|
50
|
+
"@eslint-react/eslint": "5.3.0-next.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react": "^19.2.14",
|
|
54
54
|
"@types/react-dom": "^19.2.3",
|
|
55
|
-
"eslint": "^10.2.
|
|
55
|
+
"eslint": "^10.2.1",
|
|
56
56
|
"tsdown": "^0.21.9",
|
|
57
57
|
"@local/eff": "3.0.0-beta.72",
|
|
58
58
|
"@local/configs": "0.0.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"eslint": "^10.2.
|
|
61
|
+
"eslint": "^10.2.1",
|
|
62
62
|
"typescript": "*"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|