oxlint-plugin-react-doctor 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +656 -9
- package/dist/index.js +2200 -587
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,264 +2,6 @@ import path from "node:path";
|
|
|
2
2
|
import { analyze } from "eslint-scope";
|
|
3
3
|
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
-
//#region src/plugin/constants/library.ts
|
|
6
|
-
const HEAVY_LIBRARIES = new Set([
|
|
7
|
-
"@monaco-editor/react",
|
|
8
|
-
"monaco-editor",
|
|
9
|
-
"recharts",
|
|
10
|
-
"@react-pdf/renderer",
|
|
11
|
-
"react-quill",
|
|
12
|
-
"@codemirror/view",
|
|
13
|
-
"@codemirror/state",
|
|
14
|
-
"chart.js",
|
|
15
|
-
"react-chartjs-2",
|
|
16
|
-
"@toast-ui/editor",
|
|
17
|
-
"draft-js"
|
|
18
|
-
]);
|
|
19
|
-
const FETCH_CALLEE_NAMES = new Set([
|
|
20
|
-
"fetch",
|
|
21
|
-
"ky",
|
|
22
|
-
"got",
|
|
23
|
-
"wretch",
|
|
24
|
-
"ofetch"
|
|
25
|
-
]);
|
|
26
|
-
const FETCH_MEMBER_OBJECTS = new Set([
|
|
27
|
-
"axios",
|
|
28
|
-
"ky",
|
|
29
|
-
"got",
|
|
30
|
-
"ofetch",
|
|
31
|
-
"wretch",
|
|
32
|
-
"request"
|
|
33
|
-
]);
|
|
34
|
-
const MUTATION_METHOD_NAMES = new Set([
|
|
35
|
-
"create",
|
|
36
|
-
"insert",
|
|
37
|
-
"insertInto",
|
|
38
|
-
"update",
|
|
39
|
-
"upsert",
|
|
40
|
-
"delete",
|
|
41
|
-
"remove",
|
|
42
|
-
"destroy",
|
|
43
|
-
"set",
|
|
44
|
-
"append"
|
|
45
|
-
]);
|
|
46
|
-
const MUTATING_HTTP_METHODS = new Set([
|
|
47
|
-
"POST",
|
|
48
|
-
"PUT",
|
|
49
|
-
"DELETE",
|
|
50
|
-
"PATCH"
|
|
51
|
-
]);
|
|
52
|
-
const SAFE_MUTABLE_CONSTRUCTOR_NAMES = new Set([
|
|
53
|
-
"Map",
|
|
54
|
-
"Set",
|
|
55
|
-
"WeakMap",
|
|
56
|
-
"WeakSet",
|
|
57
|
-
"Headers",
|
|
58
|
-
"URLSearchParams",
|
|
59
|
-
"FormData",
|
|
60
|
-
"Response",
|
|
61
|
-
"NextResponse"
|
|
62
|
-
]);
|
|
63
|
-
const RESPONSE_FACTORY_OBJECTS = new Set(["Response", "NextResponse"]);
|
|
64
|
-
const RESPONSE_FACTORY_METHODS = new Set([
|
|
65
|
-
"json",
|
|
66
|
-
"redirect",
|
|
67
|
-
"next",
|
|
68
|
-
"rewrite",
|
|
69
|
-
"error"
|
|
70
|
-
]);
|
|
71
|
-
//#endregion
|
|
72
|
-
//#region src/plugin/constants/dom.ts
|
|
73
|
-
const PASSIVE_EVENT_NAMES = new Set([
|
|
74
|
-
"scroll",
|
|
75
|
-
"wheel",
|
|
76
|
-
"touchstart",
|
|
77
|
-
"touchmove",
|
|
78
|
-
"touchend"
|
|
79
|
-
]);
|
|
80
|
-
const SCRIPT_LOADING_ATTRIBUTES = new Set(["defer", "async"]);
|
|
81
|
-
const EXECUTABLE_SCRIPT_TYPES = new Set([
|
|
82
|
-
"text/javascript",
|
|
83
|
-
"application/javascript",
|
|
84
|
-
"module"
|
|
85
|
-
]);
|
|
86
|
-
const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
87
|
-
"setTimeout",
|
|
88
|
-
"setInterval",
|
|
89
|
-
"requestAnimationFrame",
|
|
90
|
-
"requestIdleCallback",
|
|
91
|
-
"queueMicrotask"
|
|
92
|
-
]);
|
|
93
|
-
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
94
|
-
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
95
|
-
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
96
|
-
"location",
|
|
97
|
-
"window",
|
|
98
|
-
"document",
|
|
99
|
-
"navigator",
|
|
100
|
-
"history",
|
|
101
|
-
"screen",
|
|
102
|
-
"performance"
|
|
103
|
-
]);
|
|
104
|
-
const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
|
|
105
|
-
"IntersectionObserver",
|
|
106
|
-
"MutationObserver",
|
|
107
|
-
"ResizeObserver",
|
|
108
|
-
"PerformanceObserver"
|
|
109
|
-
]);
|
|
110
|
-
const STORAGE_OBJECTS$1 = new Set(["localStorage", "sessionStorage"]);
|
|
111
|
-
//#endregion
|
|
112
|
-
//#region src/plugin/constants/react.ts
|
|
113
|
-
const INDEX_PARAMETER_NAMES = new Set([
|
|
114
|
-
"index",
|
|
115
|
-
"idx",
|
|
116
|
-
"i"
|
|
117
|
-
]);
|
|
118
|
-
const LOADING_STATE_PATTERN = /^(?:isLoading|isPending)$/;
|
|
119
|
-
const STABLE_HOOK_WRAPPERS = new Set([
|
|
120
|
-
"useState",
|
|
121
|
-
"useMemo",
|
|
122
|
-
"useRef"
|
|
123
|
-
]);
|
|
124
|
-
const GENERIC_EVENT_SUFFIXES = new Set([
|
|
125
|
-
"Click",
|
|
126
|
-
"Change",
|
|
127
|
-
"Input",
|
|
128
|
-
"Blur",
|
|
129
|
-
"Focus"
|
|
130
|
-
]);
|
|
131
|
-
const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
132
|
-
"Boolean",
|
|
133
|
-
"String",
|
|
134
|
-
"Number",
|
|
135
|
-
"Array",
|
|
136
|
-
"Object",
|
|
137
|
-
"parseInt",
|
|
138
|
-
"parseFloat"
|
|
139
|
-
]);
|
|
140
|
-
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
141
|
-
"Boolean",
|
|
142
|
-
"String",
|
|
143
|
-
"Number",
|
|
144
|
-
"Array",
|
|
145
|
-
"Object",
|
|
146
|
-
"parseInt",
|
|
147
|
-
"parseFloat",
|
|
148
|
-
"isNaN",
|
|
149
|
-
"isFinite",
|
|
150
|
-
"BigInt",
|
|
151
|
-
"Symbol"
|
|
152
|
-
]);
|
|
153
|
-
const SETTER_PATTERN = /^set[A-Z]/;
|
|
154
|
-
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
155
|
-
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
156
|
-
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
157
|
-
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
158
|
-
const HOOKS_WITH_DEPS = new Set([
|
|
159
|
-
"useEffect",
|
|
160
|
-
"useLayoutEffect",
|
|
161
|
-
"useMemo",
|
|
162
|
-
"useCallback"
|
|
163
|
-
]);
|
|
164
|
-
const REACT_HOC_NAMES = new Set([
|
|
165
|
-
"memo",
|
|
166
|
-
"forwardRef",
|
|
167
|
-
"React.memo",
|
|
168
|
-
"React.forwardRef"
|
|
169
|
-
]);
|
|
170
|
-
const SUBSCRIPTION_METHOD_NAMES = new Set([
|
|
171
|
-
"subscribe",
|
|
172
|
-
"addEventListener",
|
|
173
|
-
"addListener",
|
|
174
|
-
"on",
|
|
175
|
-
"watch",
|
|
176
|
-
"listen",
|
|
177
|
-
"sub"
|
|
178
|
-
]);
|
|
179
|
-
const CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES = new Set(["subscribe", "sub"]);
|
|
180
|
-
const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
181
|
-
"unsubscribe",
|
|
182
|
-
"removeEventListener",
|
|
183
|
-
"removeListener",
|
|
184
|
-
"off",
|
|
185
|
-
"unwatch",
|
|
186
|
-
"unlisten",
|
|
187
|
-
"unsub",
|
|
188
|
-
"abort"
|
|
189
|
-
]);
|
|
190
|
-
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
191
|
-
"remove",
|
|
192
|
-
"cleanup",
|
|
193
|
-
"dispose",
|
|
194
|
-
"destroy",
|
|
195
|
-
"stop",
|
|
196
|
-
"teardown"
|
|
197
|
-
]);
|
|
198
|
-
const CLEANUP_LIKE_RELEASE_CALLEE_NAMES = new Set([
|
|
199
|
-
...GLOBAL_RELEASE_METHOD_NAMES,
|
|
200
|
-
"cleanup",
|
|
201
|
-
"dispose",
|
|
202
|
-
"destroy",
|
|
203
|
-
"teardown"
|
|
204
|
-
]);
|
|
205
|
-
const EXTERNAL_SYNC_MEMBER_METHOD_NAMES = new Set([
|
|
206
|
-
...SUBSCRIPTION_METHOD_NAMES,
|
|
207
|
-
"connect",
|
|
208
|
-
"disconnect",
|
|
209
|
-
"open",
|
|
210
|
-
"close",
|
|
211
|
-
"fetch",
|
|
212
|
-
"post",
|
|
213
|
-
"put",
|
|
214
|
-
"patch"
|
|
215
|
-
]);
|
|
216
|
-
const EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS = new Set([
|
|
217
|
-
...FETCH_MEMBER_OBJECTS,
|
|
218
|
-
"api",
|
|
219
|
-
"client",
|
|
220
|
-
"http",
|
|
221
|
-
"fetcher"
|
|
222
|
-
]);
|
|
223
|
-
const EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES = new Set([
|
|
224
|
-
"get",
|
|
225
|
-
"head",
|
|
226
|
-
"options",
|
|
227
|
-
"delete"
|
|
228
|
-
]);
|
|
229
|
-
const EXTERNAL_SYNC_DIRECT_CALLEE_NAMES = new Set([...FETCH_CALLEE_NAMES, ...TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES]);
|
|
230
|
-
const EVENT_TRIGGERED_SIDE_EFFECT_CALLEES = new Set([
|
|
231
|
-
...FETCH_CALLEE_NAMES,
|
|
232
|
-
"post",
|
|
233
|
-
"put",
|
|
234
|
-
"patch",
|
|
235
|
-
"navigate",
|
|
236
|
-
"navigateTo",
|
|
237
|
-
"showNotification",
|
|
238
|
-
"toast",
|
|
239
|
-
"alert",
|
|
240
|
-
"confirm",
|
|
241
|
-
"logVisit",
|
|
242
|
-
"captureEvent"
|
|
243
|
-
]);
|
|
244
|
-
const EVENT_TRIGGERED_SIDE_EFFECT_MEMBER_METHODS = new Set([
|
|
245
|
-
"post",
|
|
246
|
-
"put",
|
|
247
|
-
"patch",
|
|
248
|
-
"delete",
|
|
249
|
-
"navigate",
|
|
250
|
-
"capture",
|
|
251
|
-
"track",
|
|
252
|
-
"logEvent"
|
|
253
|
-
]);
|
|
254
|
-
const EVENT_TRIGGERED_NAVIGATION_METHOD_NAMES = new Set(["push", "replace"]);
|
|
255
|
-
const NAVIGATION_RECEIVER_NAMES = new Set([
|
|
256
|
-
"router",
|
|
257
|
-
"navigation",
|
|
258
|
-
"navigator",
|
|
259
|
-
"history",
|
|
260
|
-
"location"
|
|
261
|
-
]);
|
|
262
|
-
//#endregion
|
|
263
5
|
//#region src/plugin/utils/is-testlike-filename.ts
|
|
264
6
|
const NON_PRODUCTION_PATH_SEGMENTS = [
|
|
265
7
|
"/test/",
|
|
@@ -537,15 +279,291 @@ const defineRule = (rule) => {
|
|
|
537
279
|
};
|
|
538
280
|
};
|
|
539
281
|
//#endregion
|
|
540
|
-
//#region src/plugin/
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
282
|
+
//#region src/plugin/constants/library.ts
|
|
283
|
+
const HEAVY_LIBRARIES = new Set([
|
|
284
|
+
"@monaco-editor/react",
|
|
285
|
+
"monaco-editor",
|
|
286
|
+
"recharts",
|
|
287
|
+
"@react-pdf/renderer",
|
|
288
|
+
"react-quill",
|
|
289
|
+
"@codemirror/view",
|
|
290
|
+
"@codemirror/state",
|
|
291
|
+
"chart.js",
|
|
292
|
+
"react-chartjs-2",
|
|
293
|
+
"@toast-ui/editor",
|
|
294
|
+
"draft-js"
|
|
295
|
+
]);
|
|
296
|
+
const FETCH_CALLEE_NAMES = new Set([
|
|
297
|
+
"fetch",
|
|
298
|
+
"ky",
|
|
299
|
+
"got",
|
|
300
|
+
"wretch",
|
|
301
|
+
"ofetch"
|
|
302
|
+
]);
|
|
303
|
+
const FETCH_MEMBER_OBJECTS = new Set([
|
|
304
|
+
"axios",
|
|
305
|
+
"ky",
|
|
306
|
+
"got",
|
|
307
|
+
"ofetch",
|
|
308
|
+
"wretch",
|
|
309
|
+
"request"
|
|
310
|
+
]);
|
|
311
|
+
const MUTATION_METHOD_NAMES = new Set([
|
|
312
|
+
"create",
|
|
313
|
+
"insert",
|
|
314
|
+
"insertInto",
|
|
315
|
+
"update",
|
|
316
|
+
"upsert",
|
|
317
|
+
"delete",
|
|
318
|
+
"remove",
|
|
319
|
+
"destroy",
|
|
320
|
+
"set",
|
|
321
|
+
"append"
|
|
322
|
+
]);
|
|
323
|
+
const MUTATING_HTTP_METHODS = new Set([
|
|
324
|
+
"POST",
|
|
325
|
+
"PUT",
|
|
326
|
+
"DELETE",
|
|
327
|
+
"PATCH"
|
|
328
|
+
]);
|
|
329
|
+
const SAFE_MUTABLE_CONSTRUCTOR_NAMES = new Set([
|
|
330
|
+
"Map",
|
|
331
|
+
"Set",
|
|
332
|
+
"WeakMap",
|
|
333
|
+
"WeakSet",
|
|
334
|
+
"Headers",
|
|
335
|
+
"URLSearchParams",
|
|
336
|
+
"FormData",
|
|
337
|
+
"Response",
|
|
338
|
+
"NextResponse"
|
|
339
|
+
]);
|
|
340
|
+
const RESPONSE_FACTORY_OBJECTS = new Set(["Response", "NextResponse"]);
|
|
341
|
+
const RESPONSE_FACTORY_METHODS = new Set([
|
|
342
|
+
"json",
|
|
343
|
+
"redirect",
|
|
344
|
+
"next",
|
|
345
|
+
"rewrite",
|
|
346
|
+
"error"
|
|
347
|
+
]);
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/plugin/constants/dom.ts
|
|
350
|
+
const PASSIVE_EVENT_NAMES = new Set([
|
|
351
|
+
"scroll",
|
|
352
|
+
"wheel",
|
|
353
|
+
"touchstart",
|
|
354
|
+
"touchmove",
|
|
355
|
+
"touchend"
|
|
356
|
+
]);
|
|
357
|
+
const SCRIPT_LOADING_ATTRIBUTES = new Set(["defer", "async"]);
|
|
358
|
+
const EXECUTABLE_SCRIPT_TYPES = new Set([
|
|
359
|
+
"text/javascript",
|
|
360
|
+
"application/javascript",
|
|
361
|
+
"module"
|
|
362
|
+
]);
|
|
363
|
+
const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
364
|
+
"setTimeout",
|
|
365
|
+
"setInterval",
|
|
366
|
+
"requestAnimationFrame",
|
|
367
|
+
"requestIdleCallback",
|
|
368
|
+
"queueMicrotask"
|
|
369
|
+
]);
|
|
370
|
+
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
371
|
+
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
372
|
+
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
373
|
+
"location",
|
|
374
|
+
"window",
|
|
375
|
+
"document",
|
|
376
|
+
"navigator",
|
|
377
|
+
"history",
|
|
378
|
+
"screen",
|
|
379
|
+
"performance"
|
|
380
|
+
]);
|
|
381
|
+
const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
|
|
382
|
+
"IntersectionObserver",
|
|
383
|
+
"MutationObserver",
|
|
384
|
+
"ResizeObserver",
|
|
385
|
+
"PerformanceObserver"
|
|
386
|
+
]);
|
|
387
|
+
const STORAGE_OBJECTS$1 = new Set(["localStorage", "sessionStorage"]);
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/plugin/constants/react.ts
|
|
390
|
+
const INDEX_PARAMETER_NAMES = new Set([
|
|
391
|
+
"index",
|
|
392
|
+
"idx",
|
|
393
|
+
"i"
|
|
394
|
+
]);
|
|
395
|
+
const LOADING_STATE_PATTERN = /^(?:isLoading|isPending)$/;
|
|
396
|
+
const STABLE_HOOK_WRAPPERS = new Set([
|
|
397
|
+
"useState",
|
|
398
|
+
"useMemo",
|
|
399
|
+
"useRef"
|
|
400
|
+
]);
|
|
401
|
+
const GENERIC_EVENT_SUFFIXES = new Set([
|
|
402
|
+
"Click",
|
|
403
|
+
"Change",
|
|
404
|
+
"Input",
|
|
405
|
+
"Blur",
|
|
406
|
+
"Focus"
|
|
407
|
+
]);
|
|
408
|
+
const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
409
|
+
"Boolean",
|
|
410
|
+
"String",
|
|
411
|
+
"Number",
|
|
412
|
+
"Array",
|
|
413
|
+
"Object",
|
|
414
|
+
"parseInt",
|
|
415
|
+
"parseFloat"
|
|
416
|
+
]);
|
|
417
|
+
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
418
|
+
"Boolean",
|
|
419
|
+
"String",
|
|
420
|
+
"Number",
|
|
421
|
+
"Array",
|
|
422
|
+
"Object",
|
|
423
|
+
"parseInt",
|
|
424
|
+
"parseFloat",
|
|
425
|
+
"isNaN",
|
|
426
|
+
"isFinite",
|
|
427
|
+
"BigInt",
|
|
428
|
+
"Symbol"
|
|
429
|
+
]);
|
|
430
|
+
const SETTER_PATTERN = /^set[A-Z]/;
|
|
431
|
+
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
432
|
+
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
433
|
+
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
434
|
+
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
435
|
+
const HOOKS_WITH_DEPS = new Set([
|
|
436
|
+
"useEffect",
|
|
437
|
+
"useLayoutEffect",
|
|
438
|
+
"useMemo",
|
|
439
|
+
"useCallback"
|
|
440
|
+
]);
|
|
441
|
+
const REACT_HOC_NAMES = new Set([
|
|
442
|
+
"memo",
|
|
443
|
+
"forwardRef",
|
|
444
|
+
"React.memo",
|
|
445
|
+
"React.forwardRef"
|
|
446
|
+
]);
|
|
447
|
+
const SUBSCRIPTION_METHOD_NAMES = new Set([
|
|
448
|
+
"subscribe",
|
|
449
|
+
"addEventListener",
|
|
450
|
+
"addListener",
|
|
451
|
+
"on",
|
|
452
|
+
"watch",
|
|
453
|
+
"listen",
|
|
454
|
+
"sub"
|
|
455
|
+
]);
|
|
456
|
+
const CLEANUP_RETURNING_SUBSCRIPTION_METHOD_NAMES = new Set(["subscribe", "sub"]);
|
|
457
|
+
const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
458
|
+
"unsubscribe",
|
|
459
|
+
"removeEventListener",
|
|
460
|
+
"removeListener",
|
|
461
|
+
"off",
|
|
462
|
+
"unwatch",
|
|
463
|
+
"unlisten",
|
|
464
|
+
"unsub",
|
|
465
|
+
"abort"
|
|
466
|
+
]);
|
|
467
|
+
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
468
|
+
"remove",
|
|
469
|
+
"cleanup",
|
|
470
|
+
"dispose",
|
|
471
|
+
"destroy",
|
|
472
|
+
"stop",
|
|
473
|
+
"teardown"
|
|
474
|
+
]);
|
|
475
|
+
const CLEANUP_LIKE_RELEASE_CALLEE_NAMES = new Set([
|
|
476
|
+
...GLOBAL_RELEASE_METHOD_NAMES,
|
|
477
|
+
"cleanup",
|
|
478
|
+
"dispose",
|
|
479
|
+
"destroy",
|
|
480
|
+
"teardown"
|
|
481
|
+
]);
|
|
482
|
+
const EXTERNAL_SYNC_MEMBER_METHOD_NAMES = new Set([
|
|
483
|
+
...SUBSCRIPTION_METHOD_NAMES,
|
|
484
|
+
"connect",
|
|
485
|
+
"disconnect",
|
|
486
|
+
"open",
|
|
487
|
+
"close",
|
|
488
|
+
"fetch",
|
|
489
|
+
"post",
|
|
490
|
+
"put",
|
|
491
|
+
"patch"
|
|
492
|
+
]);
|
|
493
|
+
const EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS = new Set([
|
|
494
|
+
...FETCH_MEMBER_OBJECTS,
|
|
495
|
+
"api",
|
|
496
|
+
"client",
|
|
497
|
+
"http",
|
|
498
|
+
"fetcher"
|
|
499
|
+
]);
|
|
500
|
+
const EXTERNAL_SYNC_AMBIGUOUS_HTTP_METHOD_NAMES = new Set([
|
|
501
|
+
"get",
|
|
502
|
+
"head",
|
|
503
|
+
"options",
|
|
504
|
+
"delete"
|
|
505
|
+
]);
|
|
506
|
+
const EXTERNAL_SYNC_DIRECT_CALLEE_NAMES = new Set([...FETCH_CALLEE_NAMES, ...TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES]);
|
|
507
|
+
const EVENT_TRIGGERED_SIDE_EFFECT_CALLEES = new Set([
|
|
508
|
+
...FETCH_CALLEE_NAMES,
|
|
509
|
+
"post",
|
|
510
|
+
"put",
|
|
511
|
+
"patch",
|
|
512
|
+
"navigate",
|
|
513
|
+
"navigateTo",
|
|
514
|
+
"showNotification",
|
|
515
|
+
"toast",
|
|
516
|
+
"alert",
|
|
517
|
+
"confirm",
|
|
518
|
+
"logVisit",
|
|
519
|
+
"captureEvent"
|
|
520
|
+
]);
|
|
521
|
+
const EVENT_TRIGGERED_SIDE_EFFECT_MEMBER_METHODS = new Set([
|
|
522
|
+
"post",
|
|
523
|
+
"put",
|
|
524
|
+
"patch",
|
|
525
|
+
"delete",
|
|
526
|
+
"navigate",
|
|
527
|
+
"capture",
|
|
528
|
+
"track",
|
|
529
|
+
"logEvent"
|
|
530
|
+
]);
|
|
531
|
+
const EVENT_TRIGGERED_NAVIGATION_METHOD_NAMES = new Set(["push", "replace"]);
|
|
532
|
+
const NAVIGATION_RECEIVER_NAMES = new Set([
|
|
533
|
+
"router",
|
|
534
|
+
"navigation",
|
|
535
|
+
"navigator",
|
|
536
|
+
"history",
|
|
537
|
+
"location"
|
|
538
|
+
]);
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/plugin/utils/find-program-root.ts
|
|
541
|
+
/**
|
|
542
|
+
* Walks up the AST `parent` chain from `node` to its enclosing
|
|
543
|
+
* `Program` root and returns it; `null` when the chain doesn't lead
|
|
544
|
+
* to a `Program` (e.g. detached fragments used by test utilities).
|
|
545
|
+
*
|
|
546
|
+
* Was duplicated byte-identical across seven sites (five rule files
|
|
547
|
+
* + two utility modules). Promoted to a shared helper so adding a
|
|
548
|
+
* new ESTree top-level shape only touches one place.
|
|
549
|
+
*/
|
|
550
|
+
const findProgramRoot = (node) => {
|
|
551
|
+
let cursor = node;
|
|
552
|
+
while (cursor) {
|
|
553
|
+
if (isNodeOfType(cursor, "Program")) return cursor;
|
|
554
|
+
cursor = cursor.parent ?? null;
|
|
555
|
+
}
|
|
546
556
|
return null;
|
|
547
557
|
};
|
|
548
558
|
//#endregion
|
|
559
|
+
//#region src/plugin/utils/get-imported-name.ts
|
|
560
|
+
const getImportedName$1 = (importSpecifier) => {
|
|
561
|
+
if (!isNodeOfType(importSpecifier, "ImportSpecifier")) return void 0;
|
|
562
|
+
const imported = importSpecifier.imported;
|
|
563
|
+
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
564
|
+
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
565
|
+
};
|
|
566
|
+
//#endregion
|
|
549
567
|
//#region src/plugin/utils/get-callee-name.ts
|
|
550
568
|
const getCalleeName$1 = (node) => {
|
|
551
569
|
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
|
|
@@ -582,6 +600,132 @@ const walkAst = (node, visitor) => {
|
|
|
582
600
|
}
|
|
583
601
|
};
|
|
584
602
|
//#endregion
|
|
603
|
+
//#region src/plugin/rules/state-and-effects/activity-wraps-effect-heavy-subtree.ts
|
|
604
|
+
const ACTIVITY_IMPORTED_NAMES = new Set(["Activity", "unstable_Activity"]);
|
|
605
|
+
const isStaticallyKnownMode = (modeAttribute) => {
|
|
606
|
+
const value = modeAttribute.value;
|
|
607
|
+
if (!value) return false;
|
|
608
|
+
if (isNodeOfType(value, "Literal")) return true;
|
|
609
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return isNodeOfType(value.expression, "Literal");
|
|
610
|
+
return false;
|
|
611
|
+
};
|
|
612
|
+
const collectChildComponentNames = (element, into) => {
|
|
613
|
+
walkAst(element, (child) => {
|
|
614
|
+
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
615
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
616
|
+
const name = child.name.name;
|
|
617
|
+
if (!UPPERCASE_PATTERN.test(name)) return;
|
|
618
|
+
into.add(name);
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
const findSameFileComponentBody = (programRoot, componentName) => {
|
|
622
|
+
let foundBody = null;
|
|
623
|
+
walkAst(programRoot, (node) => {
|
|
624
|
+
if (foundBody) return false;
|
|
625
|
+
if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
|
|
626
|
+
foundBody = node.body;
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
|
|
630
|
+
const initializer = node.init;
|
|
631
|
+
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
|
|
632
|
+
foundBody = initializer.body;
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
return foundBody;
|
|
638
|
+
};
|
|
639
|
+
const countEffectHookCalls = (body) => {
|
|
640
|
+
if (!body) return 0;
|
|
641
|
+
let count = 0;
|
|
642
|
+
walkAst(body, (child) => {
|
|
643
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
644
|
+
if (isHookCall$1(child, EFFECT_HOOK_NAMES$1)) count++;
|
|
645
|
+
});
|
|
646
|
+
return count;
|
|
647
|
+
};
|
|
648
|
+
const activityWrapsEffectHeavySubtree = defineRule({
|
|
649
|
+
id: "activity-wraps-effect-heavy-subtree",
|
|
650
|
+
severity: "warn",
|
|
651
|
+
requires: ["react:19.2"],
|
|
652
|
+
recommendation: "Audit the subtree under `<Activity>` — every hide / show cycle tears down and recreates every Effect inside. Move subscriptions and effect-driven setState chains outside the Activity boundary, or pre-resolve the data above it",
|
|
653
|
+
create: (context) => {
|
|
654
|
+
const localActivityNames = /* @__PURE__ */ new Set();
|
|
655
|
+
const reactNamespaceLocalNames = /* @__PURE__ */ new Set();
|
|
656
|
+
return {
|
|
657
|
+
ImportDeclaration(node) {
|
|
658
|
+
if (node.source?.value !== "react") return;
|
|
659
|
+
for (const specifier of node.specifiers ?? []) {
|
|
660
|
+
if (isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
|
|
661
|
+
if (isNodeOfType(specifier.local, "Identifier")) reactNamespaceLocalNames.add(specifier.local.name);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (isNodeOfType(specifier, "ImportDefaultSpecifier")) {
|
|
665
|
+
if (isNodeOfType(specifier.local, "Identifier")) reactNamespaceLocalNames.add(specifier.local.name);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
669
|
+
const importedName = getImportedName$1(specifier);
|
|
670
|
+
if (!importedName || !ACTIVITY_IMPORTED_NAMES.has(importedName)) continue;
|
|
671
|
+
if (isNodeOfType(specifier.local, "Identifier")) localActivityNames.add(specifier.local.name);
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
JSXElement(node) {
|
|
675
|
+
const openingElement = node.openingElement;
|
|
676
|
+
if (!openingElement) return;
|
|
677
|
+
const elementName = openingElement.name;
|
|
678
|
+
let isActivity = false;
|
|
679
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) isActivity = localActivityNames.has(elementName.name);
|
|
680
|
+
else if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
681
|
+
if (isNodeOfType(elementName.object, "JSXIdentifier") && reactNamespaceLocalNames.has(elementName.object.name) && isNodeOfType(elementName.property, "JSXIdentifier")) isActivity = ACTIVITY_IMPORTED_NAMES.has(elementName.property.name);
|
|
682
|
+
}
|
|
683
|
+
if (!isActivity) return;
|
|
684
|
+
let modeAttribute = null;
|
|
685
|
+
for (const attribute of openingElement.attributes ?? []) {
|
|
686
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
687
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
688
|
+
if (attribute.name.name !== "mode") continue;
|
|
689
|
+
modeAttribute = attribute;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (!modeAttribute) return;
|
|
693
|
+
if (isStaticallyKnownMode(modeAttribute)) return;
|
|
694
|
+
const childComponentNames = /* @__PURE__ */ new Set();
|
|
695
|
+
collectChildComponentNames(node, childComponentNames);
|
|
696
|
+
for (const activityName of localActivityNames) childComponentNames.delete(activityName);
|
|
697
|
+
if (childComponentNames.size === 0) return;
|
|
698
|
+
const programRoot = findProgramRoot(node);
|
|
699
|
+
if (!programRoot) return;
|
|
700
|
+
let totalEffects = 0;
|
|
701
|
+
const effectfulChildren = [];
|
|
702
|
+
for (const componentName of childComponentNames) {
|
|
703
|
+
const body = findSameFileComponentBody(programRoot, componentName);
|
|
704
|
+
if (!body) continue;
|
|
705
|
+
const effectCount = countEffectHookCalls(body);
|
|
706
|
+
if (effectCount === 0) continue;
|
|
707
|
+
totalEffects += effectCount;
|
|
708
|
+
effectfulChildren.push(`<${componentName}>`);
|
|
709
|
+
}
|
|
710
|
+
if (totalEffects === 0) return;
|
|
711
|
+
context.report({
|
|
712
|
+
node: openingElement,
|
|
713
|
+
message: `<Activity> wraps ${effectfulChildren.join(", ")} which use ${totalEffects} effect hook${totalEffects === 1 ? "" : "s"} — every hide/show cycle recreates them all. Audit the subtree or move subscriptions outside the boundary`
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
//#endregion
|
|
720
|
+
//#region src/plugin/utils/get-effect-callback.ts
|
|
721
|
+
const getEffectCallback = (node) => {
|
|
722
|
+
if (!isNodeOfType(node, "CallExpression") && !isNodeOfType(node, "NewExpression")) return null;
|
|
723
|
+
if (!node.arguments?.length) return null;
|
|
724
|
+
const callback = node.arguments[0];
|
|
725
|
+
if (isNodeOfType(callback, "ArrowFunctionExpression") || isNodeOfType(callback, "FunctionExpression")) return callback;
|
|
726
|
+
return null;
|
|
727
|
+
};
|
|
728
|
+
//#endregion
|
|
585
729
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
586
730
|
const advancedEventHandlerRefs = defineRule({
|
|
587
731
|
id: "advanced-event-handler-refs",
|
|
@@ -875,7 +1019,7 @@ const altText = defineRule({
|
|
|
875
1019
|
});
|
|
876
1020
|
//#endregion
|
|
877
1021
|
//#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
|
|
878
|
-
const buildMessage$
|
|
1022
|
+
const buildMessage$28 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
|
|
879
1023
|
const DEFAULT_AMBIGUOUS = [
|
|
880
1024
|
"click here",
|
|
881
1025
|
"here",
|
|
@@ -932,14 +1076,14 @@ const anchorAmbiguousText = defineRule({
|
|
|
932
1076
|
const normalized = normalizeText(accessibleText);
|
|
933
1077
|
if (ambiguousSet.has(normalized)) context.report({
|
|
934
1078
|
node: node.openingElement.name,
|
|
935
|
-
message: buildMessage$
|
|
1079
|
+
message: buildMessage$28(normalized)
|
|
936
1080
|
});
|
|
937
1081
|
} };
|
|
938
1082
|
}
|
|
939
1083
|
});
|
|
940
1084
|
//#endregion
|
|
941
1085
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
942
|
-
const MESSAGE$
|
|
1086
|
+
const MESSAGE$47 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
|
|
943
1087
|
const anchorHasContent = defineRule({
|
|
944
1088
|
id: "anchor-has-content",
|
|
945
1089
|
tags: ["react-jsx-only"],
|
|
@@ -954,7 +1098,7 @@ const anchorHasContent = defineRule({
|
|
|
954
1098
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
955
1099
|
context.report({
|
|
956
1100
|
node: opening.name,
|
|
957
|
-
message: MESSAGE$
|
|
1101
|
+
message: MESSAGE$47
|
|
958
1102
|
});
|
|
959
1103
|
} })
|
|
960
1104
|
});
|
|
@@ -1347,7 +1491,7 @@ const parseJsxValue = (value) => {
|
|
|
1347
1491
|
};
|
|
1348
1492
|
//#endregion
|
|
1349
1493
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
1350
|
-
const MESSAGE$
|
|
1494
|
+
const MESSAGE$46 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
|
|
1351
1495
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
1352
1496
|
id: "aria-activedescendant-has-tabindex",
|
|
1353
1497
|
tags: ["react-jsx-only"],
|
|
@@ -1364,14 +1508,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
1364
1508
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
1365
1509
|
context.report({
|
|
1366
1510
|
node: node.name,
|
|
1367
|
-
message: MESSAGE$
|
|
1511
|
+
message: MESSAGE$46
|
|
1368
1512
|
});
|
|
1369
1513
|
return;
|
|
1370
1514
|
}
|
|
1371
1515
|
if (isInteractiveElement(tag, node)) return;
|
|
1372
1516
|
context.report({
|
|
1373
1517
|
node: node.name,
|
|
1374
|
-
message: MESSAGE$
|
|
1518
|
+
message: MESSAGE$46
|
|
1375
1519
|
});
|
|
1376
1520
|
} })
|
|
1377
1521
|
});
|
|
@@ -1511,7 +1655,7 @@ const ARIA_PROPERTIES = new Map([
|
|
|
1511
1655
|
const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
|
|
1512
1656
|
//#endregion
|
|
1513
1657
|
//#region src/plugin/rules/a11y/aria-props.ts
|
|
1514
|
-
const buildMessage$
|
|
1658
|
+
const buildMessage$27 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
|
|
1515
1659
|
const ariaProps = defineRule({
|
|
1516
1660
|
id: "aria-props",
|
|
1517
1661
|
tags: ["react-jsx-only"],
|
|
@@ -1524,7 +1668,7 @@ const ariaProps = defineRule({
|
|
|
1524
1668
|
if (!name || !name.startsWith("aria-")) return;
|
|
1525
1669
|
if (!isValidAriaProperty(name)) context.report({
|
|
1526
1670
|
node: node.name,
|
|
1527
|
-
message: buildMessage$
|
|
1671
|
+
message: buildMessage$27(name)
|
|
1528
1672
|
});
|
|
1529
1673
|
} })
|
|
1530
1674
|
});
|
|
@@ -1675,7 +1819,7 @@ const buildExpectedDescription = (propType) => {
|
|
|
1675
1819
|
case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
|
|
1676
1820
|
}
|
|
1677
1821
|
};
|
|
1678
|
-
const buildMessage$
|
|
1822
|
+
const buildMessage$26 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
|
|
1679
1823
|
const allowNoneValue = (propType) => {
|
|
1680
1824
|
switch (propType.kind) {
|
|
1681
1825
|
case "boolean":
|
|
@@ -1808,13 +1952,13 @@ const ariaProptypes = defineRule({
|
|
|
1808
1952
|
if (!node.value) {
|
|
1809
1953
|
if (!allowNoneValue(propType)) context.report({
|
|
1810
1954
|
node,
|
|
1811
|
-
message: buildMessage$
|
|
1955
|
+
message: buildMessage$26(propName, propType)
|
|
1812
1956
|
});
|
|
1813
1957
|
return;
|
|
1814
1958
|
}
|
|
1815
1959
|
if (!isValidValueForType(propType, node.value)) context.report({
|
|
1816
1960
|
node,
|
|
1817
|
-
message: buildMessage$
|
|
1961
|
+
message: buildMessage$26(propName, propType)
|
|
1818
1962
|
});
|
|
1819
1963
|
} })
|
|
1820
1964
|
});
|
|
@@ -2126,7 +2270,7 @@ const ariaRole = defineRule({
|
|
|
2126
2270
|
});
|
|
2127
2271
|
//#endregion
|
|
2128
2272
|
//#region src/plugin/rules/a11y/aria-unsupported-elements.ts
|
|
2129
|
-
const buildMessage$
|
|
2273
|
+
const buildMessage$25 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
|
|
2130
2274
|
const ariaUnsupportedElements = defineRule({
|
|
2131
2275
|
id: "aria-unsupported-elements",
|
|
2132
2276
|
tags: ["react-jsx-only"],
|
|
@@ -2143,7 +2287,7 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2143
2287
|
if (!attrName) continue;
|
|
2144
2288
|
if (attrName.startsWith("aria-") || attrName === "role") context.report({
|
|
2145
2289
|
node: attribute,
|
|
2146
|
-
message: buildMessage$
|
|
2290
|
+
message: buildMessage$25(tag, attrName)
|
|
2147
2291
|
});
|
|
2148
2292
|
}
|
|
2149
2293
|
} })
|
|
@@ -2398,7 +2542,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2398
2542
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2399
2543
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2400
2544
|
*/
|
|
2401
|
-
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2545
|
+
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2402
2546
|
//#endregion
|
|
2403
2547
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2404
2548
|
/**
|
|
@@ -2421,7 +2565,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2421
2565
|
let firstAwait = null;
|
|
2422
2566
|
walkAst(block, (child) => {
|
|
2423
2567
|
if (firstAwait) return false;
|
|
2424
|
-
if (child !== block && isFunctionLike(child)) return false;
|
|
2568
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
2425
2569
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2426
2570
|
});
|
|
2427
2571
|
return firstAwait;
|
|
@@ -2873,13 +3017,13 @@ const asyncDeferAwait = defineRule({
|
|
|
2873
3017
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
2874
3018
|
if (!functionBody) return;
|
|
2875
3019
|
walkAst(functionBody, (descendant) => {
|
|
2876
|
-
if (isFunctionLike(descendant)) return false;
|
|
3020
|
+
if (isFunctionLike$1(descendant)) return false;
|
|
2877
3021
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
2878
3022
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
2879
3023
|
});
|
|
2880
3024
|
};
|
|
2881
3025
|
const enterFunction = (node) => {
|
|
2882
|
-
if (!isFunctionLike(node)) return;
|
|
3026
|
+
if (!isFunctionLike$1(node)) return;
|
|
2883
3027
|
if (!node.async) return;
|
|
2884
3028
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
2885
3029
|
inspectAllStatementBlocks(node.body);
|
|
@@ -3010,7 +3154,7 @@ const asyncParallel = defineRule({
|
|
|
3010
3154
|
});
|
|
3011
3155
|
//#endregion
|
|
3012
3156
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
3013
|
-
const buildMessage$
|
|
3157
|
+
const buildMessage$24 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
|
|
3014
3158
|
const AUTOFILL_TOKENS = new Set([
|
|
3015
3159
|
"off",
|
|
3016
3160
|
"on",
|
|
@@ -3098,7 +3242,7 @@ const autocompleteValid = defineRule({
|
|
|
3098
3242
|
if (!AUTOFILL_TOKENS.has(token)) {
|
|
3099
3243
|
context.report({
|
|
3100
3244
|
node: attribute,
|
|
3101
|
-
message: buildMessage$
|
|
3245
|
+
message: buildMessage$24(value)
|
|
3102
3246
|
});
|
|
3103
3247
|
return;
|
|
3104
3248
|
}
|
|
@@ -3373,7 +3517,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
3373
3517
|
//#endregion
|
|
3374
3518
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
3375
3519
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3376
|
-
const MESSAGE$
|
|
3520
|
+
const MESSAGE$45 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
|
|
3377
3521
|
const KEY_HANDLERS = [
|
|
3378
3522
|
"onKeyUp",
|
|
3379
3523
|
"onKeyDown",
|
|
@@ -3404,7 +3548,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3404
3548
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
3405
3549
|
context.report({
|
|
3406
3550
|
node: node.name,
|
|
3407
|
-
message: MESSAGE$
|
|
3551
|
+
message: MESSAGE$45
|
|
3408
3552
|
});
|
|
3409
3553
|
} };
|
|
3410
3554
|
}
|
|
@@ -3489,14 +3633,42 @@ const isReactComponentName = (name) => {
|
|
|
3489
3633
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
3490
3634
|
};
|
|
3491
3635
|
//#endregion
|
|
3636
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
3637
|
+
const TS_WRAPPER_TYPES = new Set([
|
|
3638
|
+
"ParenthesizedExpression",
|
|
3639
|
+
"TSAsExpression",
|
|
3640
|
+
"TSSatisfiesExpression",
|
|
3641
|
+
"TSTypeAssertion",
|
|
3642
|
+
"TSNonNullExpression",
|
|
3643
|
+
"TSInstantiationExpression"
|
|
3644
|
+
]);
|
|
3645
|
+
const stripParenExpression = (node) => {
|
|
3646
|
+
let current = node;
|
|
3647
|
+
while (true) {
|
|
3648
|
+
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
3649
|
+
current = current.expression;
|
|
3650
|
+
continue;
|
|
3651
|
+
}
|
|
3652
|
+
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
3653
|
+
current = current.expression;
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3656
|
+
break;
|
|
3657
|
+
}
|
|
3658
|
+
return current;
|
|
3659
|
+
};
|
|
3660
|
+
//#endregion
|
|
3492
3661
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3493
|
-
const MESSAGE$
|
|
3662
|
+
const MESSAGE$44 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3494
3663
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
3495
3664
|
const DEFAULT_LABELLING_PROPS = [
|
|
3496
3665
|
"alt",
|
|
3497
3666
|
"aria-label",
|
|
3498
3667
|
"aria-labelledby"
|
|
3499
3668
|
];
|
|
3669
|
+
const ID_ATTRIBUTE = "id";
|
|
3670
|
+
const HTML_FOR_ATTRIBUTE = "htmlFor";
|
|
3671
|
+
const LABEL_ELEMENT = "label";
|
|
3500
3672
|
const DEFAULT_DEPTH = 5;
|
|
3501
3673
|
const MAX_DEPTH = 25;
|
|
3502
3674
|
const resolveSettings$46 = (settings) => {
|
|
@@ -3524,6 +3696,29 @@ const hasLabellingProp = (attributes, customAttributes) => {
|
|
|
3524
3696
|
}
|
|
3525
3697
|
return false;
|
|
3526
3698
|
};
|
|
3699
|
+
const toAttributeMatchKey = (kind, value) => {
|
|
3700
|
+
const trimmedValue = value.trim();
|
|
3701
|
+
return trimmedValue.length > 0 ? `${kind}:${trimmedValue}` : null;
|
|
3702
|
+
};
|
|
3703
|
+
const getLiteralAttributeMatchKey = (value) => {
|
|
3704
|
+
if (typeof value === "string") return toAttributeMatchKey("literal", value);
|
|
3705
|
+
if (typeof value === "number") return toAttributeMatchKey("literal", String(value));
|
|
3706
|
+
return null;
|
|
3707
|
+
};
|
|
3708
|
+
const getAttributeMatchKey = (attribute) => {
|
|
3709
|
+
if (!attribute?.value) return null;
|
|
3710
|
+
const value = attribute.value;
|
|
3711
|
+
if (isNodeOfType(value, "Literal")) return getLiteralAttributeMatchKey(value.value);
|
|
3712
|
+
if (!isNodeOfType(value, "JSXExpressionContainer")) return null;
|
|
3713
|
+
const expression = value.expression;
|
|
3714
|
+
if (isNodeOfType(expression, "Identifier")) return toAttributeMatchKey("identifier", expression.name);
|
|
3715
|
+
if (isNodeOfType(expression, "Literal")) return getLiteralAttributeMatchKey(expression.value);
|
|
3716
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
3717
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
3718
|
+
return staticValue === null ? null : toAttributeMatchKey("literal", staticValue);
|
|
3719
|
+
}
|
|
3720
|
+
return null;
|
|
3721
|
+
};
|
|
3527
3722
|
const checkChildForLabel = (child, currentDepth, context) => {
|
|
3528
3723
|
if (currentDepth > context.depth) return false;
|
|
3529
3724
|
if (isNodeOfType(child, "JSXExpressionContainer")) return true;
|
|
@@ -3539,6 +3734,55 @@ const checkChildForLabel = (child, currentDepth, context) => {
|
|
|
3539
3734
|
}
|
|
3540
3735
|
return false;
|
|
3541
3736
|
};
|
|
3737
|
+
const hasAccessibleLabelText = (element, context) => {
|
|
3738
|
+
if (hasLabellingProp(element.openingElement.attributes, context.customAttributes)) return true;
|
|
3739
|
+
return element.children.some((child) => checkChildForLabel(child, 1, context));
|
|
3740
|
+
};
|
|
3741
|
+
const isFunctionBoundary = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
3742
|
+
const hasAncestorLabel = (element, context) => {
|
|
3743
|
+
let current = element.parent;
|
|
3744
|
+
while (current) {
|
|
3745
|
+
if (isFunctionBoundary(current)) break;
|
|
3746
|
+
if (isNodeOfType(current, "JSXElement")) {
|
|
3747
|
+
if (getElementType(current.openingElement, context.settings) === LABEL_ELEMENT && hasAccessibleLabelText(current, context)) return true;
|
|
3748
|
+
}
|
|
3749
|
+
current = current.parent ?? null;
|
|
3750
|
+
}
|
|
3751
|
+
return false;
|
|
3752
|
+
};
|
|
3753
|
+
const findEnclosingJsxTreeRoot = (element) => {
|
|
3754
|
+
let root = element;
|
|
3755
|
+
let current = element.parent;
|
|
3756
|
+
while (current) {
|
|
3757
|
+
if (isFunctionBoundary(current)) break;
|
|
3758
|
+
if (isNodeOfType(current, "JSXElement") || isNodeOfType(current, "JSXFragment")) root = current;
|
|
3759
|
+
current = current.parent ?? null;
|
|
3760
|
+
}
|
|
3761
|
+
return root;
|
|
3762
|
+
};
|
|
3763
|
+
const collectJsxFromExpression = (rawExpression) => {
|
|
3764
|
+
const expression = stripParenExpression(rawExpression);
|
|
3765
|
+
if (isNodeOfType(expression, "JSXElement") || isNodeOfType(expression, "JSXFragment")) return [expression];
|
|
3766
|
+
if (isNodeOfType(expression, "LogicalExpression")) return [...collectJsxFromExpression(expression.left), ...collectJsxFromExpression(expression.right)];
|
|
3767
|
+
if (isNodeOfType(expression, "ConditionalExpression")) return [...collectJsxFromExpression(expression.consequent), ...collectJsxFromExpression(expression.alternate)];
|
|
3768
|
+
return [];
|
|
3769
|
+
};
|
|
3770
|
+
const searchForHtmlForLabel = (node, controlIdKey, context) => {
|
|
3771
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return collectJsxFromExpression(node.expression).some((jsxNode) => searchForHtmlForLabel(jsxNode, controlIdKey, context));
|
|
3772
|
+
const children = isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment") ? node.children : [];
|
|
3773
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
3774
|
+
if (getElementType(node.openingElement, context.settings) === LABEL_ELEMENT) {
|
|
3775
|
+
if (getAttributeMatchKey(hasJsxPropIgnoreCase(node.openingElement.attributes, HTML_FOR_ATTRIBUTE)) === controlIdKey && hasAccessibleLabelText(node, context)) return true;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
for (const child of children) if (searchForHtmlForLabel(child, controlIdKey, context)) return true;
|
|
3779
|
+
return false;
|
|
3780
|
+
};
|
|
3781
|
+
const hasHtmlForLabel = (element, context) => {
|
|
3782
|
+
const controlIdKey = getAttributeMatchKey(hasJsxPropIgnoreCase(element.openingElement.attributes, ID_ATTRIBUTE));
|
|
3783
|
+
if (controlIdKey === null) return false;
|
|
3784
|
+
return searchForHtmlForLabel(findEnclosingJsxTreeRoot(element), controlIdKey, context);
|
|
3785
|
+
};
|
|
3542
3786
|
const controlHasAssociatedLabel = defineRule({
|
|
3543
3787
|
id: "control-has-associated-label",
|
|
3544
3788
|
tags: ["react-jsx-only"],
|
|
@@ -3570,10 +3814,12 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3570
3814
|
controlComponents: settings.controlComponents,
|
|
3571
3815
|
settings: context.settings
|
|
3572
3816
|
};
|
|
3817
|
+
if (hasAncestorLabel(node, checkContext)) return;
|
|
3818
|
+
if (hasHtmlForLabel(node, checkContext)) return;
|
|
3573
3819
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
3574
3820
|
context.report({
|
|
3575
3821
|
node: opening,
|
|
3576
|
-
message: MESSAGE$
|
|
3822
|
+
message: MESSAGE$44
|
|
3577
3823
|
});
|
|
3578
3824
|
} };
|
|
3579
3825
|
}
|
|
@@ -3863,25 +4109,6 @@ const noVagueButtonLabel = defineRule({
|
|
|
3863
4109
|
} })
|
|
3864
4110
|
});
|
|
3865
4111
|
//#endregion
|
|
3866
|
-
//#region src/plugin/utils/find-program-root.ts
|
|
3867
|
-
/**
|
|
3868
|
-
* Walks up the AST `parent` chain from `node` to its enclosing
|
|
3869
|
-
* `Program` root and returns it; `null` when the chain doesn't lead
|
|
3870
|
-
* to a `Program` (e.g. detached fragments used by test utilities).
|
|
3871
|
-
*
|
|
3872
|
-
* Was duplicated byte-identical across seven sites (five rule files
|
|
3873
|
-
* + two utility modules). Promoted to a shared helper so adding a
|
|
3874
|
-
* new ESTree top-level shape only touches one place.
|
|
3875
|
-
*/
|
|
3876
|
-
const findProgramRoot = (node) => {
|
|
3877
|
-
let cursor = node;
|
|
3878
|
-
while (cursor) {
|
|
3879
|
-
if (isNodeOfType(cursor, "Program")) return cursor;
|
|
3880
|
-
cursor = cursor.parent ?? null;
|
|
3881
|
-
}
|
|
3882
|
-
return null;
|
|
3883
|
-
};
|
|
3884
|
-
//#endregion
|
|
3885
4112
|
//#region src/plugin/utils/is-es5-component.ts
|
|
3886
4113
|
const PRAGMA$2 = "React";
|
|
3887
4114
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -3916,7 +4143,7 @@ const isEs6Component = (node) => {
|
|
|
3916
4143
|
};
|
|
3917
4144
|
//#endregion
|
|
3918
4145
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
3919
|
-
const MESSAGE$
|
|
4146
|
+
const MESSAGE$43 = "Component is missing a `displayName` — assign one for easier debugging.";
|
|
3920
4147
|
const resolveSettings$45 = (settings) => {
|
|
3921
4148
|
const reactDoctor = settings?.["react-doctor"];
|
|
3922
4149
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
@@ -4112,7 +4339,7 @@ const displayName = defineRule({
|
|
|
4112
4339
|
const reportAt = (node) => {
|
|
4113
4340
|
context.report({
|
|
4114
4341
|
node,
|
|
4115
|
-
message: MESSAGE$
|
|
4342
|
+
message: MESSAGE$43
|
|
4116
4343
|
});
|
|
4117
4344
|
};
|
|
4118
4345
|
return {
|
|
@@ -4235,7 +4462,7 @@ const displayName = defineRule({
|
|
|
4235
4462
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4236
4463
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4237
4464
|
if (!node || typeof node !== "object") return;
|
|
4238
|
-
if (isFunctionLike(node)) return;
|
|
4465
|
+
if (isFunctionLike$1(node)) return;
|
|
4239
4466
|
visitor(node);
|
|
4240
4467
|
const nodeRecord = node;
|
|
4241
4468
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4323,7 +4550,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4323
4550
|
let didFindRelease = false;
|
|
4324
4551
|
walkAst(node, (child) => {
|
|
4325
4552
|
if (didFindRelease) return false;
|
|
4326
|
-
if (child !== node && isFunctionLike(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4553
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4327
4554
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4328
4555
|
didFindRelease = true;
|
|
4329
4556
|
return false;
|
|
@@ -4332,7 +4559,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4332
4559
|
return didFindRelease;
|
|
4333
4560
|
};
|
|
4334
4561
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4335
|
-
if (!isFunctionLike(node)) return false;
|
|
4562
|
+
if (!isFunctionLike$1(node)) return false;
|
|
4336
4563
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4337
4564
|
};
|
|
4338
4565
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
@@ -4574,7 +4801,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4574
4801
|
};
|
|
4575
4802
|
const isFunctionBodyBlock = (block) => {
|
|
4576
4803
|
if (!block.parent) return false;
|
|
4577
|
-
return isFunctionLike(block.parent);
|
|
4804
|
+
return isFunctionLike$1(block.parent);
|
|
4578
4805
|
};
|
|
4579
4806
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4580
4807
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -4702,7 +4929,7 @@ const setNodeScope = (node, state) => {
|
|
|
4702
4929
|
state.nodeScope.set(node, state.currentScope);
|
|
4703
4930
|
};
|
|
4704
4931
|
const walk = (node, state) => {
|
|
4705
|
-
if (isFunctionLike(node)) {
|
|
4932
|
+
if (isFunctionLike$1(node)) {
|
|
4706
4933
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
4707
4934
|
setNodeScope(node, state);
|
|
4708
4935
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -4952,7 +5179,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
4952
5179
|
const out = [];
|
|
4953
5180
|
const seen = /* @__PURE__ */ new Set();
|
|
4954
5181
|
const visit = (node) => {
|
|
4955
|
-
if (node !== functionNode && isFunctionLike(node)) {
|
|
5182
|
+
if (node !== functionNode && isFunctionLike$1(node)) {
|
|
4956
5183
|
const innerCaptures = closureCaptures(node, scopes);
|
|
4957
5184
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
4958
5185
|
if (!seen.has(reference.id)) {
|
|
@@ -5973,7 +6200,7 @@ const flattenJsxName$1 = (name) => {
|
|
|
5973
6200
|
return "";
|
|
5974
6201
|
};
|
|
5975
6202
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
5976
|
-
const buildMessage$
|
|
6203
|
+
const buildMessage$23 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
|
|
5977
6204
|
const forbidComponentProps = defineRule({
|
|
5978
6205
|
id: "forbid-component-props",
|
|
5979
6206
|
severity: "warn",
|
|
@@ -5999,7 +6226,7 @@ const forbidComponentProps = defineRule({
|
|
|
5999
6226
|
if (!isForbiddenForTag(entry, tag)) continue;
|
|
6000
6227
|
context.report({
|
|
6001
6228
|
node: attribute,
|
|
6002
|
-
message: buildMessage$
|
|
6229
|
+
message: buildMessage$23(propName, entry.message)
|
|
6003
6230
|
});
|
|
6004
6231
|
break;
|
|
6005
6232
|
}
|
|
@@ -6009,7 +6236,7 @@ const forbidComponentProps = defineRule({
|
|
|
6009
6236
|
});
|
|
6010
6237
|
//#endregion
|
|
6011
6238
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6012
|
-
const buildMessage$
|
|
6239
|
+
const buildMessage$22 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
|
|
6013
6240
|
const resolveSettings$43 = (settings) => {
|
|
6014
6241
|
const reactDoctor = settings?.["react-doctor"];
|
|
6015
6242
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
@@ -6047,7 +6274,7 @@ const forbidDomProps = defineRule({
|
|
|
6047
6274
|
if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
|
|
6048
6275
|
context.report({
|
|
6049
6276
|
node: attribute.name,
|
|
6050
|
-
message: buildMessage$
|
|
6277
|
+
message: buildMessage$22(propName, descriptor.message)
|
|
6051
6278
|
});
|
|
6052
6279
|
}
|
|
6053
6280
|
} };
|
|
@@ -6117,7 +6344,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6117
6344
|
};
|
|
6118
6345
|
//#endregion
|
|
6119
6346
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6120
|
-
const buildMessage$
|
|
6347
|
+
const buildMessage$21 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
|
|
6121
6348
|
const resolveSettings$42 = (settings) => {
|
|
6122
6349
|
const reactDoctor = settings?.["react-doctor"];
|
|
6123
6350
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
@@ -6141,7 +6368,7 @@ const forbidElements = defineRule({
|
|
|
6141
6368
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6142
6369
|
context.report({
|
|
6143
6370
|
node: node.name,
|
|
6144
|
-
message: buildMessage$
|
|
6371
|
+
message: buildMessage$21(fullName, forbidMap.get(fullName))
|
|
6145
6372
|
});
|
|
6146
6373
|
},
|
|
6147
6374
|
CallExpression(node) {
|
|
@@ -6161,7 +6388,7 @@ const forbidElements = defineRule({
|
|
|
6161
6388
|
if (!elementName || !forbidMap.has(elementName)) return;
|
|
6162
6389
|
context.report({
|
|
6163
6390
|
node: firstArgument,
|
|
6164
|
-
message: buildMessage$
|
|
6391
|
+
message: buildMessage$21(elementName, forbidMap.get(elementName))
|
|
6165
6392
|
});
|
|
6166
6393
|
}
|
|
6167
6394
|
};
|
|
@@ -6169,7 +6396,7 @@ const forbidElements = defineRule({
|
|
|
6169
6396
|
});
|
|
6170
6397
|
//#endregion
|
|
6171
6398
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
6172
|
-
const MESSAGE$
|
|
6399
|
+
const MESSAGE$42 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
|
|
6173
6400
|
const forwardRefUsesRef = defineRule({
|
|
6174
6401
|
id: "forward-ref-uses-ref",
|
|
6175
6402
|
severity: "warn",
|
|
@@ -6188,13 +6415,13 @@ const forwardRefUsesRef = defineRule({
|
|
|
6188
6415
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
6189
6416
|
context.report({
|
|
6190
6417
|
node: inner,
|
|
6191
|
-
message: MESSAGE$
|
|
6418
|
+
message: MESSAGE$42
|
|
6192
6419
|
});
|
|
6193
6420
|
} })
|
|
6194
6421
|
});
|
|
6195
6422
|
//#endregion
|
|
6196
6423
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
6197
|
-
const MESSAGE$
|
|
6424
|
+
const MESSAGE$41 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
|
|
6198
6425
|
const DEFAULT_HEADING_TAGS = [
|
|
6199
6426
|
"h1",
|
|
6200
6427
|
"h2",
|
|
@@ -6226,7 +6453,7 @@ const headingHasContent = defineRule({
|
|
|
6226
6453
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6227
6454
|
context.report({
|
|
6228
6455
|
node,
|
|
6229
|
-
message: MESSAGE$
|
|
6456
|
+
message: MESSAGE$41
|
|
6230
6457
|
});
|
|
6231
6458
|
} };
|
|
6232
6459
|
}
|
|
@@ -6327,8 +6554,42 @@ const hookUseState = defineRule({
|
|
|
6327
6554
|
}
|
|
6328
6555
|
});
|
|
6329
6556
|
//#endregion
|
|
6557
|
+
//#region src/plugin/rules/state-and-effects/hooks-no-nan-in-deps.ts
|
|
6558
|
+
const HOOKS_WITH_DEP_ARRAY = new Set([
|
|
6559
|
+
"useEffect",
|
|
6560
|
+
"useLayoutEffect",
|
|
6561
|
+
"useInsertionEffect",
|
|
6562
|
+
"useCallback",
|
|
6563
|
+
"useMemo",
|
|
6564
|
+
"useImperativeHandle"
|
|
6565
|
+
]);
|
|
6566
|
+
const NAN_MESSAGE = "`NaN` in a hook dependency array is almost always a coercion bug upstream (e.g. `Number(input)` returned `NaN` from an unchecked value). React's `Object.is` comparator treats `NaN` as equal to `NaN`, so once a poisoned value lands in the deps the hook keeps firing as if nothing changed — and any later transition between `NaN` and a real number can wedge tracking. Guard the value before passing it.";
|
|
6567
|
+
const isNanLiteral = (node) => {
|
|
6568
|
+
if (isNodeOfType(node, "Identifier") && node.name === "NaN") return true;
|
|
6569
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "Number" && isNodeOfType(node.property, "Identifier") && node.property.name === "NaN") return true;
|
|
6570
|
+
return false;
|
|
6571
|
+
};
|
|
6572
|
+
const hooksNoNanInDeps = defineRule({
|
|
6573
|
+
id: "hooks-no-nan-in-deps",
|
|
6574
|
+
severity: "warn",
|
|
6575
|
+
recommendation: "Remove `NaN` (or `Number.NaN`) from the dependency array. If a value can be NaN at runtime, normalise it (`Number.isNaN(x) ? 0 : x`) before passing it.",
|
|
6576
|
+
create: (context) => ({ CallExpression(node) {
|
|
6577
|
+
if (!isHookCall$1(node, HOOKS_WITH_DEP_ARRAY)) return;
|
|
6578
|
+
const depsIndex = getCalleeName$1(node) === "useImperativeHandle" ? 2 : 1;
|
|
6579
|
+
const depsArgument = node.arguments[depsIndex];
|
|
6580
|
+
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
6581
|
+
for (const element of depsArgument.elements) {
|
|
6582
|
+
if (!element) continue;
|
|
6583
|
+
if (isNanLiteral(element)) context.report({
|
|
6584
|
+
node: element,
|
|
6585
|
+
message: NAN_MESSAGE
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
} })
|
|
6589
|
+
});
|
|
6590
|
+
//#endregion
|
|
6330
6591
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6331
|
-
const MESSAGE$
|
|
6592
|
+
const MESSAGE$40 = "`<html>` element must have a non-empty `lang` attribute.";
|
|
6332
6593
|
const resolveSettings$39 = (settings) => {
|
|
6333
6594
|
const reactDoctor = settings?.["react-doctor"];
|
|
6334
6595
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -6375,7 +6636,7 @@ const htmlHasLang = defineRule({
|
|
|
6375
6636
|
if (!lang) {
|
|
6376
6637
|
context.report({
|
|
6377
6638
|
node: node.name,
|
|
6378
|
-
message: MESSAGE$
|
|
6639
|
+
message: MESSAGE$40
|
|
6379
6640
|
});
|
|
6380
6641
|
return;
|
|
6381
6642
|
}
|
|
@@ -6383,20 +6644,224 @@ const htmlHasLang = defineRule({
|
|
|
6383
6644
|
if (verdict === "missing" || verdict === "empty") {
|
|
6384
6645
|
context.report({
|
|
6385
6646
|
node: lang,
|
|
6386
|
-
message: MESSAGE$
|
|
6647
|
+
message: MESSAGE$40
|
|
6387
6648
|
});
|
|
6388
6649
|
return;
|
|
6389
6650
|
}
|
|
6390
6651
|
if (hasSpread && !lang) context.report({
|
|
6391
6652
|
node: node.name,
|
|
6392
|
-
message: MESSAGE$
|
|
6653
|
+
message: MESSAGE$40
|
|
6393
6654
|
});
|
|
6394
6655
|
} };
|
|
6395
6656
|
}
|
|
6396
6657
|
});
|
|
6397
6658
|
//#endregion
|
|
6659
|
+
//#region src/plugin/rules/correctness/html-no-invalid-paragraph-child.ts
|
|
6660
|
+
const BLOCK_LEVEL_ELEMENTS = new Set([
|
|
6661
|
+
"address",
|
|
6662
|
+
"article",
|
|
6663
|
+
"aside",
|
|
6664
|
+
"blockquote",
|
|
6665
|
+
"details",
|
|
6666
|
+
"div",
|
|
6667
|
+
"dl",
|
|
6668
|
+
"fieldset",
|
|
6669
|
+
"figcaption",
|
|
6670
|
+
"figure",
|
|
6671
|
+
"footer",
|
|
6672
|
+
"form",
|
|
6673
|
+
"h1",
|
|
6674
|
+
"h2",
|
|
6675
|
+
"h3",
|
|
6676
|
+
"h4",
|
|
6677
|
+
"h5",
|
|
6678
|
+
"h6",
|
|
6679
|
+
"header",
|
|
6680
|
+
"hgroup",
|
|
6681
|
+
"hr",
|
|
6682
|
+
"main",
|
|
6683
|
+
"menu",
|
|
6684
|
+
"nav",
|
|
6685
|
+
"ol",
|
|
6686
|
+
"p",
|
|
6687
|
+
"pre",
|
|
6688
|
+
"search",
|
|
6689
|
+
"section",
|
|
6690
|
+
"table",
|
|
6691
|
+
"ul"
|
|
6692
|
+
]);
|
|
6693
|
+
const buildMessage$20 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
|
|
6694
|
+
const isParagraphElement = (candidate) => {
|
|
6695
|
+
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6696
|
+
const opening = candidate.openingElement;
|
|
6697
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
6698
|
+
return opening.name.name === "p";
|
|
6699
|
+
};
|
|
6700
|
+
const findEnclosingParagraph = (openingElement) => {
|
|
6701
|
+
const owningElement = openingElement.parent;
|
|
6702
|
+
if (!owningElement) return null;
|
|
6703
|
+
let ancestor = owningElement.parent;
|
|
6704
|
+
while (ancestor) {
|
|
6705
|
+
if (isParagraphElement(ancestor)) return ancestor;
|
|
6706
|
+
ancestor = ancestor.parent ?? null;
|
|
6707
|
+
}
|
|
6708
|
+
return null;
|
|
6709
|
+
};
|
|
6710
|
+
const htmlNoInvalidParagraphChild = defineRule({
|
|
6711
|
+
id: "html-no-invalid-paragraph-child",
|
|
6712
|
+
severity: "warn",
|
|
6713
|
+
recommendation: "Replace the surrounding `<p>` with a `<div>`, or hoist the block-level child outside the paragraph.",
|
|
6714
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
6715
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
6716
|
+
const childTagName = node.name.name;
|
|
6717
|
+
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
6718
|
+
if (!findEnclosingParagraph(node)) return;
|
|
6719
|
+
context.report({
|
|
6720
|
+
node: node.name,
|
|
6721
|
+
message: buildMessage$20(childTagName)
|
|
6722
|
+
});
|
|
6723
|
+
} })
|
|
6724
|
+
});
|
|
6725
|
+
//#endregion
|
|
6726
|
+
//#region src/plugin/rules/correctness/html-no-invalid-table-nesting.ts
|
|
6727
|
+
const TABLE_ELEMENTS = new Set([
|
|
6728
|
+
"table",
|
|
6729
|
+
"thead",
|
|
6730
|
+
"tbody",
|
|
6731
|
+
"tfoot",
|
|
6732
|
+
"tr",
|
|
6733
|
+
"td",
|
|
6734
|
+
"th"
|
|
6735
|
+
]);
|
|
6736
|
+
const ROW_GROUPS = new Set([
|
|
6737
|
+
"thead",
|
|
6738
|
+
"tbody",
|
|
6739
|
+
"tfoot"
|
|
6740
|
+
]);
|
|
6741
|
+
const buildMessage$19 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
|
|
6742
|
+
const buildNestedTableMessage = () => "Improper table nesting — `<table>` cannot be a direct descendant of another table element. Tables can only nest inside a `<td>` or `<th>` cell of an outer table.";
|
|
6743
|
+
const getHostTagName = (jsxElement) => {
|
|
6744
|
+
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
6745
|
+
const opening = jsxElement.openingElement;
|
|
6746
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
6747
|
+
const tagName = opening.name.name;
|
|
6748
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
6749
|
+
return tagName;
|
|
6750
|
+
};
|
|
6751
|
+
const findClosestHostAncestor = (jsxElement) => {
|
|
6752
|
+
let ancestor = jsxElement.parent;
|
|
6753
|
+
while (ancestor) {
|
|
6754
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
6755
|
+
const opening = ancestor.openingElement;
|
|
6756
|
+
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
6757
|
+
const ancestorTag = opening.name.name;
|
|
6758
|
+
if (ancestorTag.length === 0) {
|
|
6759
|
+
ancestor = ancestor.parent ?? null;
|
|
6760
|
+
continue;
|
|
6761
|
+
}
|
|
6762
|
+
if (ancestorTag[0] === ancestorTag[0].toLowerCase()) return {
|
|
6763
|
+
kind: "host",
|
|
6764
|
+
tagName: ancestorTag,
|
|
6765
|
+
element: ancestor
|
|
6766
|
+
};
|
|
6767
|
+
return { kind: "opaque" };
|
|
6768
|
+
}
|
|
6769
|
+
return { kind: "opaque" };
|
|
6770
|
+
}
|
|
6771
|
+
ancestor = ancestor.parent ?? null;
|
|
6772
|
+
}
|
|
6773
|
+
return { kind: "none" };
|
|
6774
|
+
};
|
|
6775
|
+
const NESTED_TABLE_BOUNDARY_CELLS = new Set(["td", "th"]);
|
|
6776
|
+
const findEnclosingTable = (jsxElement) => {
|
|
6777
|
+
let ancestor = jsxElement.parent;
|
|
6778
|
+
while (ancestor) {
|
|
6779
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
6780
|
+
const tag = getHostTagName(ancestor);
|
|
6781
|
+
if (tag === "table") return ancestor;
|
|
6782
|
+
if (tag !== null && NESTED_TABLE_BOUNDARY_CELLS.has(tag)) return null;
|
|
6783
|
+
if (tag === null) return null;
|
|
6784
|
+
}
|
|
6785
|
+
ancestor = ancestor.parent ?? null;
|
|
6786
|
+
}
|
|
6787
|
+
return null;
|
|
6788
|
+
};
|
|
6789
|
+
const htmlNoInvalidTableNesting = defineRule({
|
|
6790
|
+
id: "html-no-invalid-table-nesting",
|
|
6791
|
+
severity: "warn",
|
|
6792
|
+
recommendation: "Wrap each table element in its required parent: `<thead>`/`<tbody>`/`<tfoot>` directly inside `<table>`, `<tr>` inside a row group, `<td>`/`<th>` inside `<tr>`. Browsers reflow malformed table structure silently — the only safe fix is to author the markup to spec.",
|
|
6793
|
+
create: (context) => ({ JSXElement(node) {
|
|
6794
|
+
const tagName = getHostTagName(node);
|
|
6795
|
+
if (!tagName || !TABLE_ELEMENTS.has(tagName)) return;
|
|
6796
|
+
if (tagName === "table") {
|
|
6797
|
+
if (findEnclosingTable(node)) context.report({
|
|
6798
|
+
node: node.openingElement.name,
|
|
6799
|
+
message: buildNestedTableMessage()
|
|
6800
|
+
});
|
|
6801
|
+
return;
|
|
6802
|
+
}
|
|
6803
|
+
const closestHost = findClosestHostAncestor(node);
|
|
6804
|
+
if (closestHost.kind !== "host") return;
|
|
6805
|
+
const actualParent = closestHost.tagName;
|
|
6806
|
+
if (ROW_GROUPS.has(tagName)) {
|
|
6807
|
+
if (actualParent !== "table") context.report({
|
|
6808
|
+
node: node.openingElement.name,
|
|
6809
|
+
message: buildMessage$19(tagName, "`<table>`", actualParent)
|
|
6810
|
+
});
|
|
6811
|
+
return;
|
|
6812
|
+
}
|
|
6813
|
+
if (tagName === "tr") {
|
|
6814
|
+
if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
|
|
6815
|
+
node: node.openingElement.name,
|
|
6816
|
+
message: buildMessage$19(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
|
|
6817
|
+
});
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6820
|
+
if (tagName === "td" || tagName === "th") {
|
|
6821
|
+
if (actualParent !== "tr") context.report({
|
|
6822
|
+
node: node.openingElement.name,
|
|
6823
|
+
message: buildMessage$19(tagName, "`<tr>`", actualParent)
|
|
6824
|
+
});
|
|
6825
|
+
}
|
|
6826
|
+
} })
|
|
6827
|
+
});
|
|
6828
|
+
//#endregion
|
|
6829
|
+
//#region src/plugin/rules/correctness/html-no-nested-interactive.ts
|
|
6830
|
+
const buildMessage$18 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
|
|
6831
|
+
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
6832
|
+
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6833
|
+
const opening = candidate.openingElement;
|
|
6834
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
6835
|
+
return opening.name.name === tagName;
|
|
6836
|
+
};
|
|
6837
|
+
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
6838
|
+
const owningElement = openingElement.parent;
|
|
6839
|
+
if (!owningElement) return null;
|
|
6840
|
+
let ancestor = owningElement.parent;
|
|
6841
|
+
while (ancestor) {
|
|
6842
|
+
if (isJsxElementWithTagName(ancestor, tagName)) return ancestor;
|
|
6843
|
+
ancestor = ancestor.parent ?? null;
|
|
6844
|
+
}
|
|
6845
|
+
return null;
|
|
6846
|
+
};
|
|
6847
|
+
const htmlNoNestedInteractive = defineRule({
|
|
6848
|
+
id: "html-no-nested-interactive",
|
|
6849
|
+
severity: "warn",
|
|
6850
|
+
recommendation: "Hoist the inner `<a>` or `<button>` to a sibling, or replace the outer one with a non-interactive wrapper (e.g. a `<div>` or `<span>`).",
|
|
6851
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
6852
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
6853
|
+
const tagName = node.name.name;
|
|
6854
|
+
if (tagName !== "a" && tagName !== "button") return;
|
|
6855
|
+
if (!findEnclosingSameTag(node, tagName)) return;
|
|
6856
|
+
context.report({
|
|
6857
|
+
node: node.name,
|
|
6858
|
+
message: buildMessage$18(tagName)
|
|
6859
|
+
});
|
|
6860
|
+
} })
|
|
6861
|
+
});
|
|
6862
|
+
//#endregion
|
|
6398
6863
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
6399
|
-
const MESSAGE$
|
|
6864
|
+
const MESSAGE$39 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
|
|
6400
6865
|
const evaluateTitleValue = (value) => {
|
|
6401
6866
|
if (!value) return "missing";
|
|
6402
6867
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -6435,14 +6900,14 @@ const iframeHasTitle = defineRule({
|
|
|
6435
6900
|
if (!titleAttr) {
|
|
6436
6901
|
if (hasSpread || tag === "iframe") context.report({
|
|
6437
6902
|
node: node.name,
|
|
6438
|
-
message: MESSAGE$
|
|
6903
|
+
message: MESSAGE$39
|
|
6439
6904
|
});
|
|
6440
6905
|
return;
|
|
6441
6906
|
}
|
|
6442
6907
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
6443
6908
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
6444
6909
|
node: titleAttr,
|
|
6445
|
-
message: MESSAGE$
|
|
6910
|
+
message: MESSAGE$39
|
|
6446
6911
|
});
|
|
6447
6912
|
} })
|
|
6448
6913
|
});
|
|
@@ -6545,7 +7010,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
6545
7010
|
});
|
|
6546
7011
|
//#endregion
|
|
6547
7012
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
6548
|
-
const MESSAGE$
|
|
7013
|
+
const MESSAGE$38 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
|
|
6549
7014
|
const DEFAULT_COMPONENTS = ["img"];
|
|
6550
7015
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
6551
7016
|
"image",
|
|
@@ -6607,7 +7072,7 @@ const imgRedundantAlt = defineRule({
|
|
|
6607
7072
|
if (!altAttribute) return;
|
|
6608
7073
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
6609
7074
|
node: altAttribute,
|
|
6610
|
-
message: MESSAGE$
|
|
7075
|
+
message: MESSAGE$38
|
|
6611
7076
|
});
|
|
6612
7077
|
} };
|
|
6613
7078
|
}
|
|
@@ -6720,6 +7185,437 @@ const interactiveSupportsFocus = defineRule({
|
|
|
6720
7185
|
}
|
|
6721
7186
|
});
|
|
6722
7187
|
//#endregion
|
|
7188
|
+
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
7189
|
+
const collectFromProgram = (programRoot) => {
|
|
7190
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
7191
|
+
const visit = (node) => {
|
|
7192
|
+
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
7193
|
+
const source = node.source.value;
|
|
7194
|
+
if (typeof source !== "string") return;
|
|
7195
|
+
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
7196
|
+
if (!("local" in specifier) || !specifier.local) continue;
|
|
7197
|
+
const local = specifier.local;
|
|
7198
|
+
if (typeof local.name !== "string") continue;
|
|
7199
|
+
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
7200
|
+
source,
|
|
7201
|
+
imported: null,
|
|
7202
|
+
isDefault: true,
|
|
7203
|
+
isNamespace: false
|
|
7204
|
+
});
|
|
7205
|
+
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
7206
|
+
source,
|
|
7207
|
+
imported: null,
|
|
7208
|
+
isDefault: false,
|
|
7209
|
+
isNamespace: true
|
|
7210
|
+
});
|
|
7211
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
7212
|
+
const importedNode = specifier.imported;
|
|
7213
|
+
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
7214
|
+
lookup.set(local.name, {
|
|
7215
|
+
source,
|
|
7216
|
+
imported: importedName,
|
|
7217
|
+
isDefault: false,
|
|
7218
|
+
isNamespace: false
|
|
7219
|
+
});
|
|
7220
|
+
}
|
|
7221
|
+
}
|
|
7222
|
+
return;
|
|
7223
|
+
}
|
|
7224
|
+
const nodeRecord = node;
|
|
7225
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
7226
|
+
if (key === "parent") continue;
|
|
7227
|
+
const child = nodeRecord[key];
|
|
7228
|
+
if (Array.isArray(child)) {
|
|
7229
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
7230
|
+
} else if (isAstNode(child)) visit(child);
|
|
7231
|
+
}
|
|
7232
|
+
};
|
|
7233
|
+
visit(programRoot);
|
|
7234
|
+
return lookup;
|
|
7235
|
+
};
|
|
7236
|
+
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
7237
|
+
const getImportLookup = (node) => {
|
|
7238
|
+
const programRoot = findProgramRoot(node);
|
|
7239
|
+
if (!programRoot) return null;
|
|
7240
|
+
let cached = importLookupCache.get(programRoot);
|
|
7241
|
+
if (!cached) {
|
|
7242
|
+
cached = collectFromProgram(programRoot);
|
|
7243
|
+
importLookupCache.set(programRoot, cached);
|
|
7244
|
+
}
|
|
7245
|
+
return cached;
|
|
7246
|
+
};
|
|
7247
|
+
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7248
|
+
const lookup = getImportLookup(contextNode);
|
|
7249
|
+
if (!lookup) return false;
|
|
7250
|
+
const info = lookup.get(localIdentifierName);
|
|
7251
|
+
if (!info) return false;
|
|
7252
|
+
return info.source === moduleSource;
|
|
7253
|
+
};
|
|
7254
|
+
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7255
|
+
const lookup = getImportLookup(contextNode);
|
|
7256
|
+
if (!lookup) return null;
|
|
7257
|
+
const info = lookup.get(localIdentifierName);
|
|
7258
|
+
if (!info) return null;
|
|
7259
|
+
if (info.source !== moduleSource) return null;
|
|
7260
|
+
return info.imported;
|
|
7261
|
+
};
|
|
7262
|
+
//#endregion
|
|
7263
|
+
//#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
|
|
7264
|
+
const isAtomFromJotai = (callExpression) => {
|
|
7265
|
+
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
7266
|
+
const localName = callExpression.callee.name;
|
|
7267
|
+
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7268
|
+
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7269
|
+
};
|
|
7270
|
+
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7271
|
+
const getFirstParameterName = (fn) => {
|
|
7272
|
+
const parameters = fn.params ?? [];
|
|
7273
|
+
if (parameters.length !== 1) return null;
|
|
7274
|
+
const first = parameters[0];
|
|
7275
|
+
return isNodeOfType(first, "Identifier") ? first.name : null;
|
|
7276
|
+
};
|
|
7277
|
+
const FRESH_ARRAY_INSTANCE_METHODS = new Set([
|
|
7278
|
+
"filter",
|
|
7279
|
+
"map",
|
|
7280
|
+
"flatMap",
|
|
7281
|
+
"slice",
|
|
7282
|
+
"concat",
|
|
7283
|
+
"flat",
|
|
7284
|
+
"toSorted",
|
|
7285
|
+
"toReversed",
|
|
7286
|
+
"toSpliced",
|
|
7287
|
+
"with",
|
|
7288
|
+
"sort",
|
|
7289
|
+
"reverse"
|
|
7290
|
+
]);
|
|
7291
|
+
const FRESH_STATIC_OBJECT_CALLS = {
|
|
7292
|
+
Object: new Set([
|
|
7293
|
+
"keys",
|
|
7294
|
+
"values",
|
|
7295
|
+
"entries",
|
|
7296
|
+
"fromEntries",
|
|
7297
|
+
"assign",
|
|
7298
|
+
"create"
|
|
7299
|
+
]),
|
|
7300
|
+
Array: new Set(["from", "of"])
|
|
7301
|
+
};
|
|
7302
|
+
const freshFromObjectLiteral = (expression) => {
|
|
7303
|
+
if (isNodeOfType(expression, "ObjectExpression")) return {
|
|
7304
|
+
kind: "object",
|
|
7305
|
+
reportNode: expression
|
|
7306
|
+
};
|
|
7307
|
+
if (isNodeOfType(expression, "ArrayExpression")) return {
|
|
7308
|
+
kind: "array",
|
|
7309
|
+
reportNode: expression
|
|
7310
|
+
};
|
|
7311
|
+
return null;
|
|
7312
|
+
};
|
|
7313
|
+
const freshFromMethodChain = (expression) => {
|
|
7314
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
7315
|
+
const callee = expression.callee;
|
|
7316
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
7317
|
+
if (callee.computed) return null;
|
|
7318
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
7319
|
+
const methodName = callee.property.name;
|
|
7320
|
+
if (FRESH_ARRAY_INSTANCE_METHODS.has(methodName)) return {
|
|
7321
|
+
kind: "array",
|
|
7322
|
+
reportNode: expression
|
|
7323
|
+
};
|
|
7324
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
7325
|
+
if (FRESH_STATIC_OBJECT_CALLS[callee.object.name]?.has(methodName)) return {
|
|
7326
|
+
kind: callee.object.name === "Array" || methodName === "keys" || methodName === "values" || methodName === "entries" ? "array" : "object",
|
|
7327
|
+
reportNode: expression
|
|
7328
|
+
};
|
|
7329
|
+
}
|
|
7330
|
+
return null;
|
|
7331
|
+
};
|
|
7332
|
+
const classifyReturnedExpression = (expression) => {
|
|
7333
|
+
if (!expression) return null;
|
|
7334
|
+
const inner = stripParenExpression(expression);
|
|
7335
|
+
const literalReturn = freshFromObjectLiteral(inner);
|
|
7336
|
+
if (literalReturn) return literalReturn;
|
|
7337
|
+
return freshFromMethodChain(inner);
|
|
7338
|
+
};
|
|
7339
|
+
const collectTopLevelReturnExpressions$1 = (block) => {
|
|
7340
|
+
const returns = [];
|
|
7341
|
+
walkAst(block, (child) => {
|
|
7342
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) return false;
|
|
7343
|
+
if (isNodeOfType(child, "ReturnStatement")) returns.push(child.argument);
|
|
7344
|
+
});
|
|
7345
|
+
return returns;
|
|
7346
|
+
};
|
|
7347
|
+
const getFreshReturnForFunction = (fn) => {
|
|
7348
|
+
const body = fn.body;
|
|
7349
|
+
if (!body) return null;
|
|
7350
|
+
if (!isNodeOfType(body, "BlockStatement")) return classifyReturnedExpression(body);
|
|
7351
|
+
const returnExpressions = collectTopLevelReturnExpressions$1(body);
|
|
7352
|
+
if (returnExpressions.length === 0) return null;
|
|
7353
|
+
let firstFresh = null;
|
|
7354
|
+
for (const returnArgument of returnExpressions) {
|
|
7355
|
+
const classification = classifyReturnedExpression(returnArgument);
|
|
7356
|
+
if (!classification) return null;
|
|
7357
|
+
if (!firstFresh) firstFresh = classification;
|
|
7358
|
+
}
|
|
7359
|
+
return firstFresh;
|
|
7360
|
+
};
|
|
7361
|
+
const functionBodyReferencesGetParameter = (fn, getParameterName) => {
|
|
7362
|
+
const body = fn.body;
|
|
7363
|
+
if (!body) return false;
|
|
7364
|
+
let found = false;
|
|
7365
|
+
walkAst(body, (child) => {
|
|
7366
|
+
if (found) return false;
|
|
7367
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) {
|
|
7368
|
+
if (child !== fn) return false;
|
|
7369
|
+
}
|
|
7370
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7371
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
7372
|
+
if (child.callee.name === getParameterName) {
|
|
7373
|
+
found = true;
|
|
7374
|
+
return false;
|
|
7375
|
+
}
|
|
7376
|
+
});
|
|
7377
|
+
return found;
|
|
7378
|
+
};
|
|
7379
|
+
const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
7380
|
+
id: "jotai-derived-atom-returns-fresh-object",
|
|
7381
|
+
severity: "warn",
|
|
7382
|
+
recommendation: "Split the derivation into multiple primitive derived atoms (each `Object.is`-dedupable), or wrap with `selectAtom(source, fn, shallow)` from jotai/utils if a wrapper object is required",
|
|
7383
|
+
create: (context) => ({ CallExpression(node) {
|
|
7384
|
+
if (!isAtomFromJotai(node)) return;
|
|
7385
|
+
const args = node.arguments ?? [];
|
|
7386
|
+
if (args.length === 0) return;
|
|
7387
|
+
const reader = args[0];
|
|
7388
|
+
if (!isFunctionLike(reader)) return;
|
|
7389
|
+
const getParameterName = getFirstParameterName(reader);
|
|
7390
|
+
if (!getParameterName) return;
|
|
7391
|
+
const freshReturn = getFreshReturnForFunction(reader);
|
|
7392
|
+
if (!freshReturn) return;
|
|
7393
|
+
if (!functionBodyReferencesGetParameter(reader, getParameterName)) return;
|
|
7394
|
+
const shape = freshReturn.kind === "object" ? "object" : "array";
|
|
7395
|
+
context.report({
|
|
7396
|
+
node: freshReturn.reportNode,
|
|
7397
|
+
message: `Derived atom returns a fresh ${shape} — jotai compares with Object.is, so every upstream notify re-renders every consumer. Split into per-field derived atoms or use \`selectAtom(source, fn, shallow)\``
|
|
7398
|
+
});
|
|
7399
|
+
} })
|
|
7400
|
+
});
|
|
7401
|
+
//#endregion
|
|
7402
|
+
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
7403
|
+
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
7404
|
+
const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
|
|
7405
|
+
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
7406
|
+
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7407
|
+
const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
7408
|
+
const isImportedSelectAtom = (callExpression) => {
|
|
7409
|
+
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
7410
|
+
const localName = callExpression.callee.name;
|
|
7411
|
+
for (const source of JOTAI_SELECT_ATOM_SOURCES) {
|
|
7412
|
+
if (!isImportedFromModule(callExpression, localName, source)) continue;
|
|
7413
|
+
if (getImportedNameFromModule(callExpression, localName, source) === "selectAtom") return true;
|
|
7414
|
+
}
|
|
7415
|
+
return false;
|
|
7416
|
+
};
|
|
7417
|
+
const isCallbackOfMemoizingHook = (functionNode) => {
|
|
7418
|
+
const callParent = functionNode.parent;
|
|
7419
|
+
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
7420
|
+
if (!isNodeOfType(callParent.callee, "Identifier")) return false;
|
|
7421
|
+
if (!MEMOIZING_HOOK_NAMES.has(callParent.callee.name)) return false;
|
|
7422
|
+
return callParent.arguments?.[0] === functionNode;
|
|
7423
|
+
};
|
|
7424
|
+
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
7425
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
7426
|
+
const declaredName = functionNode.id.name;
|
|
7427
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
|
|
7428
|
+
}
|
|
7429
|
+
let cursor = functionNode.parent ?? null;
|
|
7430
|
+
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
7431
|
+
if (!cursor) return false;
|
|
7432
|
+
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
7433
|
+
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
7434
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN.test(cursor.id.name);
|
|
7435
|
+
};
|
|
7436
|
+
const jotaiSelectAtomInRenderBody = defineRule({
|
|
7437
|
+
id: "jotai-select-atom-in-render-body",
|
|
7438
|
+
severity: "error",
|
|
7439
|
+
recommendation: "Lift `selectAtom(base, fn)` to module scope, or wrap it: `const atom = useMemo(() => selectAtom(base, fn), [deps])`. Calling it in render rebuilds the derived atom every render and infinitely re-subscribes",
|
|
7440
|
+
create: (context) => ({ CallExpression(node) {
|
|
7441
|
+
if (!isImportedSelectAtom(node)) return;
|
|
7442
|
+
let cursor = node.parent ?? null;
|
|
7443
|
+
let nearestFunctionLike = null;
|
|
7444
|
+
while (cursor) {
|
|
7445
|
+
if (isFunctionLikeNode(cursor)) {
|
|
7446
|
+
nearestFunctionLike = cursor;
|
|
7447
|
+
break;
|
|
7448
|
+
}
|
|
7449
|
+
cursor = cursor.parent ?? null;
|
|
7450
|
+
}
|
|
7451
|
+
if (!nearestFunctionLike) return;
|
|
7452
|
+
if (isCallbackOfMemoizingHook(nearestFunctionLike)) return;
|
|
7453
|
+
let outerCursor = nearestFunctionLike;
|
|
7454
|
+
while (outerCursor) {
|
|
7455
|
+
if (isFunctionLikeNode(outerCursor) && containingFunctionIsComponentOrHook(outerCursor)) {
|
|
7456
|
+
context.report({
|
|
7457
|
+
node,
|
|
7458
|
+
message: "`selectAtom(...)` called in a component / hook body without `useMemo` — every render builds a new derived atom and `useAtomValue` re-subscribes forever. Lift it to module scope or wrap with `useMemo(() => selectAtom(...), [deps])`"
|
|
7459
|
+
});
|
|
7460
|
+
return;
|
|
7461
|
+
}
|
|
7462
|
+
outerCursor = outerCursor.parent ?? null;
|
|
7463
|
+
}
|
|
7464
|
+
} })
|
|
7465
|
+
});
|
|
7466
|
+
//#endregion
|
|
7467
|
+
//#region src/plugin/rules/jotai/jotai-tq-use-raw-query-atom.ts
|
|
7468
|
+
const QUERY_ATOM_FACTORY_IMPORTED_NAMES = new Set([
|
|
7469
|
+
"atomWithQuery",
|
|
7470
|
+
"atomWithSuspenseQuery",
|
|
7471
|
+
"atomWithInfiniteQuery",
|
|
7472
|
+
"atomWithSuspenseInfiniteQuery"
|
|
7473
|
+
]);
|
|
7474
|
+
const SUBSCRIBING_HOOK_NAMES = new Set(["useAtomValue", "useAtom"]);
|
|
7475
|
+
const QUERY_ATOM_NAMING_CONVENTION = /(SuspenseInfiniteQuery|SuspenseQuery|InfiniteQuery|Query)Atom$/;
|
|
7476
|
+
const jotaiTqUseRawQueryAtom = defineRule({
|
|
7477
|
+
id: "jotai-tq-use-raw-query-atom",
|
|
7478
|
+
severity: "warn",
|
|
7479
|
+
recommendation: "Derive the field you read: `const dataAtom = atom((get) => get(queryAtom).data)`. Subscribing directly to a jotai-tanstack-query atom re-renders on every observer notify (refetches, focus events, no-op cache hits)",
|
|
7480
|
+
create: (context) => {
|
|
7481
|
+
const queryAtomFactoryLocalNames = /* @__PURE__ */ new Set();
|
|
7482
|
+
const queryAtomBindingNames = /* @__PURE__ */ new Set();
|
|
7483
|
+
return {
|
|
7484
|
+
ImportDeclaration(node) {
|
|
7485
|
+
const source = node.source?.value;
|
|
7486
|
+
for (const specifier of node.specifiers ?? []) {
|
|
7487
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
7488
|
+
if (!isNodeOfType(specifier.local, "Identifier")) continue;
|
|
7489
|
+
const localName = specifier.local.name;
|
|
7490
|
+
if (source === "jotai-tanstack-query") {
|
|
7491
|
+
const importedName = getImportedName$1(specifier);
|
|
7492
|
+
if (importedName && QUERY_ATOM_FACTORY_IMPORTED_NAMES.has(importedName)) queryAtomFactoryLocalNames.add(localName);
|
|
7493
|
+
continue;
|
|
7494
|
+
}
|
|
7495
|
+
if (typeof source !== "string") continue;
|
|
7496
|
+
if (source.startsWith("jotai") || source === "react" || source.startsWith("react/")) continue;
|
|
7497
|
+
if (QUERY_ATOM_NAMING_CONVENTION.test(localName)) queryAtomBindingNames.add(localName);
|
|
7498
|
+
}
|
|
7499
|
+
},
|
|
7500
|
+
VariableDeclarator(node) {
|
|
7501
|
+
if (queryAtomFactoryLocalNames.size === 0) return;
|
|
7502
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
7503
|
+
const initializer = node.init;
|
|
7504
|
+
if (!isNodeOfType(initializer, "CallExpression")) return;
|
|
7505
|
+
if (!isNodeOfType(initializer.callee, "Identifier")) return;
|
|
7506
|
+
if (!queryAtomFactoryLocalNames.has(initializer.callee.name)) return;
|
|
7507
|
+
queryAtomBindingNames.add(node.id.name);
|
|
7508
|
+
},
|
|
7509
|
+
CallExpression(node) {
|
|
7510
|
+
if (queryAtomBindingNames.size === 0) return;
|
|
7511
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
7512
|
+
if (!SUBSCRIBING_HOOK_NAMES.has(node.callee.name)) return;
|
|
7513
|
+
const args = node.arguments ?? [];
|
|
7514
|
+
if (args.length === 0) return;
|
|
7515
|
+
const firstArgument = args[0];
|
|
7516
|
+
if (!isNodeOfType(firstArgument, "Identifier")) return;
|
|
7517
|
+
if (!queryAtomBindingNames.has(firstArgument.name)) return;
|
|
7518
|
+
context.report({
|
|
7519
|
+
node,
|
|
7520
|
+
message: `\`${node.callee.name}(${firstArgument.name})\` subscribes directly to a jotai-tanstack-query atom — every observer notify (refetch, focus, no-op cache hit) re-renders consumers. Derive the field first: \`const dataAtom = atom((get) => get(${firstArgument.name}).data)\``
|
|
7521
|
+
});
|
|
7522
|
+
}
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
7525
|
+
});
|
|
7526
|
+
//#endregion
|
|
7527
|
+
//#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
|
|
7528
|
+
const isAsyncFunctionLike$1 = (node) => {
|
|
7529
|
+
if (!node) return false;
|
|
7530
|
+
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
7531
|
+
return node.async === true;
|
|
7532
|
+
};
|
|
7533
|
+
const classifyFirstParameter = (fn) => {
|
|
7534
|
+
const parameters = fn.params ?? [];
|
|
7535
|
+
if (parameters.length === 0) return null;
|
|
7536
|
+
const first = parameters[0];
|
|
7537
|
+
if (isNodeOfType(first, "Identifier")) return {
|
|
7538
|
+
kind: "identifier",
|
|
7539
|
+
name: first.name
|
|
7540
|
+
};
|
|
7541
|
+
if (isNodeOfType(first, "ArrayPattern") || isNodeOfType(first, "ObjectPattern")) return { kind: "destructured" };
|
|
7542
|
+
if (isNodeOfType(first, "AssignmentPattern")) {
|
|
7543
|
+
if (isNodeOfType(first.left, "Identifier")) return {
|
|
7544
|
+
kind: "identifier",
|
|
7545
|
+
name: first.left.name
|
|
7546
|
+
};
|
|
7547
|
+
if (isNodeOfType(first.left, "ArrayPattern") || isNodeOfType(first.left, "ObjectPattern")) return { kind: "destructured" };
|
|
7548
|
+
}
|
|
7549
|
+
return null;
|
|
7550
|
+
};
|
|
7551
|
+
const isReduceCallee = (callee) => {
|
|
7552
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
7553
|
+
if (!callee.computed) {
|
|
7554
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
7555
|
+
if (callee.property.name !== "reduce" && callee.property.name !== "reduceRight") return null;
|
|
7556
|
+
return { methodName: callee.property.name };
|
|
7557
|
+
}
|
|
7558
|
+
if (isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string") {
|
|
7559
|
+
const propertyName = callee.property.value;
|
|
7560
|
+
if (propertyName !== "reduce" && propertyName !== "reduceRight") return null;
|
|
7561
|
+
return { methodName: propertyName };
|
|
7562
|
+
}
|
|
7563
|
+
return null;
|
|
7564
|
+
};
|
|
7565
|
+
const bodyAwaitsAccumulator = (fn, accumulatorName) => {
|
|
7566
|
+
const body = fn.body;
|
|
7567
|
+
if (!body) return false;
|
|
7568
|
+
let awaitsAccumulator = false;
|
|
7569
|
+
walkAst(body, (child) => {
|
|
7570
|
+
if (awaitsAccumulator) return false;
|
|
7571
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) {
|
|
7572
|
+
if (child !== fn) return false;
|
|
7573
|
+
}
|
|
7574
|
+
if (!isNodeOfType(child, "AwaitExpression")) return;
|
|
7575
|
+
if (!child.argument) return;
|
|
7576
|
+
const awaitArgument = stripParenExpression(child.argument);
|
|
7577
|
+
if (isNodeOfType(awaitArgument, "Identifier") && awaitArgument.name === accumulatorName) {
|
|
7578
|
+
awaitsAccumulator = true;
|
|
7579
|
+
return false;
|
|
7580
|
+
}
|
|
7581
|
+
});
|
|
7582
|
+
return awaitsAccumulator;
|
|
7583
|
+
};
|
|
7584
|
+
const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
7585
|
+
id: "js-async-reduce-without-awaited-acc",
|
|
7586
|
+
severity: "warn",
|
|
7587
|
+
recommendation: "Await the accumulator first: `const acc = await previous; ...; return acc;`. Use `Promise.resolve(initial)` as the seed so iteration 1's accumulator is also a Promise",
|
|
7588
|
+
create: (context) => ({ CallExpression(node) {
|
|
7589
|
+
const reduceMatch = isReduceCallee(node.callee);
|
|
7590
|
+
if (!reduceMatch) return;
|
|
7591
|
+
const args = node.arguments ?? [];
|
|
7592
|
+
if (args.length === 0) return;
|
|
7593
|
+
const reducerCandidate = stripParenExpression(args[0]);
|
|
7594
|
+
if (!isAsyncFunctionLike$1(reducerCandidate)) return;
|
|
7595
|
+
const reducer = reducerCandidate;
|
|
7596
|
+
if (!containsDirectAwait(reducer.body)) return;
|
|
7597
|
+
const firstParameter = classifyFirstParameter(reducer);
|
|
7598
|
+
if (!firstParameter) return;
|
|
7599
|
+
if (firstParameter.kind === "destructured") {
|
|
7600
|
+
context.report({
|
|
7601
|
+
node: reducer,
|
|
7602
|
+
message: `Async \`.${reduceMatch.methodName}\` reducer destructures its accumulator — destructuring runs against the previous Promise (iteration 2+), produces undefined slots, and silently drops every iteration's work. Use \`async (previous, item) => { const [...] = await previous; ...; return [...]; }\` and seed with \`Promise.resolve([...])\``
|
|
7603
|
+
});
|
|
7604
|
+
return;
|
|
7605
|
+
}
|
|
7606
|
+
if (bodyAwaitsAccumulator(reducer, firstParameter.name)) return;
|
|
7607
|
+
const previousParamName = [
|
|
7608
|
+
"previous",
|
|
7609
|
+
"prev",
|
|
7610
|
+
"priorResult"
|
|
7611
|
+
].find((candidate) => candidate !== firstParameter.name) ?? `${firstParameter.name}Prev`;
|
|
7612
|
+
context.report({
|
|
7613
|
+
node: reducer,
|
|
7614
|
+
message: `Async \`.${reduceMatch.methodName}\` reducer never awaits its accumulator "${firstParameter.name}" — every iteration sees a Promise and the final result silently drops every iteration's work. Either reassign at the top (\`${firstParameter.name} = await ${firstParameter.name};\`) or restructure as \`async (${previousParamName}, item) => { const ${firstParameter.name} = await ${previousParamName}; ...; return ${firstParameter.name}; }\`, and seed with \`Promise.resolve(...)\``
|
|
7615
|
+
});
|
|
7616
|
+
} })
|
|
7617
|
+
});
|
|
7618
|
+
//#endregion
|
|
6723
7619
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
6724
7620
|
const ITERATOR_METHOD_NAMES$1 = new Set([
|
|
6725
7621
|
"forEach",
|
|
@@ -6743,7 +7639,7 @@ const isInsideLoopContext = (node) => {
|
|
|
6743
7639
|
let current = node.parent;
|
|
6744
7640
|
while (current) {
|
|
6745
7641
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
6746
|
-
if (isFunctionLike(current)) {
|
|
7642
|
+
if (isFunctionLike$1(current)) {
|
|
6747
7643
|
if (isIteratorCallback(current)) return true;
|
|
6748
7644
|
return false;
|
|
6749
7645
|
}
|
|
@@ -7097,7 +7993,7 @@ const jsHoistIntl = defineRule({
|
|
|
7097
7993
|
let cursor = node.parent ?? null;
|
|
7098
7994
|
let inFunctionBody = false;
|
|
7099
7995
|
while (cursor) {
|
|
7100
|
-
if (isFunctionLike(cursor)) {
|
|
7996
|
+
if (isFunctionLike$1(cursor)) {
|
|
7101
7997
|
inFunctionBody = true;
|
|
7102
7998
|
const fnParent = cursor.parent;
|
|
7103
7999
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -8440,33 +9336,8 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
8440
9336
|
return best;
|
|
8441
9337
|
};
|
|
8442
9338
|
//#endregion
|
|
8443
|
-
//#region src/plugin/utils/strip-paren-expression.ts
|
|
8444
|
-
const TS_WRAPPER_TYPES = new Set([
|
|
8445
|
-
"ParenthesizedExpression",
|
|
8446
|
-
"TSAsExpression",
|
|
8447
|
-
"TSSatisfiesExpression",
|
|
8448
|
-
"TSTypeAssertion",
|
|
8449
|
-
"TSNonNullExpression",
|
|
8450
|
-
"TSInstantiationExpression"
|
|
8451
|
-
]);
|
|
8452
|
-
const stripParenExpression = (node) => {
|
|
8453
|
-
let current = node;
|
|
8454
|
-
while (true) {
|
|
8455
|
-
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
8456
|
-
current = current.expression;
|
|
8457
|
-
continue;
|
|
8458
|
-
}
|
|
8459
|
-
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
8460
|
-
current = current.expression;
|
|
8461
|
-
continue;
|
|
8462
|
-
}
|
|
8463
|
-
break;
|
|
8464
|
-
}
|
|
8465
|
-
return current;
|
|
8466
|
-
};
|
|
8467
|
-
//#endregion
|
|
8468
9339
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
8469
|
-
const buildMessage$
|
|
9340
|
+
const buildMessage$17 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
8470
9341
|
const DEFAULT_MAX_DEPTH = 14;
|
|
8471
9342
|
const resolveSettings$30 = (settings) => {
|
|
8472
9343
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -8533,7 +9404,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8533
9404
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
8534
9405
|
if (total > max) context.report({
|
|
8535
9406
|
node,
|
|
8536
|
-
message: buildMessage$
|
|
9407
|
+
message: buildMessage$17(total, max)
|
|
8537
9408
|
});
|
|
8538
9409
|
};
|
|
8539
9410
|
return {
|
|
@@ -8548,7 +9419,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8548
9419
|
});
|
|
8549
9420
|
//#endregion
|
|
8550
9421
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
8551
|
-
const MESSAGE$
|
|
9422
|
+
const MESSAGE$37 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
|
|
8552
9423
|
const LITERAL_TEXT_TAGS = new Set([
|
|
8553
9424
|
"code",
|
|
8554
9425
|
"pre",
|
|
@@ -8583,7 +9454,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
8583
9454
|
if (isInsideLiteralTextTag(node)) return;
|
|
8584
9455
|
context.report({
|
|
8585
9456
|
node,
|
|
8586
|
-
message: MESSAGE$
|
|
9457
|
+
message: MESSAGE$37
|
|
8587
9458
|
});
|
|
8588
9459
|
} })
|
|
8589
9460
|
});
|
|
@@ -8605,7 +9476,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
8605
9476
|
};
|
|
8606
9477
|
//#endregion
|
|
8607
9478
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
8608
|
-
const MESSAGE$
|
|
9479
|
+
const MESSAGE$36 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
|
|
8609
9480
|
const isConstructedValue = (expression) => {
|
|
8610
9481
|
const stripped = stripParenExpression(expression);
|
|
8611
9482
|
if (isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "NewExpression") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment")) return true;
|
|
@@ -8641,7 +9512,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
8641
9512
|
if (!isConstructedValue(innerExpression)) continue;
|
|
8642
9513
|
context.report({
|
|
8643
9514
|
node: attribute,
|
|
8644
|
-
message: MESSAGE$
|
|
9515
|
+
message: MESSAGE$36
|
|
8645
9516
|
});
|
|
8646
9517
|
}
|
|
8647
9518
|
} };
|
|
@@ -8724,7 +9595,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
8724
9595
|
};
|
|
8725
9596
|
//#endregion
|
|
8726
9597
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
8727
|
-
const MESSAGE$
|
|
9598
|
+
const MESSAGE$35 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
|
|
8728
9599
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
8729
9600
|
"icon",
|
|
8730
9601
|
"Icon",
|
|
@@ -8992,7 +9863,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
8992
9863
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
8993
9864
|
context.report({
|
|
8994
9865
|
node,
|
|
8995
|
-
message: MESSAGE$
|
|
9866
|
+
message: MESSAGE$35
|
|
8996
9867
|
});
|
|
8997
9868
|
}
|
|
8998
9869
|
};
|
|
@@ -9280,7 +10151,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
9280
10151
|
];
|
|
9281
10152
|
//#endregion
|
|
9282
10153
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
9283
|
-
const MESSAGE$
|
|
10154
|
+
const MESSAGE$34 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
|
|
9284
10155
|
const isDataArrayPropName = (propName) => {
|
|
9285
10156
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
9286
10157
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -9363,7 +10234,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
9363
10234
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
9364
10235
|
context.report({
|
|
9365
10236
|
node,
|
|
9366
|
-
message: MESSAGE$
|
|
10237
|
+
message: MESSAGE$34
|
|
9367
10238
|
});
|
|
9368
10239
|
}
|
|
9369
10240
|
};
|
|
@@ -9621,7 +10492,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
9621
10492
|
]);
|
|
9622
10493
|
//#endregion
|
|
9623
10494
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
9624
|
-
const MESSAGE$
|
|
10495
|
+
const MESSAGE$33 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
|
|
9625
10496
|
const isAccessorPredicateName = (propName) => {
|
|
9626
10497
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
9627
10498
|
if (propName.length <= prefix.length) continue;
|
|
@@ -9826,7 +10697,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
9826
10697
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
9827
10698
|
context.report({
|
|
9828
10699
|
node,
|
|
9829
|
-
message: MESSAGE$
|
|
10700
|
+
message: MESSAGE$33
|
|
9830
10701
|
});
|
|
9831
10702
|
}
|
|
9832
10703
|
};
|
|
@@ -10046,7 +10917,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
10046
10917
|
];
|
|
10047
10918
|
//#endregion
|
|
10048
10919
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
10049
|
-
const MESSAGE$
|
|
10920
|
+
const MESSAGE$32 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
|
|
10050
10921
|
const isConfigObjectPropName = (propName) => {
|
|
10051
10922
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
10052
10923
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10133,7 +11004,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10133
11004
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
10134
11005
|
context.report({
|
|
10135
11006
|
node,
|
|
10136
|
-
message: MESSAGE$
|
|
11007
|
+
message: MESSAGE$32
|
|
10137
11008
|
});
|
|
10138
11009
|
}
|
|
10139
11010
|
};
|
|
@@ -10141,7 +11012,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10141
11012
|
});
|
|
10142
11013
|
//#endregion
|
|
10143
11014
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
10144
|
-
const MESSAGE$
|
|
11015
|
+
const MESSAGE$31 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
|
|
10145
11016
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
10146
11017
|
const resolveSettings$29 = (settings) => {
|
|
10147
11018
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -10181,7 +11052,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
10181
11052
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
10182
11053
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
10183
11054
|
node: attribute,
|
|
10184
|
-
message: MESSAGE$
|
|
11055
|
+
message: MESSAGE$31
|
|
10185
11056
|
});
|
|
10186
11057
|
}
|
|
10187
11058
|
} };
|
|
@@ -10469,7 +11340,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
10469
11340
|
});
|
|
10470
11341
|
//#endregion
|
|
10471
11342
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
10472
|
-
const buildMessage$
|
|
11343
|
+
const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
10473
11344
|
const KNOWN_GLOBALS = new Set([
|
|
10474
11345
|
"globalThis",
|
|
10475
11346
|
"window",
|
|
@@ -10504,7 +11375,7 @@ const jsxNoUndef = defineRule({
|
|
|
10504
11375
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
10505
11376
|
context.report({
|
|
10506
11377
|
node: node.name,
|
|
10507
|
-
message: buildMessage$
|
|
11378
|
+
message: buildMessage$16(rootIdentifier)
|
|
10508
11379
|
});
|
|
10509
11380
|
} })
|
|
10510
11381
|
});
|
|
@@ -10603,7 +11474,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
10603
11474
|
});
|
|
10604
11475
|
//#endregion
|
|
10605
11476
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
10606
|
-
const buildMessage$
|
|
11477
|
+
const buildMessage$15 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
|
|
10607
11478
|
const resolveSettings$26 = (settings) => {
|
|
10608
11479
|
const reactDoctor = settings?.["react-doctor"];
|
|
10609
11480
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -10719,7 +11590,7 @@ const jsxPascalCase = defineRule({
|
|
|
10719
11590
|
if (!isPascal && !isAllCaps) {
|
|
10720
11591
|
context.report({
|
|
10721
11592
|
node,
|
|
10722
|
-
message: buildMessage$
|
|
11593
|
+
message: buildMessage$15(segment, settings.allowAllCaps)
|
|
10723
11594
|
});
|
|
10724
11595
|
return;
|
|
10725
11596
|
}
|
|
@@ -10771,7 +11642,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
10771
11642
|
});
|
|
10772
11643
|
//#endregion
|
|
10773
11644
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
10774
|
-
const MESSAGE$
|
|
11645
|
+
const MESSAGE$30 = "JSX prop spreading is forbidden — list each prop explicitly.";
|
|
10775
11646
|
const resolveSettings$25 = (settings) => {
|
|
10776
11647
|
const reactDoctor = settings?.["react-doctor"];
|
|
10777
11648
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -10811,7 +11682,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
10811
11682
|
}
|
|
10812
11683
|
context.report({
|
|
10813
11684
|
node: attribute,
|
|
10814
|
-
message: MESSAGE$
|
|
11685
|
+
message: MESSAGE$30
|
|
10815
11686
|
});
|
|
10816
11687
|
}
|
|
10817
11688
|
} };
|
|
@@ -10966,7 +11837,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
10966
11837
|
});
|
|
10967
11838
|
//#endregion
|
|
10968
11839
|
//#region src/plugin/rules/a11y/lang.ts
|
|
10969
|
-
const MESSAGE$
|
|
11840
|
+
const MESSAGE$29 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
|
|
10970
11841
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
10971
11842
|
"aa",
|
|
10972
11843
|
"ab",
|
|
@@ -11177,7 +12048,7 @@ const lang = defineRule({
|
|
|
11177
12048
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
11178
12049
|
context.report({
|
|
11179
12050
|
node: langAttr,
|
|
11180
|
-
message: MESSAGE$
|
|
12051
|
+
message: MESSAGE$29
|
|
11181
12052
|
});
|
|
11182
12053
|
return;
|
|
11183
12054
|
}
|
|
@@ -11186,13 +12057,13 @@ const lang = defineRule({
|
|
|
11186
12057
|
if (value === null) return;
|
|
11187
12058
|
if (!isValidLangTag(value)) context.report({
|
|
11188
12059
|
node: langAttr,
|
|
11189
|
-
message: MESSAGE$
|
|
12060
|
+
message: MESSAGE$29
|
|
11190
12061
|
});
|
|
11191
12062
|
} })
|
|
11192
12063
|
});
|
|
11193
12064
|
//#endregion
|
|
11194
12065
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
11195
|
-
const MESSAGE$
|
|
12066
|
+
const MESSAGE$28 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
|
|
11196
12067
|
const DEFAULT_AUDIO = ["audio"];
|
|
11197
12068
|
const DEFAULT_VIDEO = ["video"];
|
|
11198
12069
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -11232,7 +12103,7 @@ const mediaHasCaption = defineRule({
|
|
|
11232
12103
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
11233
12104
|
context.report({
|
|
11234
12105
|
node: node.name,
|
|
11235
|
-
message: MESSAGE$
|
|
12106
|
+
message: MESSAGE$28
|
|
11236
12107
|
});
|
|
11237
12108
|
return;
|
|
11238
12109
|
}
|
|
@@ -11249,7 +12120,7 @@ const mediaHasCaption = defineRule({
|
|
|
11249
12120
|
return kindValue.value.toLowerCase() === "captions";
|
|
11250
12121
|
})) context.report({
|
|
11251
12122
|
node: node.name,
|
|
11252
|
-
message: MESSAGE$
|
|
12123
|
+
message: MESSAGE$28
|
|
11253
12124
|
});
|
|
11254
12125
|
} };
|
|
11255
12126
|
}
|
|
@@ -11940,7 +12811,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
11940
12811
|
};
|
|
11941
12812
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
11942
12813
|
if (remainingDepth <= 0) return [];
|
|
11943
|
-
if (isFunctionLike(expression)) return expression.body ? [expression.body] : [];
|
|
12814
|
+
if (isFunctionLike$1(expression)) return expression.body ? [expression.body] : [];
|
|
11944
12815
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
11945
12816
|
for (const callArgument of expression.arguments ?? []) {
|
|
11946
12817
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -12020,14 +12891,6 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12020
12891
|
}
|
|
12021
12892
|
});
|
|
12022
12893
|
//#endregion
|
|
12023
|
-
//#region src/plugin/utils/get-imported-name.ts
|
|
12024
|
-
const getImportedName = (importSpecifier) => {
|
|
12025
|
-
if (!isNodeOfType(importSpecifier, "ImportSpecifier")) return void 0;
|
|
12026
|
-
const imported = importSpecifier.imported;
|
|
12027
|
-
if (isNodeOfType(imported, "Identifier")) return imported.name;
|
|
12028
|
-
if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
|
|
12029
|
-
};
|
|
12030
|
-
//#endregion
|
|
12031
12894
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
12032
12895
|
const fileMentionsSuspense = (programNode) => {
|
|
12033
12896
|
let didSee = false;
|
|
@@ -12038,7 +12901,7 @@ const fileMentionsSuspense = (programNode) => {
|
|
|
12038
12901
|
return false;
|
|
12039
12902
|
}
|
|
12040
12903
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
12041
|
-
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense")) {
|
|
12904
|
+
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
|
|
12042
12905
|
didSee = true;
|
|
12043
12906
|
return false;
|
|
12044
12907
|
}
|
|
@@ -12071,7 +12934,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
12071
12934
|
});
|
|
12072
12935
|
//#endregion
|
|
12073
12936
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
12074
|
-
const MESSAGE$
|
|
12937
|
+
const MESSAGE$27 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
|
|
12075
12938
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
12076
12939
|
const noAccessKey = defineRule({
|
|
12077
12940
|
id: "no-access-key",
|
|
@@ -12087,7 +12950,7 @@ const noAccessKey = defineRule({
|
|
|
12087
12950
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
12088
12951
|
context.report({
|
|
12089
12952
|
node: accessKey,
|
|
12090
|
-
message: MESSAGE$
|
|
12953
|
+
message: MESSAGE$27
|
|
12091
12954
|
});
|
|
12092
12955
|
return;
|
|
12093
12956
|
}
|
|
@@ -12097,7 +12960,7 @@ const noAccessKey = defineRule({
|
|
|
12097
12960
|
if (isUndefinedIdentifier(expression)) return;
|
|
12098
12961
|
context.report({
|
|
12099
12962
|
node: accessKey,
|
|
12100
|
-
message: MESSAGE$
|
|
12963
|
+
message: MESSAGE$27
|
|
12101
12964
|
});
|
|
12102
12965
|
}
|
|
12103
12966
|
} })
|
|
@@ -12406,7 +13269,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
12406
13269
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
12407
13270
|
if (isNodeOfType(fn, "Identifier")) {
|
|
12408
13271
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
12409
|
-
if (definitionNode && isFunctionLike(definitionNode)) return definitionNode;
|
|
13272
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return definitionNode;
|
|
12410
13273
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12411
13274
|
const initializer = definitionNode.init;
|
|
12412
13275
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -12499,14 +13362,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
12499
13362
|
return node ?? null;
|
|
12500
13363
|
};
|
|
12501
13364
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
12502
|
-
if (isFunctionLike(node)) return true;
|
|
13365
|
+
if (isFunctionLike$1(node)) return true;
|
|
12503
13366
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
12504
13367
|
if (isNodeOfType(node, "Identifier")) {
|
|
12505
13368
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
12506
|
-
if (definitionNode && isFunctionLike(definitionNode)) return true;
|
|
13369
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return true;
|
|
12507
13370
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12508
13371
|
const initializer = definitionNode.init;
|
|
12509
|
-
return isFunctionLike(initializer);
|
|
13372
|
+
return isFunctionLike$1(initializer);
|
|
12510
13373
|
}
|
|
12511
13374
|
}
|
|
12512
13375
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -12516,7 +13379,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12516
13379
|
if (visited.has(node)) return false;
|
|
12517
13380
|
visited.add(node);
|
|
12518
13381
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
12519
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike(node)) return false;
|
|
13382
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
|
|
12520
13383
|
const record = node;
|
|
12521
13384
|
for (const [key, value] of Object.entries(record)) {
|
|
12522
13385
|
if (key === "parent") continue;
|
|
@@ -12528,7 +13391,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12528
13391
|
};
|
|
12529
13392
|
const hasCleanup = (analysis, node) => {
|
|
12530
13393
|
const fn = getEffectFn(analysis, node);
|
|
12531
|
-
if (!isFunctionLike(fn)) return false;
|
|
13394
|
+
if (!isFunctionLike$1(fn)) return false;
|
|
12532
13395
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
12533
13396
|
return hasCleanupReturn(analysis, fn.body);
|
|
12534
13397
|
};
|
|
@@ -12570,7 +13433,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
12570
13433
|
});
|
|
12571
13434
|
//#endregion
|
|
12572
13435
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
12573
|
-
const MESSAGE$
|
|
13436
|
+
const MESSAGE$26 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
|
|
12574
13437
|
const noAriaHiddenOnFocusable = defineRule({
|
|
12575
13438
|
id: "no-aria-hidden-on-focusable",
|
|
12576
13439
|
tags: ["react-jsx-only"],
|
|
@@ -12596,7 +13459,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
12596
13459
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
12597
13460
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
12598
13461
|
node: ariaHidden,
|
|
12599
|
-
message: MESSAGE$
|
|
13462
|
+
message: MESSAGE$26
|
|
12600
13463
|
});
|
|
12601
13464
|
} })
|
|
12602
13465
|
});
|
|
@@ -12876,7 +13739,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
12876
13739
|
let current = node;
|
|
12877
13740
|
while (current.parent) {
|
|
12878
13741
|
const parent = current.parent;
|
|
12879
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13742
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12880
13743
|
const callee = parent.callee;
|
|
12881
13744
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
|
|
12882
13745
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -12895,7 +13758,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
12895
13758
|
let current = node;
|
|
12896
13759
|
while (current.parent) {
|
|
12897
13760
|
const parent = current.parent;
|
|
12898
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13761
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12899
13762
|
const callee = parent.callee;
|
|
12900
13763
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
12901
13764
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -12963,7 +13826,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
12963
13826
|
});
|
|
12964
13827
|
//#endregion
|
|
12965
13828
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
12966
|
-
const MESSAGE$
|
|
13829
|
+
const MESSAGE$25 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
|
|
12967
13830
|
const SECOND_INDEX_METHODS = new Set([
|
|
12968
13831
|
"every",
|
|
12969
13832
|
"filter",
|
|
@@ -13165,7 +14028,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13165
14028
|
}
|
|
13166
14029
|
context.report({
|
|
13167
14030
|
node: keyAttribute,
|
|
13168
|
-
message: MESSAGE$
|
|
14031
|
+
message: MESSAGE$25
|
|
13169
14032
|
});
|
|
13170
14033
|
},
|
|
13171
14034
|
CallExpression(node) {
|
|
@@ -13185,7 +14048,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13185
14048
|
if (propName !== "key") continue;
|
|
13186
14049
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
13187
14050
|
node: property,
|
|
13188
|
-
message: MESSAGE$
|
|
14051
|
+
message: MESSAGE$25
|
|
13189
14052
|
});
|
|
13190
14053
|
}
|
|
13191
14054
|
}
|
|
@@ -13193,7 +14056,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13193
14056
|
});
|
|
13194
14057
|
//#endregion
|
|
13195
14058
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
13196
|
-
const MESSAGE$
|
|
14059
|
+
const MESSAGE$24 = "`autoFocus` should not be used — it disrupts users who expect the page focus to remain at the top of the document on load.";
|
|
13197
14060
|
const resolveSettings$21 = (settings) => {
|
|
13198
14061
|
const reactDoctor = settings?.["react-doctor"];
|
|
13199
14062
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -13248,7 +14111,7 @@ const noAutofocus = defineRule({
|
|
|
13248
14111
|
}
|
|
13249
14112
|
context.report({
|
|
13250
14113
|
node: autoFocusAttribute,
|
|
13251
|
-
message: MESSAGE$
|
|
14114
|
+
message: MESSAGE$24
|
|
13252
14115
|
});
|
|
13253
14116
|
} };
|
|
13254
14117
|
}
|
|
@@ -13739,7 +14602,7 @@ const noChainStateUpdates = defineRule({
|
|
|
13739
14602
|
});
|
|
13740
14603
|
//#endregion
|
|
13741
14604
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
13742
|
-
const MESSAGE$
|
|
14605
|
+
const MESSAGE$23 = "Avoid passing children using a `children` prop — nest them between the JSX tags or pass them as additional `React.createElement` arguments instead.";
|
|
13743
14606
|
const noChildrenProp = defineRule({
|
|
13744
14607
|
id: "no-children-prop",
|
|
13745
14608
|
severity: "warn",
|
|
@@ -13750,7 +14613,7 @@ const noChildrenProp = defineRule({
|
|
|
13750
14613
|
if (node.name.name !== "children") return;
|
|
13751
14614
|
context.report({
|
|
13752
14615
|
node: node.name,
|
|
13753
|
-
message: MESSAGE$
|
|
14616
|
+
message: MESSAGE$23
|
|
13754
14617
|
});
|
|
13755
14618
|
},
|
|
13756
14619
|
CallExpression(node) {
|
|
@@ -13763,90 +14626,15 @@ const noChildrenProp = defineRule({
|
|
|
13763
14626
|
const propertyKey = property.key;
|
|
13764
14627
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
13765
14628
|
node: propertyKey,
|
|
13766
|
-
message: MESSAGE$
|
|
14629
|
+
message: MESSAGE$23
|
|
13767
14630
|
});
|
|
13768
14631
|
}
|
|
13769
14632
|
}
|
|
13770
14633
|
})
|
|
13771
14634
|
});
|
|
13772
14635
|
//#endregion
|
|
13773
|
-
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
13774
|
-
const collectFromProgram = (programRoot) => {
|
|
13775
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
13776
|
-
const visit = (node) => {
|
|
13777
|
-
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
13778
|
-
const source = node.source.value;
|
|
13779
|
-
if (typeof source !== "string") return;
|
|
13780
|
-
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
13781
|
-
if (!("local" in specifier) || !specifier.local) continue;
|
|
13782
|
-
const local = specifier.local;
|
|
13783
|
-
if (typeof local.name !== "string") continue;
|
|
13784
|
-
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
13785
|
-
source,
|
|
13786
|
-
imported: null,
|
|
13787
|
-
isDefault: true,
|
|
13788
|
-
isNamespace: false
|
|
13789
|
-
});
|
|
13790
|
-
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
13791
|
-
source,
|
|
13792
|
-
imported: null,
|
|
13793
|
-
isDefault: false,
|
|
13794
|
-
isNamespace: true
|
|
13795
|
-
});
|
|
13796
|
-
else if (specifier.type === "ImportSpecifier") {
|
|
13797
|
-
const importedNode = specifier.imported;
|
|
13798
|
-
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
13799
|
-
lookup.set(local.name, {
|
|
13800
|
-
source,
|
|
13801
|
-
imported: importedName,
|
|
13802
|
-
isDefault: false,
|
|
13803
|
-
isNamespace: false
|
|
13804
|
-
});
|
|
13805
|
-
}
|
|
13806
|
-
}
|
|
13807
|
-
return;
|
|
13808
|
-
}
|
|
13809
|
-
const nodeRecord = node;
|
|
13810
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
13811
|
-
if (key === "parent") continue;
|
|
13812
|
-
const child = nodeRecord[key];
|
|
13813
|
-
if (Array.isArray(child)) {
|
|
13814
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
13815
|
-
} else if (isAstNode(child)) visit(child);
|
|
13816
|
-
}
|
|
13817
|
-
};
|
|
13818
|
-
visit(programRoot);
|
|
13819
|
-
return lookup;
|
|
13820
|
-
};
|
|
13821
|
-
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
13822
|
-
const getImportLookup = (node) => {
|
|
13823
|
-
const programRoot = findProgramRoot(node);
|
|
13824
|
-
if (!programRoot) return null;
|
|
13825
|
-
let cached = importLookupCache.get(programRoot);
|
|
13826
|
-
if (!cached) {
|
|
13827
|
-
cached = collectFromProgram(programRoot);
|
|
13828
|
-
importLookupCache.set(programRoot, cached);
|
|
13829
|
-
}
|
|
13830
|
-
return cached;
|
|
13831
|
-
};
|
|
13832
|
-
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
13833
|
-
const lookup = getImportLookup(contextNode);
|
|
13834
|
-
if (!lookup) return false;
|
|
13835
|
-
const info = lookup.get(localIdentifierName);
|
|
13836
|
-
if (!info) return false;
|
|
13837
|
-
return info.source === moduleSource;
|
|
13838
|
-
};
|
|
13839
|
-
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
13840
|
-
const lookup = getImportLookup(contextNode);
|
|
13841
|
-
if (!lookup) return null;
|
|
13842
|
-
const info = lookup.get(localIdentifierName);
|
|
13843
|
-
if (!info) return null;
|
|
13844
|
-
if (info.source !== moduleSource) return null;
|
|
13845
|
-
return info.imported;
|
|
13846
|
-
};
|
|
13847
|
-
//#endregion
|
|
13848
14636
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
13849
|
-
const MESSAGE$
|
|
14637
|
+
const MESSAGE$22 = "`React.cloneElement` is uncommon and leads to fragile components.";
|
|
13850
14638
|
const noCloneElement = defineRule({
|
|
13851
14639
|
id: "no-clone-element",
|
|
13852
14640
|
severity: "warn",
|
|
@@ -13858,7 +14646,7 @@ const noCloneElement = defineRule({
|
|
|
13858
14646
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
13859
14647
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
13860
14648
|
node: callee,
|
|
13861
|
-
message: MESSAGE$
|
|
14649
|
+
message: MESSAGE$22
|
|
13862
14650
|
});
|
|
13863
14651
|
return;
|
|
13864
14652
|
}
|
|
@@ -13871,14 +14659,14 @@ const noCloneElement = defineRule({
|
|
|
13871
14659
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
13872
14660
|
context.report({
|
|
13873
14661
|
node: callee,
|
|
13874
|
-
message: MESSAGE$
|
|
14662
|
+
message: MESSAGE$22
|
|
13875
14663
|
});
|
|
13876
14664
|
}
|
|
13877
14665
|
} })
|
|
13878
14666
|
});
|
|
13879
14667
|
//#endregion
|
|
13880
14668
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
13881
|
-
const MESSAGE$
|
|
14669
|
+
const MESSAGE$21 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
|
|
13882
14670
|
const noDanger = defineRule({
|
|
13883
14671
|
id: "no-danger",
|
|
13884
14672
|
severity: "warn",
|
|
@@ -13889,7 +14677,7 @@ const noDanger = defineRule({
|
|
|
13889
14677
|
if (!propAttribute) return;
|
|
13890
14678
|
context.report({
|
|
13891
14679
|
node: propAttribute.name,
|
|
13892
|
-
message: MESSAGE$
|
|
14680
|
+
message: MESSAGE$21
|
|
13893
14681
|
});
|
|
13894
14682
|
},
|
|
13895
14683
|
CallExpression(node) {
|
|
@@ -13901,7 +14689,7 @@ const noDanger = defineRule({
|
|
|
13901
14689
|
const propertyKey = property.key;
|
|
13902
14690
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
13903
14691
|
node: propertyKey,
|
|
13904
|
-
message: MESSAGE$
|
|
14692
|
+
message: MESSAGE$21
|
|
13905
14693
|
});
|
|
13906
14694
|
}
|
|
13907
14695
|
}
|
|
@@ -13909,7 +14697,7 @@ const noDanger = defineRule({
|
|
|
13909
14697
|
});
|
|
13910
14698
|
//#endregion
|
|
13911
14699
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
13912
|
-
const MESSAGE$
|
|
14700
|
+
const MESSAGE$20 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
|
|
13913
14701
|
const isLineBreak = (child) => {
|
|
13914
14702
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
13915
14703
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -13978,7 +14766,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13978
14766
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
13979
14767
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
13980
14768
|
node: opening,
|
|
13981
|
-
message: MESSAGE$
|
|
14769
|
+
message: MESSAGE$20
|
|
13982
14770
|
});
|
|
13983
14771
|
},
|
|
13984
14772
|
CallExpression(node) {
|
|
@@ -13990,7 +14778,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13990
14778
|
if (!propsShape.hasDangerously) return;
|
|
13991
14779
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
13992
14780
|
node,
|
|
13993
|
-
message: MESSAGE$
|
|
14781
|
+
message: MESSAGE$20
|
|
13994
14782
|
});
|
|
13995
14783
|
}
|
|
13996
14784
|
})
|
|
@@ -14365,7 +15153,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
14365
15153
|
};
|
|
14366
15154
|
const getInlineFunctionNode = (node) => {
|
|
14367
15155
|
if (!node) return null;
|
|
14368
|
-
if (isFunctionLike(node)) return node;
|
|
15156
|
+
if (isFunctionLike$1(node)) return node;
|
|
14369
15157
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
14370
15158
|
for (const argument of node.arguments ?? []) {
|
|
14371
15159
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -14376,7 +15164,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
14376
15164
|
const getNearestComponentFunction = (node) => {
|
|
14377
15165
|
let cursor = node.parent ?? null;
|
|
14378
15166
|
while (cursor) {
|
|
14379
|
-
if (isFunctionLike(cursor)) return cursor;
|
|
15167
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
14380
15168
|
cursor = cursor.parent ?? null;
|
|
14381
15169
|
}
|
|
14382
15170
|
return null;
|
|
@@ -14557,7 +15345,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
14557
15345
|
//#endregion
|
|
14558
15346
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
14559
15347
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
14560
|
-
const MESSAGE$
|
|
15348
|
+
const MESSAGE$19 = "Do not use `this.setState` in `componentDidMount`.";
|
|
14561
15349
|
const resolveSettings$20 = (settings) => {
|
|
14562
15350
|
const reactDoctor = settings?.["react-doctor"];
|
|
14563
15351
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14575,7 +15363,7 @@ const noDidMountSetState = defineRule({
|
|
|
14575
15363
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14576
15364
|
context.report({
|
|
14577
15365
|
node: node.callee,
|
|
14578
|
-
message: MESSAGE$
|
|
15366
|
+
message: MESSAGE$19
|
|
14579
15367
|
});
|
|
14580
15368
|
} };
|
|
14581
15369
|
}
|
|
@@ -14583,7 +15371,7 @@ const noDidMountSetState = defineRule({
|
|
|
14583
15371
|
//#endregion
|
|
14584
15372
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
14585
15373
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
14586
|
-
const MESSAGE$
|
|
15374
|
+
const MESSAGE$18 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
|
|
14587
15375
|
const resolveSettings$19 = (settings) => {
|
|
14588
15376
|
const reactDoctor = settings?.["react-doctor"];
|
|
14589
15377
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14601,7 +15389,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
14601
15389
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14602
15390
|
context.report({
|
|
14603
15391
|
node: node.callee,
|
|
14604
|
-
message: MESSAGE$
|
|
15392
|
+
message: MESSAGE$18
|
|
14605
15393
|
});
|
|
14606
15394
|
} };
|
|
14607
15395
|
}
|
|
@@ -14624,7 +15412,7 @@ const isStateMemberExpression = (node) => {
|
|
|
14624
15412
|
};
|
|
14625
15413
|
//#endregion
|
|
14626
15414
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
14627
|
-
const MESSAGE$
|
|
15415
|
+
const MESSAGE$17 = "Never mutate `this.state` directly.";
|
|
14628
15416
|
const shouldIgnoreMutation = (node) => {
|
|
14629
15417
|
let isConstructor = false;
|
|
14630
15418
|
let isInsideCallExpression = false;
|
|
@@ -14646,7 +15434,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
14646
15434
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
14647
15435
|
context.report({
|
|
14648
15436
|
node: reportNode,
|
|
14649
|
-
message: MESSAGE$
|
|
15437
|
+
message: MESSAGE$17
|
|
14650
15438
|
});
|
|
14651
15439
|
};
|
|
14652
15440
|
const noDirectMutationState = defineRule({
|
|
@@ -14702,7 +15490,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
14702
15490
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
14703
15491
|
if (!node || typeof node !== "object") return;
|
|
14704
15492
|
let nextShadowedStateNames = shadowedStateNames;
|
|
14705
|
-
if (isFunctionLike(node)) {
|
|
15493
|
+
if (isFunctionLike$1(node)) {
|
|
14706
15494
|
const localBindings = collectFunctionLocalBindings(node);
|
|
14707
15495
|
if (localBindings.size > 0) {
|
|
14708
15496
|
const merged = new Set(shadowedStateNames);
|
|
@@ -14809,7 +15597,7 @@ const noDisabledZoom = defineRule({
|
|
|
14809
15597
|
});
|
|
14810
15598
|
//#endregion
|
|
14811
15599
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
14812
|
-
const buildMessage$
|
|
15600
|
+
const buildMessage$14 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
14813
15601
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
14814
15602
|
const resolveSettings$18 = (settings) => {
|
|
14815
15603
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -14829,7 +15617,7 @@ const noDistractingElements = defineRule({
|
|
|
14829
15617
|
const tag = getElementType(node, context.settings);
|
|
14830
15618
|
if (distractingTags.has(tag)) context.report({
|
|
14831
15619
|
node: node.name,
|
|
14832
|
-
message: buildMessage$
|
|
15620
|
+
message: buildMessage$14(tag)
|
|
14833
15621
|
});
|
|
14834
15622
|
} };
|
|
14835
15623
|
}
|
|
@@ -16159,7 +16947,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
16159
16947
|
"ReactDOM",
|
|
16160
16948
|
"ReactDom"
|
|
16161
16949
|
]);
|
|
16162
|
-
const MESSAGE$
|
|
16950
|
+
const MESSAGE$16 = "Unexpected call to `findDOMNode` — removed in React 19.";
|
|
16163
16951
|
const noFindDomNode = defineRule({
|
|
16164
16952
|
id: "no-find-dom-node",
|
|
16165
16953
|
severity: "warn",
|
|
@@ -16169,7 +16957,7 @@ const noFindDomNode = defineRule({
|
|
|
16169
16957
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
16170
16958
|
context.report({
|
|
16171
16959
|
node: callee,
|
|
16172
|
-
message: MESSAGE$
|
|
16960
|
+
message: MESSAGE$16
|
|
16173
16961
|
});
|
|
16174
16962
|
return;
|
|
16175
16963
|
}
|
|
@@ -16180,7 +16968,7 @@ const noFindDomNode = defineRule({
|
|
|
16180
16968
|
if (callee.property.name !== "findDOMNode") return;
|
|
16181
16969
|
context.report({
|
|
16182
16970
|
node: callee.property,
|
|
16183
|
-
message: MESSAGE$
|
|
16971
|
+
message: MESSAGE$16
|
|
16184
16972
|
});
|
|
16185
16973
|
}
|
|
16186
16974
|
} })
|
|
@@ -16197,7 +16985,7 @@ const noFlushSync = defineRule({
|
|
|
16197
16985
|
if (node.source?.value !== "react-dom") return;
|
|
16198
16986
|
for (const specifier of node.specifiers ?? []) {
|
|
16199
16987
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
16200
|
-
if (getImportedName(specifier) === "flushSync") context.report({
|
|
16988
|
+
if (getImportedName$1(specifier) === "flushSync") context.report({
|
|
16201
16989
|
node: specifier,
|
|
16202
16990
|
message: "flushSync from react-dom skips View Transition snapshots and concurrent rendering — prefer startTransition for non-urgent updates"
|
|
16203
16991
|
});
|
|
@@ -16239,6 +17027,64 @@ const noGenericHandlerNames = defineRule({
|
|
|
16239
17027
|
} })
|
|
16240
17028
|
});
|
|
16241
17029
|
//#endregion
|
|
17030
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
17031
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
17032
|
+
"FunctionDeclaration",
|
|
17033
|
+
"FunctionExpression",
|
|
17034
|
+
"ArrowFunctionExpression",
|
|
17035
|
+
"ClassDeclaration",
|
|
17036
|
+
"ClassExpression"
|
|
17037
|
+
]);
|
|
17038
|
+
const isReactImport$1 = (symbol) => {
|
|
17039
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
17040
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
17041
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
17042
|
+
return importDeclaration.source.value === "react";
|
|
17043
|
+
};
|
|
17044
|
+
const getImportedName = (symbol) => {
|
|
17045
|
+
if (symbol.kind !== "import") return null;
|
|
17046
|
+
if (!isReactImport$1(symbol)) return null;
|
|
17047
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
17048
|
+
};
|
|
17049
|
+
const isReactNamespaceImport = (symbol) => {
|
|
17050
|
+
if (symbol.kind !== "import") return false;
|
|
17051
|
+
if (!isReactImport$1(symbol)) return false;
|
|
17052
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
17053
|
+
};
|
|
17054
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
17055
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
17056
|
+
const symbol = scopes.symbolFor(callee);
|
|
17057
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
17058
|
+
};
|
|
17059
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
17060
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
17061
|
+
if (callee.computed) return false;
|
|
17062
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
17063
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
17064
|
+
if (callee.property.name !== "createElement") return false;
|
|
17065
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
17066
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
17067
|
+
};
|
|
17068
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
17069
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
17070
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
17071
|
+
};
|
|
17072
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
17073
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
17074
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
17075
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
17076
|
+
const nodeRecord = node;
|
|
17077
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
17078
|
+
if (key === "parent") continue;
|
|
17079
|
+
const child = nodeRecord[key];
|
|
17080
|
+
if (Array.isArray(child)) {
|
|
17081
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
17082
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
17083
|
+
}
|
|
17084
|
+
return false;
|
|
17085
|
+
};
|
|
17086
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
17087
|
+
//#endregion
|
|
16242
17088
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
16243
17089
|
const noGiantComponent = defineRule({
|
|
16244
17090
|
id: "no-giant-component",
|
|
@@ -16246,10 +17092,13 @@ const noGiantComponent = defineRule({
|
|
|
16246
17092
|
tags: ["test-noise", "react-jsx-only"],
|
|
16247
17093
|
recommendation: "Extract logical sections into focused components: `<UserHeader />`, `<UserActions />`, etc.",
|
|
16248
17094
|
create: (context) => {
|
|
16249
|
-
const
|
|
16250
|
-
if (!bodyNode.loc) return;
|
|
17095
|
+
const getOversizedComponentLineCount = (bodyNode) => {
|
|
17096
|
+
if (!bodyNode.loc) return null;
|
|
16251
17097
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
16252
|
-
|
|
17098
|
+
return lineCount > 300 ? lineCount : null;
|
|
17099
|
+
};
|
|
17100
|
+
const reportOversizedComponent = (nameNode, componentName, lineCount) => {
|
|
17101
|
+
context.report({
|
|
16253
17102
|
node: nameNode,
|
|
16254
17103
|
message: `Component "${componentName}" is ${lineCount} lines — consider breaking it into smaller focused components`
|
|
16255
17104
|
});
|
|
@@ -16257,12 +17106,18 @@ const noGiantComponent = defineRule({
|
|
|
16257
17106
|
return {
|
|
16258
17107
|
FunctionDeclaration(node) {
|
|
16259
17108
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
16260
|
-
|
|
17109
|
+
const lineCount = getOversizedComponentLineCount(node);
|
|
17110
|
+
if (lineCount === null) return;
|
|
17111
|
+
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
17112
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16261
17113
|
},
|
|
16262
17114
|
VariableDeclarator(node) {
|
|
16263
17115
|
if (!isComponentAssignment(node)) return;
|
|
16264
17116
|
if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
|
|
16265
|
-
|
|
17117
|
+
const lineCount = getOversizedComponentLineCount(node.init);
|
|
17118
|
+
if (lineCount === null) return;
|
|
17119
|
+
if (!functionContainsReactRenderOutput(node.init, context.scopes)) return;
|
|
17120
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16266
17121
|
}
|
|
16267
17122
|
};
|
|
16268
17123
|
}
|
|
@@ -16603,7 +17458,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
16603
17458
|
});
|
|
16604
17459
|
//#endregion
|
|
16605
17460
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
16606
|
-
const buildMessage$
|
|
17461
|
+
const buildMessage$13 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
16607
17462
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
16608
17463
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
16609
17464
|
tr: ["none", "presentation"],
|
|
@@ -16647,7 +17502,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
16647
17502
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
16648
17503
|
context.report({
|
|
16649
17504
|
node: roleAttribute,
|
|
16650
|
-
message: buildMessage$
|
|
17505
|
+
message: buildMessage$13(elementType, firstRole)
|
|
16651
17506
|
});
|
|
16652
17507
|
} };
|
|
16653
17508
|
}
|
|
@@ -16848,7 +17703,7 @@ const isInsideClassBody = (node) => {
|
|
|
16848
17703
|
let current = node.parent;
|
|
16849
17704
|
while (current) {
|
|
16850
17705
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
16851
|
-
if (isFunctionLike(current)) return false;
|
|
17706
|
+
if (isFunctionLike$1(current)) return false;
|
|
16852
17707
|
current = current.parent;
|
|
16853
17708
|
}
|
|
16854
17709
|
return false;
|
|
@@ -17091,7 +17946,7 @@ const noMoment = defineRule({
|
|
|
17091
17946
|
});
|
|
17092
17947
|
//#endregion
|
|
17093
17948
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17094
|
-
const buildMessage$
|
|
17949
|
+
const buildMessage$12 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17095
17950
|
const resolveSettings$16 = (settings) => {
|
|
17096
17951
|
const reactDoctor = settings?.["react-doctor"];
|
|
17097
17952
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -17412,7 +18267,7 @@ const noMultiComp = defineRule({
|
|
|
17412
18267
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
17413
18268
|
for (const component of flagged.slice(1)) context.report({
|
|
17414
18269
|
node: component.reportNode,
|
|
17415
|
-
message: buildMessage$
|
|
18270
|
+
message: buildMessage$12(component.name)
|
|
17416
18271
|
});
|
|
17417
18272
|
} };
|
|
17418
18273
|
}
|
|
@@ -17490,7 +18345,7 @@ const noMutableInDeps = defineRule({
|
|
|
17490
18345
|
});
|
|
17491
18346
|
//#endregion
|
|
17492
18347
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
17493
|
-
const MESSAGE$
|
|
18348
|
+
const MESSAGE$15 = "Reducer mutates its current state and returns the same reference. Return a copied object or array so React can observe the update.";
|
|
17494
18349
|
const MUTATING_ARRAY_METHODS = new Set([
|
|
17495
18350
|
"copyWithin",
|
|
17496
18351
|
"fill",
|
|
@@ -17687,7 +18542,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
17687
18542
|
reportedNodes.add(mutation.node);
|
|
17688
18543
|
context.report({
|
|
17689
18544
|
node: mutation.node,
|
|
17690
|
-
message: MESSAGE$
|
|
18545
|
+
message: MESSAGE$15
|
|
17691
18546
|
});
|
|
17692
18547
|
}
|
|
17693
18548
|
};
|
|
@@ -17773,7 +18628,7 @@ const noMutatingReducerState = defineRule({
|
|
|
17773
18628
|
});
|
|
17774
18629
|
//#endregion
|
|
17775
18630
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
17776
|
-
const buildMessage$
|
|
18631
|
+
const buildMessage$11 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
17777
18632
|
const noNamespace = defineRule({
|
|
17778
18633
|
id: "no-namespace",
|
|
17779
18634
|
severity: "warn",
|
|
@@ -17785,7 +18640,7 @@ const noNamespace = defineRule({
|
|
|
17785
18640
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
17786
18641
|
context.report({
|
|
17787
18642
|
node: namespaced,
|
|
17788
|
-
message: buildMessage$
|
|
18643
|
+
message: buildMessage$11(fullName)
|
|
17789
18644
|
});
|
|
17790
18645
|
},
|
|
17791
18646
|
CallExpression(node) {
|
|
@@ -17796,7 +18651,7 @@ const noNamespace = defineRule({
|
|
|
17796
18651
|
if (!firstArgument.value.includes(":")) return;
|
|
17797
18652
|
context.report({
|
|
17798
18653
|
node: firstArgument,
|
|
17799
|
-
message: buildMessage$
|
|
18654
|
+
message: buildMessage$11(firstArgument.value)
|
|
17800
18655
|
});
|
|
17801
18656
|
}
|
|
17802
18657
|
})
|
|
@@ -17840,7 +18695,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
17840
18695
|
});
|
|
17841
18696
|
//#endregion
|
|
17842
18697
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
17843
|
-
const buildMessage$
|
|
18698
|
+
const buildMessage$10 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
|
|
17844
18699
|
const INTERACTIVE_HANDLERS = [
|
|
17845
18700
|
"onClick",
|
|
17846
18701
|
"onMouseDown",
|
|
@@ -17866,13 +18721,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
17866
18721
|
}
|
|
17867
18722
|
context.report({
|
|
17868
18723
|
node: node.name,
|
|
17869
|
-
message: buildMessage$
|
|
18724
|
+
message: buildMessage$10(tag)
|
|
17870
18725
|
});
|
|
17871
18726
|
} })
|
|
17872
18727
|
});
|
|
17873
18728
|
//#endregion
|
|
17874
18729
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
17875
|
-
const buildMessage$
|
|
18730
|
+
const buildMessage$9 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
17876
18731
|
const DEFAULT_ALLOWED_ROLES = {
|
|
17877
18732
|
ul: [
|
|
17878
18733
|
"menu",
|
|
@@ -17936,14 +18791,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
17936
18791
|
if (!isInteractiveRole(firstRole)) return;
|
|
17937
18792
|
context.report({
|
|
17938
18793
|
node: roleAttribute,
|
|
17939
|
-
message: buildMessage$
|
|
18794
|
+
message: buildMessage$9(elementType, firstRole)
|
|
17940
18795
|
});
|
|
17941
18796
|
} };
|
|
17942
18797
|
}
|
|
17943
18798
|
});
|
|
17944
18799
|
//#endregion
|
|
17945
18800
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
17946
|
-
const MESSAGE$
|
|
18801
|
+
const MESSAGE$14 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
|
|
17947
18802
|
const resolveSettings$14 = (settings) => {
|
|
17948
18803
|
const reactDoctor = settings?.["react-doctor"];
|
|
17949
18804
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -17970,7 +18825,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17970
18825
|
if (numeric === null) {
|
|
17971
18826
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
17972
18827
|
node: tabIndex,
|
|
17973
|
-
message: MESSAGE$
|
|
18828
|
+
message: MESSAGE$14
|
|
17974
18829
|
});
|
|
17975
18830
|
return;
|
|
17976
18831
|
}
|
|
@@ -17983,7 +18838,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17983
18838
|
if (!roleAttribute) {
|
|
17984
18839
|
context.report({
|
|
17985
18840
|
node: tabIndex,
|
|
17986
|
-
message: MESSAGE$
|
|
18841
|
+
message: MESSAGE$14
|
|
17987
18842
|
});
|
|
17988
18843
|
return;
|
|
17989
18844
|
}
|
|
@@ -17997,7 +18852,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17997
18852
|
}
|
|
17998
18853
|
context.report({
|
|
17999
18854
|
node: tabIndex,
|
|
18000
|
-
message: MESSAGE$
|
|
18855
|
+
message: MESSAGE$14
|
|
18001
18856
|
});
|
|
18002
18857
|
} };
|
|
18003
18858
|
}
|
|
@@ -18558,7 +19413,7 @@ const noPureBlackBackground = defineRule({
|
|
|
18558
19413
|
});
|
|
18559
19414
|
//#endregion
|
|
18560
19415
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
18561
|
-
const MESSAGE$
|
|
19416
|
+
const MESSAGE$13 = "`React.Children` is uncommon and leads to fragile components.";
|
|
18562
19417
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
18563
19418
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
18564
19419
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -18583,13 +19438,13 @@ const noReactChildren = defineRule({
|
|
|
18583
19438
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
18584
19439
|
context.report({
|
|
18585
19440
|
node: calleeOuter,
|
|
18586
|
-
message: MESSAGE$
|
|
19441
|
+
message: MESSAGE$13
|
|
18587
19442
|
});
|
|
18588
19443
|
return;
|
|
18589
19444
|
}
|
|
18590
19445
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
18591
19446
|
node: calleeOuter,
|
|
18592
|
-
message: MESSAGE$
|
|
19447
|
+
message: MESSAGE$13
|
|
18593
19448
|
});
|
|
18594
19449
|
} })
|
|
18595
19450
|
});
|
|
@@ -18605,7 +19460,7 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
18605
19460
|
if (sourceValue !== source) return;
|
|
18606
19461
|
for (const specifier of node.specifiers ?? []) {
|
|
18607
19462
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18608
|
-
const importedName = getImportedName(specifier);
|
|
19463
|
+
const importedName = getImportedName$1(specifier);
|
|
18609
19464
|
if (!importedName) continue;
|
|
18610
19465
|
const message = messages.get(importedName);
|
|
18611
19466
|
if (message) context.report({
|
|
@@ -18657,7 +19512,7 @@ const buildTestUtilsMessage = (importedName) => {
|
|
|
18657
19512
|
const reportTestUtilsImports = (node, context) => {
|
|
18658
19513
|
for (const specifier of node.specifiers ?? []) {
|
|
18659
19514
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18660
|
-
const importedName = getImportedName(specifier) ?? "default";
|
|
19515
|
+
const importedName = getImportedName$1(specifier) ?? "default";
|
|
18661
19516
|
context.report({
|
|
18662
19517
|
node: specifier,
|
|
18663
19518
|
message: buildTestUtilsMessage(importedName)
|
|
@@ -18785,7 +19640,7 @@ const getTagsForRole = (role) => {
|
|
|
18785
19640
|
};
|
|
18786
19641
|
//#endregion
|
|
18787
19642
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
18788
|
-
const buildMessage$
|
|
19643
|
+
const buildMessage$8 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
18789
19644
|
const resolveSettings$13 = (settings) => {
|
|
18790
19645
|
const reactDoctor = settings?.["react-doctor"];
|
|
18791
19646
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -18808,14 +19663,14 @@ const noRedundantRoles = defineRule({
|
|
|
18808
19663
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
18809
19664
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
18810
19665
|
node: roleAttr,
|
|
18811
|
-
message: buildMessage$
|
|
19666
|
+
message: buildMessage$8(tag, role)
|
|
18812
19667
|
});
|
|
18813
19668
|
} };
|
|
18814
19669
|
}
|
|
18815
19670
|
});
|
|
18816
19671
|
//#endregion
|
|
18817
19672
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
18818
|
-
const buildMessage$
|
|
19673
|
+
const buildMessage$7 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
18819
19674
|
const isPureComponentSuper = (superClass) => {
|
|
18820
19675
|
if (!superClass) return false;
|
|
18821
19676
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -18847,7 +19702,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
18847
19702
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
18848
19703
|
context.report({
|
|
18849
19704
|
node: reportNode,
|
|
18850
|
-
message: buildMessage$
|
|
19705
|
+
message: buildMessage$7(className)
|
|
18851
19706
|
});
|
|
18852
19707
|
};
|
|
18853
19708
|
return {
|
|
@@ -18906,7 +19761,7 @@ const noRenderPropChildren = defineRule({
|
|
|
18906
19761
|
});
|
|
18907
19762
|
//#endregion
|
|
18908
19763
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
18909
|
-
const MESSAGE$
|
|
19764
|
+
const MESSAGE$12 = "Do not use the return value from `ReactDOM.render`.";
|
|
18910
19765
|
const isReactDomRenderCall = (node) => {
|
|
18911
19766
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
18912
19767
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -18929,7 +19784,7 @@ const noRenderReturnValue = defineRule({
|
|
|
18929
19784
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
18930
19785
|
context.report({
|
|
18931
19786
|
node: node.callee,
|
|
18932
|
-
message: MESSAGE$
|
|
19787
|
+
message: MESSAGE$12
|
|
18933
19788
|
});
|
|
18934
19789
|
} })
|
|
18935
19790
|
});
|
|
@@ -19324,7 +20179,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
19324
20179
|
const isInsideServerOnlyScope = (node) => {
|
|
19325
20180
|
let currentNode = node.parent ?? null;
|
|
19326
20181
|
while (currentNode) {
|
|
19327
|
-
if (isFunctionLike(currentNode)) {
|
|
20182
|
+
if (isFunctionLike$1(currentNode)) {
|
|
19328
20183
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
19329
20184
|
}
|
|
19330
20185
|
currentNode = currentNode.parent ?? null;
|
|
@@ -19389,7 +20244,7 @@ const getParentComponent = (node) => {
|
|
|
19389
20244
|
};
|
|
19390
20245
|
//#endregion
|
|
19391
20246
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
19392
|
-
const MESSAGE$
|
|
20247
|
+
const MESSAGE$11 = "Do not use `this.setState` in components.";
|
|
19393
20248
|
const noSetState = defineRule({
|
|
19394
20249
|
id: "no-set-state",
|
|
19395
20250
|
severity: "warn",
|
|
@@ -19403,7 +20258,7 @@ const noSetState = defineRule({
|
|
|
19403
20258
|
if (!getParentComponent(node)) return;
|
|
19404
20259
|
context.report({
|
|
19405
20260
|
node: node.callee,
|
|
19406
|
-
message: MESSAGE$
|
|
20261
|
+
message: MESSAGE$11
|
|
19407
20262
|
});
|
|
19408
20263
|
} })
|
|
19409
20264
|
});
|
|
@@ -19562,7 +20417,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
19562
20417
|
};
|
|
19563
20418
|
//#endregion
|
|
19564
20419
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
19565
|
-
const MESSAGE$
|
|
20420
|
+
const MESSAGE$10 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
19566
20421
|
const DEFAULT_HANDLERS = [
|
|
19567
20422
|
"onClick",
|
|
19568
20423
|
"onMouseDown",
|
|
@@ -19621,7 +20476,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
19621
20476
|
if (!roleAttribute || !roleAttribute.value) {
|
|
19622
20477
|
context.report({
|
|
19623
20478
|
node: node.name,
|
|
19624
|
-
message: MESSAGE$
|
|
20479
|
+
message: MESSAGE$10
|
|
19625
20480
|
});
|
|
19626
20481
|
return;
|
|
19627
20482
|
}
|
|
@@ -19631,14 +20486,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
19631
20486
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
19632
20487
|
context.report({
|
|
19633
20488
|
node: node.name,
|
|
19634
|
-
message: MESSAGE$
|
|
20489
|
+
message: MESSAGE$10
|
|
19635
20490
|
});
|
|
19636
20491
|
return;
|
|
19637
20492
|
}
|
|
19638
20493
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
19639
20494
|
context.report({
|
|
19640
20495
|
node: node.name,
|
|
19641
|
-
message: MESSAGE$
|
|
20496
|
+
message: MESSAGE$10
|
|
19642
20497
|
});
|
|
19643
20498
|
} };
|
|
19644
20499
|
}
|
|
@@ -19694,7 +20549,7 @@ const noStringRefs = defineRule({
|
|
|
19694
20549
|
});
|
|
19695
20550
|
//#endregion
|
|
19696
20551
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
19697
|
-
const MESSAGE$
|
|
20552
|
+
const MESSAGE$9 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
19698
20553
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
19699
20554
|
let ancestor = node.parent;
|
|
19700
20555
|
while (ancestor) {
|
|
@@ -19762,7 +20617,7 @@ const noThisInSfc = defineRule({
|
|
|
19762
20617
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
19763
20618
|
context.report({
|
|
19764
20619
|
node,
|
|
19765
|
-
message: MESSAGE$
|
|
20620
|
+
message: MESSAGE$9
|
|
19766
20621
|
});
|
|
19767
20622
|
} };
|
|
19768
20623
|
}
|
|
@@ -19944,7 +20799,7 @@ const ESCAPED_VERSIONS = {
|
|
|
19944
20799
|
">": "`>` / `>`",
|
|
19945
20800
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
19946
20801
|
};
|
|
19947
|
-
const buildMessage$
|
|
20802
|
+
const buildMessage$6 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
19948
20803
|
const noUnescapedEntities = defineRule({
|
|
19949
20804
|
id: "no-unescaped-entities",
|
|
19950
20805
|
severity: "warn",
|
|
@@ -19955,7 +20810,7 @@ const noUnescapedEntities = defineRule({
|
|
|
19955
20810
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
19956
20811
|
context.report({
|
|
19957
20812
|
node,
|
|
19958
|
-
message: buildMessage$
|
|
20813
|
+
message: buildMessage$6(character)
|
|
19959
20814
|
});
|
|
19960
20815
|
return;
|
|
19961
20816
|
}
|
|
@@ -20976,7 +21831,7 @@ const SAFER_REPLACEMENT = {
|
|
|
20976
21831
|
componentWillUpdate: "componentDidUpdate",
|
|
20977
21832
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
20978
21833
|
};
|
|
20979
|
-
const buildMessage$
|
|
21834
|
+
const buildMessage$5 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
20980
21835
|
const resolveSettings$9 = (settings) => {
|
|
20981
21836
|
const reactDoctor = settings?.["react-doctor"];
|
|
20982
21837
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21024,7 +21879,7 @@ const noUnsafe = defineRule({
|
|
|
21024
21879
|
if (!getParentComponent(node)) return;
|
|
21025
21880
|
context.report({
|
|
21026
21881
|
node: node.key,
|
|
21027
|
-
message: buildMessage$
|
|
21882
|
+
message: buildMessage$5(name)
|
|
21028
21883
|
});
|
|
21029
21884
|
},
|
|
21030
21885
|
Property(node) {
|
|
@@ -21035,7 +21890,7 @@ const noUnsafe = defineRule({
|
|
|
21035
21890
|
if (isEs5Component(ancestor)) {
|
|
21036
21891
|
context.report({
|
|
21037
21892
|
node: node.key,
|
|
21038
|
-
message: buildMessage$
|
|
21893
|
+
message: buildMessage$5(name)
|
|
21039
21894
|
});
|
|
21040
21895
|
return;
|
|
21041
21896
|
}
|
|
@@ -21047,7 +21902,7 @@ const noUnsafe = defineRule({
|
|
|
21047
21902
|
});
|
|
21048
21903
|
//#endregion
|
|
21049
21904
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21050
|
-
const buildMessage$
|
|
21905
|
+
const buildMessage$4 = (parentName, isInProp, allowAsProps) => {
|
|
21051
21906
|
let message = "Don't define components inside another component";
|
|
21052
21907
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21053
21908
|
message += " — extract it to module scope.";
|
|
@@ -21116,7 +21971,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
21116
21971
|
const findEnclosingComponent = (node) => {
|
|
21117
21972
|
let walker = node.parent;
|
|
21118
21973
|
while (walker) {
|
|
21119
|
-
if (isFunctionLike(walker)) {
|
|
21974
|
+
if (isFunctionLike$1(walker)) {
|
|
21120
21975
|
const componentName = inferFunctionLikeName(walker);
|
|
21121
21976
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
21122
21977
|
component: walker,
|
|
@@ -21282,7 +22137,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
21282
22137
|
if (!enclosing) return;
|
|
21283
22138
|
context.report({
|
|
21284
22139
|
node: reportNode,
|
|
21285
|
-
message: buildMessage$
|
|
22140
|
+
message: buildMessage$4(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
21286
22141
|
});
|
|
21287
22142
|
};
|
|
21288
22143
|
const checkFunctionLike = (node) => {
|
|
@@ -21414,7 +22269,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
21414
22269
|
//#endregion
|
|
21415
22270
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
21416
22271
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
21417
|
-
const MESSAGE$
|
|
22272
|
+
const MESSAGE$8 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
21418
22273
|
const resolveSettings$7 = (settings) => {
|
|
21419
22274
|
const reactDoctor = settings?.["react-doctor"];
|
|
21420
22275
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -21447,7 +22302,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
21447
22302
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
21448
22303
|
context.report({
|
|
21449
22304
|
node: node.callee,
|
|
21450
|
-
message: MESSAGE$
|
|
22305
|
+
message: MESSAGE$8
|
|
21451
22306
|
});
|
|
21452
22307
|
} };
|
|
21453
22308
|
}
|
|
@@ -21985,6 +22840,214 @@ const onlyExportComponents = defineRule({
|
|
|
21985
22840
|
}
|
|
21986
22841
|
});
|
|
21987
22842
|
//#endregion
|
|
22843
|
+
//#region src/plugin/rules/preact/preact-no-children-length.ts
|
|
22844
|
+
const ARRAY_READ_METHOD_NAMES = new Set([
|
|
22845
|
+
"length",
|
|
22846
|
+
"map",
|
|
22847
|
+
"forEach",
|
|
22848
|
+
"filter",
|
|
22849
|
+
"find",
|
|
22850
|
+
"reduce",
|
|
22851
|
+
"some",
|
|
22852
|
+
"every",
|
|
22853
|
+
"flat",
|
|
22854
|
+
"flatMap",
|
|
22855
|
+
"indexOf",
|
|
22856
|
+
"includes",
|
|
22857
|
+
"slice",
|
|
22858
|
+
"concat",
|
|
22859
|
+
"join"
|
|
22860
|
+
]);
|
|
22861
|
+
const CHILDREN_ARRAY_MESSAGE = "`props.children` is not always an array in Preact — use `toChildArray(children)` from `preact` before calling array methods or reading `.length`.";
|
|
22862
|
+
const isDestructuredChildrenParam = (identifier) => {
|
|
22863
|
+
let cursor = identifier.parent;
|
|
22864
|
+
while (cursor) {
|
|
22865
|
+
if (isNodeOfType(cursor, "FunctionDeclaration") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "ArrowFunctionExpression")) {
|
|
22866
|
+
const firstParam = cursor.params[0];
|
|
22867
|
+
if (!firstParam || !isNodeOfType(firstParam, "ObjectPattern")) return false;
|
|
22868
|
+
return firstParam.properties.some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "children");
|
|
22869
|
+
}
|
|
22870
|
+
cursor = cursor.parent ?? null;
|
|
22871
|
+
}
|
|
22872
|
+
return false;
|
|
22873
|
+
};
|
|
22874
|
+
const isChildrenMemberExpression = (node) => {
|
|
22875
|
+
const object = node.object;
|
|
22876
|
+
if (!isNodeOfType(object, "MemberExpression")) return isNodeOfType(object, "Identifier") && object.name === "children" && isDestructuredChildrenParam(object);
|
|
22877
|
+
if (!isNodeOfType(object.property, "Identifier") || object.property.name !== "children") return false;
|
|
22878
|
+
const propsObject = object.object;
|
|
22879
|
+
if (isNodeOfType(propsObject, "Identifier") && propsObject.name === "props") return true;
|
|
22880
|
+
if (isNodeOfType(propsObject, "MemberExpression") && isNodeOfType(propsObject.property, "Identifier") && propsObject.property.name === "props" && isNodeOfType(propsObject.object, "ThisExpression")) return true;
|
|
22881
|
+
return false;
|
|
22882
|
+
};
|
|
22883
|
+
const preactNoChildrenLength = defineRule({
|
|
22884
|
+
id: "preact-no-children-length",
|
|
22885
|
+
requires: ["preact"],
|
|
22886
|
+
severity: "warn",
|
|
22887
|
+
recommendation: "Wrap with `toChildArray(children)` from `preact` before accessing array methods or `.length`.",
|
|
22888
|
+
create: (context) => ({ MemberExpression(node) {
|
|
22889
|
+
if (node.computed) return;
|
|
22890
|
+
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
22891
|
+
if (!ARRAY_READ_METHOD_NAMES.has(node.property.name)) return;
|
|
22892
|
+
if (!isChildrenMemberExpression(node)) return;
|
|
22893
|
+
context.report({
|
|
22894
|
+
node,
|
|
22895
|
+
message: CHILDREN_ARRAY_MESSAGE
|
|
22896
|
+
});
|
|
22897
|
+
} })
|
|
22898
|
+
});
|
|
22899
|
+
//#endregion
|
|
22900
|
+
//#region src/plugin/rules/preact/preact-no-react-hooks-import.ts
|
|
22901
|
+
const REACT_HOOK_NAMES = new Set([
|
|
22902
|
+
"useCallback",
|
|
22903
|
+
"useContext",
|
|
22904
|
+
"useDebugValue",
|
|
22905
|
+
"useDeferredValue",
|
|
22906
|
+
"useEffect",
|
|
22907
|
+
"useId",
|
|
22908
|
+
"useImperativeHandle",
|
|
22909
|
+
"useInsertionEffect",
|
|
22910
|
+
"useLayoutEffect",
|
|
22911
|
+
"useMemo",
|
|
22912
|
+
"useReducer",
|
|
22913
|
+
"useRef",
|
|
22914
|
+
"useState",
|
|
22915
|
+
"useSyncExternalStore",
|
|
22916
|
+
"useTransition"
|
|
22917
|
+
]);
|
|
22918
|
+
const buildMessage$3 = (importedNames) => `Import ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`preact/hooks\` (or \`preact/compat\`) — importing hooks from \`react\` in a pure-Preact project loads a second copy of Preact's hook state and triggers \`__H\` undefined errors.`;
|
|
22919
|
+
const preactNoReactHooksImport = defineRule({
|
|
22920
|
+
id: "preact-no-react-hooks-import",
|
|
22921
|
+
requires: ["pure-preact"],
|
|
22922
|
+
severity: "warn",
|
|
22923
|
+
recommendation: "Replace `from \"react\"` with `from \"preact/hooks\"` (or `from \"preact/compat\"` if other React API surface is needed).",
|
|
22924
|
+
create: (context) => ({ ImportDeclaration(node) {
|
|
22925
|
+
const source = node.source;
|
|
22926
|
+
if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
|
|
22927
|
+
const reactHookSpecifiers = [];
|
|
22928
|
+
for (const specifier of node.specifiers) {
|
|
22929
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
22930
|
+
const imported = specifier.imported;
|
|
22931
|
+
if (!isNodeOfType(imported, "Identifier")) continue;
|
|
22932
|
+
if (REACT_HOOK_NAMES.has(imported.name)) reactHookSpecifiers.push(specifier);
|
|
22933
|
+
}
|
|
22934
|
+
if (reactHookSpecifiers.length === 0) return;
|
|
22935
|
+
const importedNames = reactHookSpecifiers.map((specifier) => {
|
|
22936
|
+
const imported = specifier.imported;
|
|
22937
|
+
return isNodeOfType(imported, "Identifier") ? imported.name : "";
|
|
22938
|
+
});
|
|
22939
|
+
context.report({
|
|
22940
|
+
node,
|
|
22941
|
+
message: buildMessage$3(importedNames)
|
|
22942
|
+
});
|
|
22943
|
+
} })
|
|
22944
|
+
});
|
|
22945
|
+
//#endregion
|
|
22946
|
+
//#region src/plugin/rules/preact/preact-no-render-arguments.ts
|
|
22947
|
+
const PREACT_COMPONENT_NAMESPACES = new Set(["Preact"]);
|
|
22948
|
+
const PREACT_COMPONENT_NAMES = new Set(["Component", "PureComponent"]);
|
|
22949
|
+
const isPreactNamespaceComponentRef = (node) => {
|
|
22950
|
+
if (!isNodeOfType(node, "MemberExpression")) return false;
|
|
22951
|
+
if (!isNodeOfType(node.object, "Identifier")) return false;
|
|
22952
|
+
if (!PREACT_COMPONENT_NAMESPACES.has(node.object.name)) return false;
|
|
22953
|
+
if (!isNodeOfType(node.property, "Identifier")) return false;
|
|
22954
|
+
return PREACT_COMPONENT_NAMES.has(node.property.name);
|
|
22955
|
+
};
|
|
22956
|
+
const isPreactOrReactComponentClass = (node) => {
|
|
22957
|
+
if (isEs6Component(node)) return true;
|
|
22958
|
+
if (!isNodeOfType(node, "ClassDeclaration") && !isNodeOfType(node, "ClassExpression")) return false;
|
|
22959
|
+
const superClass = node.superClass;
|
|
22960
|
+
if (!superClass) return false;
|
|
22961
|
+
return isPreactNamespaceComponentRef(superClass);
|
|
22962
|
+
};
|
|
22963
|
+
const RENDER_ARGUMENTS_MESSAGE = "Preact's `render(props, state)` argument shape is harder to type than `this.props` / `this.state`, breaks under `preact/compat` (which mirrors React's parameterless signature), and quietly diverges from every other Preact lifecycle method. Prefer reading from `this.props` / `this.state`.";
|
|
22964
|
+
const isInstanceMethodNamedRender = (node) => isNodeOfType(node, "MethodDefinition") && node.kind === "method" && node.static !== true && isNodeOfType(node.key, "Identifier") && node.key.name === "render";
|
|
22965
|
+
const isInsideEs6Component$1 = (methodDefinition) => {
|
|
22966
|
+
const classBody = methodDefinition.parent;
|
|
22967
|
+
if (!classBody || !isNodeOfType(classBody, "ClassBody")) return false;
|
|
22968
|
+
const owningClass = classBody.parent;
|
|
22969
|
+
if (!owningClass) return false;
|
|
22970
|
+
return isPreactOrReactComponentClass(owningClass);
|
|
22971
|
+
};
|
|
22972
|
+
const stripThisParameter = (params) => {
|
|
22973
|
+
const first = params[0];
|
|
22974
|
+
if (!first) return params;
|
|
22975
|
+
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
22976
|
+
return params;
|
|
22977
|
+
};
|
|
22978
|
+
const preactNoRenderArguments = defineRule({
|
|
22979
|
+
id: "preact-no-render-arguments",
|
|
22980
|
+
requires: ["preact"],
|
|
22981
|
+
severity: "warn",
|
|
22982
|
+
recommendation: "Read state/props from `this.props` / `this.state` inside `render()` instead of declaring positional parameters.",
|
|
22983
|
+
create: (context) => ({ MethodDefinition(node) {
|
|
22984
|
+
if (!isInstanceMethodNamedRender(node)) return;
|
|
22985
|
+
if (!isInsideEs6Component$1(node)) return;
|
|
22986
|
+
const renderFunction = node.value;
|
|
22987
|
+
if (!renderFunction || !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
22988
|
+
const firstParameter = stripThisParameter(renderFunction.params)[0];
|
|
22989
|
+
if (!firstParameter) return;
|
|
22990
|
+
context.report({
|
|
22991
|
+
node: firstParameter,
|
|
22992
|
+
message: RENDER_ARGUMENTS_MESSAGE
|
|
22993
|
+
});
|
|
22994
|
+
} })
|
|
22995
|
+
});
|
|
22996
|
+
//#endregion
|
|
22997
|
+
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
22998
|
+
const MESSAGE$7 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
|
|
22999
|
+
const preactPreferOndblclick = defineRule({
|
|
23000
|
+
id: "preact-prefer-ondblclick",
|
|
23001
|
+
requires: ["pure-preact"],
|
|
23002
|
+
severity: "warn",
|
|
23003
|
+
recommendation: "Rename the handler from `onDoubleClick` to `onDblClick` to match the DOM event name.",
|
|
23004
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23005
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23006
|
+
const tagName = node.name.name;
|
|
23007
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23008
|
+
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
23009
|
+
if (!onDoubleClickAttribute) return;
|
|
23010
|
+
context.report({
|
|
23011
|
+
node: onDoubleClickAttribute,
|
|
23012
|
+
message: MESSAGE$7
|
|
23013
|
+
});
|
|
23014
|
+
} })
|
|
23015
|
+
});
|
|
23016
|
+
//#endregion
|
|
23017
|
+
//#region src/plugin/rules/preact/preact-prefer-oninput.ts
|
|
23018
|
+
const PREFER_ONINPUT_MESSAGE = "In Preact core, `onChange` on text-like inputs only fires on blur — use `onInput` for real-time updates. If using `preact/compat`, this is handled automatically.";
|
|
23019
|
+
const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
23020
|
+
"checkbox",
|
|
23021
|
+
"radio",
|
|
23022
|
+
"file"
|
|
23023
|
+
]);
|
|
23024
|
+
const isTextLikeInput = (openingElement) => {
|
|
23025
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
23026
|
+
const tagName = openingElement.name.name;
|
|
23027
|
+
if (tagName === "textarea") return true;
|
|
23028
|
+
if (tagName !== "input") return false;
|
|
23029
|
+
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
23030
|
+
if (!typeAttribute) return true;
|
|
23031
|
+
const typeValue = getJsxPropStringValue(typeAttribute);
|
|
23032
|
+
if (typeValue === null) return true;
|
|
23033
|
+
return !COMPAT_EXEMPT_INPUT_TYPES.has(typeValue);
|
|
23034
|
+
};
|
|
23035
|
+
const preactPreferOninput = defineRule({
|
|
23036
|
+
id: "preact-prefer-oninput",
|
|
23037
|
+
requires: ["pure-preact"],
|
|
23038
|
+
severity: "warn",
|
|
23039
|
+
recommendation: "Replace `onChange` with `onInput` on text-like inputs, or use `preact/compat` which remaps `onChange` automatically.",
|
|
23040
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23041
|
+
if (!isTextLikeInput(node)) return;
|
|
23042
|
+
const onChangeAttribute = findJsxAttribute(node.attributes, "onChange");
|
|
23043
|
+
if (!onChangeAttribute) return;
|
|
23044
|
+
context.report({
|
|
23045
|
+
node: onChangeAttribute,
|
|
23046
|
+
message: PREFER_ONINPUT_MESSAGE
|
|
23047
|
+
});
|
|
23048
|
+
} })
|
|
23049
|
+
});
|
|
23050
|
+
//#endregion
|
|
21988
23051
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
21989
23052
|
const preferDynamicImport = defineRule({
|
|
21990
23053
|
id: "prefer-dynamic-import",
|
|
@@ -22106,6 +23169,49 @@ const preferFunctionComponent = defineRule({
|
|
|
22106
23169
|
}
|
|
22107
23170
|
});
|
|
22108
23171
|
//#endregion
|
|
23172
|
+
//#region src/plugin/rules/a11y/prefer-html-dialog.ts
|
|
23173
|
+
const ROLE_DIALOG_VALUES = new Set(["dialog", "alertdialog"]);
|
|
23174
|
+
const ROLE_DIALOG_MESSAGE = "Use the native `<dialog>` element instead of `role=\"dialog\"` on a generic container — `<dialog>` ships built-in focus trap, `Escape` dismissal, the top-layer backdrop, and the right accessibility tree without you having to wire any of it up.";
|
|
23175
|
+
const ARIA_MODAL_MESSAGE = "Use the native `<dialog>` element with `dialog.showModal()` instead of `aria-modal=\"true\"` on a generic container — the browser then owns focus trapping, scroll locking, and `Escape` dismissal, none of which `aria-modal` actually delivers on its own.";
|
|
23176
|
+
const isAriaModalTrue = (attribute) => {
|
|
23177
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
23178
|
+
if (stringValue !== null) return stringValue === "true";
|
|
23179
|
+
const value = attribute.value;
|
|
23180
|
+
if (!value) return true;
|
|
23181
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
23182
|
+
const expression = value.expression;
|
|
23183
|
+
if (isNodeOfType(expression, "Literal") && expression.value === true) return true;
|
|
23184
|
+
}
|
|
23185
|
+
return false;
|
|
23186
|
+
};
|
|
23187
|
+
const preferHtmlDialog = defineRule({
|
|
23188
|
+
id: "prefer-html-dialog",
|
|
23189
|
+
severity: "warn",
|
|
23190
|
+
recommendation: "Replace the wrapper with `<dialog>` and open it with `dialog.showModal()`. For the trigger, prefer `<button commandfor=\"id\" command=\"show-modal\">` (Chrome 135+) or fall back to a `useRef`-driven `dialogRef.current?.showModal()`.",
|
|
23191
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23192
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23193
|
+
const tagName = node.name.name;
|
|
23194
|
+
if (tagName === "dialog") return;
|
|
23195
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23196
|
+
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
23197
|
+
if (roleAttribute) {
|
|
23198
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
23199
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
23200
|
+
context.report({
|
|
23201
|
+
node: roleAttribute,
|
|
23202
|
+
message: ROLE_DIALOG_MESSAGE
|
|
23203
|
+
});
|
|
23204
|
+
return;
|
|
23205
|
+
}
|
|
23206
|
+
}
|
|
23207
|
+
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
23208
|
+
if (ariaModalAttribute && isAriaModalTrue(ariaModalAttribute)) context.report({
|
|
23209
|
+
node: ariaModalAttribute,
|
|
23210
|
+
message: ARIA_MODAL_MESSAGE
|
|
23211
|
+
});
|
|
23212
|
+
} })
|
|
23213
|
+
});
|
|
23214
|
+
//#endregion
|
|
22109
23215
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
22110
23216
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
22111
23217
|
const preferTagOverRole = defineRule({
|
|
@@ -23118,7 +24224,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
23118
24224
|
let found = false;
|
|
23119
24225
|
walkAst(functionBody, (child) => {
|
|
23120
24226
|
if (found) return;
|
|
23121
|
-
if (child !== functionBody && isFunctionLike(child)) return false;
|
|
24227
|
+
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
23122
24228
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
23123
24229
|
});
|
|
23124
24230
|
return found;
|
|
@@ -23137,7 +24243,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
23137
24243
|
let found = false;
|
|
23138
24244
|
walkAst(componentBody, (child) => {
|
|
23139
24245
|
if (found) return;
|
|
23140
|
-
if (!isFunctionLike(child)) return;
|
|
24246
|
+
if (!isFunctionLike$1(child)) return;
|
|
23141
24247
|
const functionBody = child.body;
|
|
23142
24248
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
23143
24249
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24209,6 +25315,55 @@ const rnListDataMapped = defineRule({
|
|
|
24209
25315
|
} })
|
|
24210
25316
|
});
|
|
24211
25317
|
//#endregion
|
|
25318
|
+
//#region src/plugin/rules/react-native/rn-list-missing-estimated-item-size.ts
|
|
25319
|
+
const RECYCLABLE_LIST_PACKAGES = {
|
|
25320
|
+
FlashList: ["@shopify/flash-list"],
|
|
25321
|
+
LegendList: ["@legendapp/list"]
|
|
25322
|
+
};
|
|
25323
|
+
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
25324
|
+
const isEmptyArrayLiteral = (node) => {
|
|
25325
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
25326
|
+
const expression = node.value.expression;
|
|
25327
|
+
return isNodeOfType(expression, "ArrayExpression") && (expression.elements?.length ?? 0) === 0;
|
|
25328
|
+
};
|
|
25329
|
+
const rnListMissingEstimatedItemSize = defineRule({
|
|
25330
|
+
id: "rn-list-missing-estimated-item-size",
|
|
25331
|
+
tags: ["test-noise"],
|
|
25332
|
+
requires: ["react-native"],
|
|
25333
|
+
severity: "warn",
|
|
25334
|
+
recommendation: "Add `estimatedItemSize={<avg-row-height-in-px>}` so the initial container pool matches the real rows — without it the engine guesses and flashes blank cells on fast scroll",
|
|
25335
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
25336
|
+
const localElementName = resolveJsxElementName(node);
|
|
25337
|
+
if (!localElementName) return;
|
|
25338
|
+
let canonicalRecyclerName = null;
|
|
25339
|
+
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) if (packageSources.some((packageSource) => getImportedNameFromModule(node, localElementName, packageSource) === canonicalName)) {
|
|
25340
|
+
canonicalRecyclerName = canonicalName;
|
|
25341
|
+
break;
|
|
25342
|
+
}
|
|
25343
|
+
if (!canonicalRecyclerName) return;
|
|
25344
|
+
let hasSizingHint = false;
|
|
25345
|
+
let dataIsEmptyLiteral = false;
|
|
25346
|
+
let hasDataProp = false;
|
|
25347
|
+
for (const attribute of node.attributes ?? []) {
|
|
25348
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
25349
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
25350
|
+
const attributeName = attribute.name.name;
|
|
25351
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
25352
|
+
if (attributeName === "data") {
|
|
25353
|
+
hasDataProp = true;
|
|
25354
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
25355
|
+
}
|
|
25356
|
+
}
|
|
25357
|
+
if (hasSizingHint) return;
|
|
25358
|
+
if (dataIsEmptyLiteral) return;
|
|
25359
|
+
if (!hasDataProp) return;
|
|
25360
|
+
context.report({
|
|
25361
|
+
node,
|
|
25362
|
+
message: `<${localElementName}> (from ${canonicalRecyclerName}) is missing \`estimatedItemSize\` — the engine guesses the initial container pool from a default that often mismatches your rows, causing blank flashes on fast scroll`
|
|
25363
|
+
});
|
|
25364
|
+
} })
|
|
25365
|
+
});
|
|
25366
|
+
//#endregion
|
|
24212
25367
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
24213
25368
|
const RECYCLABLE_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
24214
25369
|
const rnListRecyclableWithoutTypes = defineRule({
|
|
@@ -24396,7 +25551,7 @@ const rnNoDeprecatedModules = defineRule({
|
|
|
24396
25551
|
if (node.source?.value !== "react-native") return;
|
|
24397
25552
|
for (const specifier of node.specifiers ?? []) {
|
|
24398
25553
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
24399
|
-
const importedName = getImportedName(specifier);
|
|
25554
|
+
const importedName = getImportedName$1(specifier);
|
|
24400
25555
|
if (!importedName) continue;
|
|
24401
25556
|
const baseReplacement = DEPRECATED_RN_MODULE_REPLACEMENTS.get(importedName);
|
|
24402
25557
|
if (!baseReplacement) continue;
|
|
@@ -24850,6 +26005,80 @@ const rnNoRawText = defineRule({
|
|
|
24850
26005
|
}
|
|
24851
26006
|
});
|
|
24852
26007
|
//#endregion
|
|
26008
|
+
//#region src/plugin/rules/react-native/rn-no-renderitem-key.ts
|
|
26009
|
+
const collectTopLevelReturnExpressions = (functionNode) => {
|
|
26010
|
+
if (isNodeOfType(functionNode, "ArrowFunctionExpression") && functionNode.body) {
|
|
26011
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
|
|
26012
|
+
}
|
|
26013
|
+
const block = functionNode.body;
|
|
26014
|
+
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
26015
|
+
const returnExpressions = [];
|
|
26016
|
+
const visit = (node) => {
|
|
26017
|
+
if (FUNCTION_LIKE_TYPES$1.has(node.type)) return;
|
|
26018
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
26019
|
+
const nodeRecord = node;
|
|
26020
|
+
for (const fieldName of Object.keys(nodeRecord)) {
|
|
26021
|
+
if (fieldName === "parent") continue;
|
|
26022
|
+
const child = nodeRecord[fieldName];
|
|
26023
|
+
if (Array.isArray(child)) {
|
|
26024
|
+
for (const childItem of child) if (isAstNode(childItem)) visit(childItem);
|
|
26025
|
+
} else if (isAstNode(child)) visit(child);
|
|
26026
|
+
}
|
|
26027
|
+
};
|
|
26028
|
+
visit(block);
|
|
26029
|
+
return returnExpressions;
|
|
26030
|
+
};
|
|
26031
|
+
const collectReturnedJsxElements = (expression) => {
|
|
26032
|
+
const elements = [];
|
|
26033
|
+
const visit = (current) => {
|
|
26034
|
+
const unwrapped = stripParenExpression(current);
|
|
26035
|
+
if (isNodeOfType(unwrapped, "JSXElement")) {
|
|
26036
|
+
elements.push(unwrapped);
|
|
26037
|
+
return;
|
|
26038
|
+
}
|
|
26039
|
+
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
26040
|
+
visit(unwrapped.consequent);
|
|
26041
|
+
visit(unwrapped.alternate);
|
|
26042
|
+
return;
|
|
26043
|
+
}
|
|
26044
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) {
|
|
26045
|
+
visit(unwrapped.right);
|
|
26046
|
+
if (unwrapped.operator === "||" || unwrapped.operator === "??") visit(unwrapped.left);
|
|
26047
|
+
}
|
|
26048
|
+
};
|
|
26049
|
+
visit(expression);
|
|
26050
|
+
return elements;
|
|
26051
|
+
};
|
|
26052
|
+
const rnNoRenderitemKey = defineRule({
|
|
26053
|
+
id: "rn-no-renderitem-key",
|
|
26054
|
+
tags: ["test-noise"],
|
|
26055
|
+
requires: ["react-native"],
|
|
26056
|
+
severity: "warn",
|
|
26057
|
+
recommendation: "Remove `key` from the JSX returned by renderItem — React Native lists key rows from `keyExtractor` (or `item.key`); the inner `key` is a no-op and hides a missing `keyExtractor`",
|
|
26058
|
+
create: (context) => ({ JSXAttribute(attributeNode) {
|
|
26059
|
+
if (!isNodeOfType(attributeNode.name, "JSXIdentifier") || !RENDER_ITEM_PROP_NAMES.has(attributeNode.name.name)) return;
|
|
26060
|
+
const openingElement = attributeNode.parent;
|
|
26061
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return;
|
|
26062
|
+
const listComponentName = resolveJsxElementName(openingElement);
|
|
26063
|
+
if (!listComponentName || !REACT_NATIVE_LIST_COMPONENTS.has(listComponentName)) return;
|
|
26064
|
+
if (!attributeNode.value || !isNodeOfType(attributeNode.value, "JSXExpressionContainer")) return;
|
|
26065
|
+
const renderFunction = attributeNode.value.expression;
|
|
26066
|
+
if (!isNodeOfType(renderFunction, "ArrowFunctionExpression") && !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
26067
|
+
const returnExpressions = collectTopLevelReturnExpressions(renderFunction);
|
|
26068
|
+
const renderPropName = attributeNode.name.name;
|
|
26069
|
+
for (const returnExpression of returnExpressions) {
|
|
26070
|
+
const returnedJsxElements = collectReturnedJsxElements(returnExpression);
|
|
26071
|
+
for (const jsxElement of returnedJsxElements) {
|
|
26072
|
+
if (!hasJsxKeyAttribute(jsxElement.openingElement)) continue;
|
|
26073
|
+
context.report({
|
|
26074
|
+
node: jsxElement.openingElement,
|
|
26075
|
+
message: `\`key\` on the JSX returned by ${renderPropName} on <${listComponentName}> is a no-op — React Native lists key rows from \`keyExtractor\` (or \`item.key\`). Remove this \`key\` and set \`keyExtractor\` on the list instead`
|
|
26076
|
+
});
|
|
26077
|
+
}
|
|
26078
|
+
}
|
|
26079
|
+
} })
|
|
26080
|
+
});
|
|
26081
|
+
//#endregion
|
|
24853
26082
|
//#region src/plugin/rules/react-native/rn-no-scroll-state.ts
|
|
24854
26083
|
const SET_STATE_PATTERN = /^set[A-Z]/;
|
|
24855
26084
|
const findSetStateInBody = (body) => {
|
|
@@ -25004,7 +26233,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
25004
26233
|
if (node.source?.value !== "react-native") return;
|
|
25005
26234
|
for (const specifier of node.specifiers ?? []) {
|
|
25006
26235
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25007
|
-
const importedName = getImportedName(specifier);
|
|
26236
|
+
const importedName = getImportedName$1(specifier);
|
|
25008
26237
|
if (importedName !== "Image" && importedName !== "ImageBackground") continue;
|
|
25009
26238
|
context.report({
|
|
25010
26239
|
node: specifier,
|
|
@@ -25034,7 +26263,7 @@ const rnPreferPressable = defineRule({
|
|
|
25034
26263
|
if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
|
|
25035
26264
|
for (const specifier of node.specifiers ?? []) {
|
|
25036
26265
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25037
|
-
const importedName = getImportedName(specifier);
|
|
26266
|
+
const importedName = getImportedName$1(specifier);
|
|
25038
26267
|
if (!importedName || !TOUCHABLE_COMPONENTS.has(importedName)) continue;
|
|
25039
26268
|
context.report({
|
|
25040
26269
|
node: specifier,
|
|
@@ -25044,6 +26273,81 @@ const rnPreferPressable = defineRule({
|
|
|
25044
26273
|
} })
|
|
25045
26274
|
});
|
|
25046
26275
|
//#endregion
|
|
26276
|
+
//#region src/plugin/rules/react-native/rn-prefer-pressable-over-gesture-detector.ts
|
|
26277
|
+
const COMPOSING_CHAIN_METHOD_NAMES = new Set([
|
|
26278
|
+
"simultaneousWithExternalGesture",
|
|
26279
|
+
"requireExternalGestureToFail",
|
|
26280
|
+
"blocksExternalGesture"
|
|
26281
|
+
]);
|
|
26282
|
+
const analyzeGestureChain = (expression) => {
|
|
26283
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
26284
|
+
const chainMethodNames = [];
|
|
26285
|
+
let numberOfTapsArgument = null;
|
|
26286
|
+
let cursor = expression;
|
|
26287
|
+
while (cursor && isNodeOfType(cursor, "CallExpression")) {
|
|
26288
|
+
const callExpression = cursor;
|
|
26289
|
+
const callee = callExpression.callee;
|
|
26290
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
26291
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
26292
|
+
const methodName = callee.property.name;
|
|
26293
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
26294
|
+
factoryName: methodName,
|
|
26295
|
+
chainMethodNames,
|
|
26296
|
+
numberOfTapsArgument
|
|
26297
|
+
};
|
|
26298
|
+
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
26299
|
+
chainMethodNames.push(methodName);
|
|
26300
|
+
cursor = callee.object;
|
|
26301
|
+
}
|
|
26302
|
+
return null;
|
|
26303
|
+
};
|
|
26304
|
+
const isTapChainEligibleForPressable = (chain) => {
|
|
26305
|
+
if (chain.factoryName !== "Tap") return false;
|
|
26306
|
+
for (const methodName of chain.chainMethodNames) if (COMPOSING_CHAIN_METHOD_NAMES.has(methodName)) return false;
|
|
26307
|
+
const tapsArg = chain.numberOfTapsArgument;
|
|
26308
|
+
if (tapsArg !== null) {
|
|
26309
|
+
if (!isNodeOfType(tapsArg, "Literal")) return false;
|
|
26310
|
+
if (typeof tapsArg.value !== "number") return false;
|
|
26311
|
+
if (tapsArg.value !== 1) return false;
|
|
26312
|
+
}
|
|
26313
|
+
return true;
|
|
26314
|
+
};
|
|
26315
|
+
const rnPreferPressableOverGestureDetector = defineRule({
|
|
26316
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
26317
|
+
tags: ["test-noise"],
|
|
26318
|
+
requires: ["react-native"],
|
|
26319
|
+
severity: "warn",
|
|
26320
|
+
recommendation: "Replace `<GestureDetector gesture={Gesture.Tap()...}>` with `<Pressable>` (or `createCSSAnimatedComponent(Pressable)` from react-native-reanimated/css for animated press feedback) — every GestureDetector registers a native handler on mount",
|
|
26321
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26322
|
+
if (resolveJsxElementName(node) !== "GestureDetector") return;
|
|
26323
|
+
if (!isImportedFromModule(node, "GestureDetector", "react-native-gesture-handler")) return;
|
|
26324
|
+
let gestureExpression = null;
|
|
26325
|
+
for (const attribute of node.attributes ?? []) {
|
|
26326
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26327
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
26328
|
+
if (attribute.name.name !== "gesture") continue;
|
|
26329
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) continue;
|
|
26330
|
+
gestureExpression = attribute.value.expression;
|
|
26331
|
+
break;
|
|
26332
|
+
}
|
|
26333
|
+
if (!gestureExpression) return;
|
|
26334
|
+
const resolvedExpression = stripParenExpression(gestureExpression);
|
|
26335
|
+
let chainExpression = resolvedExpression;
|
|
26336
|
+
if (isNodeOfType(resolvedExpression, "Identifier")) {
|
|
26337
|
+
const binding = findVariableInitializer(node, resolvedExpression.name);
|
|
26338
|
+
if (!binding || !binding.initializer) return;
|
|
26339
|
+
chainExpression = stripParenExpression(binding.initializer);
|
|
26340
|
+
}
|
|
26341
|
+
const chain = analyzeGestureChain(chainExpression);
|
|
26342
|
+
if (!chain) return;
|
|
26343
|
+
if (!isTapChainEligibleForPressable(chain)) return;
|
|
26344
|
+
context.report({
|
|
26345
|
+
node,
|
|
26346
|
+
message: "<GestureDetector gesture={Gesture.Tap()...}> registers a native handler per mount — for tap-only feedback use <Pressable> (with createCSSAnimatedComponent(Pressable) from react-native-reanimated/css for animation)"
|
|
26347
|
+
});
|
|
26348
|
+
} })
|
|
26349
|
+
});
|
|
26350
|
+
//#endregion
|
|
25047
26351
|
//#region src/plugin/rules/react-native/rn-prefer-reanimated.ts
|
|
25048
26352
|
const JS_THREAD_ANIMATION_IMPORTS = new Set(["Animated", "LayoutAnimation"]);
|
|
25049
26353
|
const rnPreferReanimated = defineRule({
|
|
@@ -25056,7 +26360,7 @@ const rnPreferReanimated = defineRule({
|
|
|
25056
26360
|
if (node.source?.value !== "react-native") return;
|
|
25057
26361
|
for (const specifier of node.specifiers ?? []) {
|
|
25058
26362
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25059
|
-
const importedName = getImportedName(specifier);
|
|
26363
|
+
const importedName = getImportedName$1(specifier);
|
|
25060
26364
|
if (!importedName || !JS_THREAD_ANIMATION_IMPORTS.has(importedName)) continue;
|
|
25061
26365
|
const suggestion = importedName === "LayoutAnimation" ? "LayoutAnimation runs animations on the JS thread and causes full layout recalculations — use Reanimated's Layout Animations (entering/exiting/layout props) for UI-thread layout transitions" : "Animated from react-native runs animations on the JS thread — use react-native-reanimated for performant UI-thread animations";
|
|
25062
26366
|
context.report({
|
|
@@ -25187,6 +26491,101 @@ const rnScrollviewDynamicPadding = defineRule({
|
|
|
25187
26491
|
} })
|
|
25188
26492
|
});
|
|
25189
26493
|
//#endregion
|
|
26494
|
+
//#region src/plugin/rules/react-native/rn-scrollview-flex-in-content-container.ts
|
|
26495
|
+
const VIRTUALIZED_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
26496
|
+
const getStaticMemberKeyName = (expression) => {
|
|
26497
|
+
if (!expression.computed) {
|
|
26498
|
+
if (isNodeOfType(expression.property, "Identifier")) return expression.property.name;
|
|
26499
|
+
return null;
|
|
26500
|
+
}
|
|
26501
|
+
if (isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "string") return expression.property.value;
|
|
26502
|
+
return null;
|
|
26503
|
+
};
|
|
26504
|
+
const isStyleSheetCreateCallExpression = (expression) => {
|
|
26505
|
+
if (!expression) return false;
|
|
26506
|
+
const callExpression = stripParenExpression(expression);
|
|
26507
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26508
|
+
const callee = callExpression.callee;
|
|
26509
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "StyleSheet" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "create";
|
|
26510
|
+
};
|
|
26511
|
+
const resolveContentContainerStyleObject = (attribute) => {
|
|
26512
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) return null;
|
|
26513
|
+
if (attribute.name.name !== "contentContainerStyle") return null;
|
|
26514
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
26515
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
26516
|
+
if (isNodeOfType(expression, "ObjectExpression")) return expression;
|
|
26517
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
26518
|
+
const styleObjectKeyName = getStaticMemberKeyName(expression);
|
|
26519
|
+
if (!styleObjectKeyName) return null;
|
|
26520
|
+
const styleObjectIdentifierName = getRootIdentifierName(expression);
|
|
26521
|
+
if (!styleObjectIdentifierName) return null;
|
|
26522
|
+
const binding = findVariableInitializer(expression, styleObjectIdentifierName);
|
|
26523
|
+
if (!binding || !binding.initializer) return null;
|
|
26524
|
+
if (!isStyleSheetCreateCallExpression(binding.initializer)) return null;
|
|
26525
|
+
const argument = stripParenExpression(binding.initializer).arguments?.[0];
|
|
26526
|
+
if (!isNodeOfType(argument, "ObjectExpression")) return null;
|
|
26527
|
+
for (const property of argument.properties ?? []) {
|
|
26528
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26529
|
+
if (property.computed) continue;
|
|
26530
|
+
let matchesKey = false;
|
|
26531
|
+
if (isNodeOfType(property.key, "Identifier")) matchesKey = property.key.name === styleObjectKeyName;
|
|
26532
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") matchesKey = property.key.value === styleObjectKeyName;
|
|
26533
|
+
if (!matchesKey) continue;
|
|
26534
|
+
const propertyValue = stripParenExpression(property.value);
|
|
26535
|
+
if (isNodeOfType(propertyValue, "ObjectExpression")) return propertyValue;
|
|
26536
|
+
return null;
|
|
26537
|
+
}
|
|
26538
|
+
}
|
|
26539
|
+
return null;
|
|
26540
|
+
};
|
|
26541
|
+
const collectStyleKeyNames = (objectExpression) => {
|
|
26542
|
+
const names = /* @__PURE__ */ new Set();
|
|
26543
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26544
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26545
|
+
if (property.computed) continue;
|
|
26546
|
+
if (isNodeOfType(property.key, "Identifier")) names.add(property.key.name);
|
|
26547
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") names.add(property.key.value);
|
|
26548
|
+
}
|
|
26549
|
+
return names;
|
|
26550
|
+
};
|
|
26551
|
+
const findFlexShorthandProperty = (objectExpression) => {
|
|
26552
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26553
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26554
|
+
if (property.computed) continue;
|
|
26555
|
+
if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "flex") continue;
|
|
26556
|
+
const value = property.value;
|
|
26557
|
+
if (!isNodeOfType(value, "Literal")) return null;
|
|
26558
|
+
if (typeof value.value !== "number" || value.value <= 0) return null;
|
|
26559
|
+
return property;
|
|
26560
|
+
}
|
|
26561
|
+
return null;
|
|
26562
|
+
};
|
|
26563
|
+
const rnScrollviewFlexInContentContainer = defineRule({
|
|
26564
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
26565
|
+
tags: ["test-noise"],
|
|
26566
|
+
requires: ["react-native"],
|
|
26567
|
+
severity: "warn",
|
|
26568
|
+
recommendation: "Use `flexGrow: 1` on `contentContainerStyle` — RN's `flex: 1` shorthand sets `flexBasis: 0` and collapses the container on small devices",
|
|
26569
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26570
|
+
const elementName = resolveJsxElementName(node);
|
|
26571
|
+
if (!elementName) return;
|
|
26572
|
+
if (!SCROLLVIEW_NAMES.has(elementName) && !VIRTUALIZED_LIST_NAMES.has(elementName)) return;
|
|
26573
|
+
for (const attribute of node.attributes ?? []) {
|
|
26574
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26575
|
+
const objectExpression = resolveContentContainerStyleObject(attribute);
|
|
26576
|
+
if (!objectExpression) continue;
|
|
26577
|
+
const keyNames = collectStyleKeyNames(objectExpression);
|
|
26578
|
+
if (keyNames.has("flexGrow") || keyNames.has("flexBasis")) continue;
|
|
26579
|
+
const flexProperty = findFlexShorthandProperty(objectExpression);
|
|
26580
|
+
if (!flexProperty) continue;
|
|
26581
|
+
context.report({
|
|
26582
|
+
node: flexProperty,
|
|
26583
|
+
message: `<${elementName}> contentContainerStyle uses \`flex: <number>\` — RN's flex shorthand sets flexBasis: 0 and collapses the container on small devices. Use \`flexGrow: 1\` instead`
|
|
26584
|
+
});
|
|
26585
|
+
}
|
|
26586
|
+
} })
|
|
26587
|
+
});
|
|
26588
|
+
//#endregion
|
|
25190
26589
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
25191
26590
|
const LEGACY_SHADOW_KEYS = new Set([
|
|
25192
26591
|
"shadowColor",
|
|
@@ -28746,7 +30145,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
28746
30145
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
28747
30146
|
let current = node.parent;
|
|
28748
30147
|
while (current) {
|
|
28749
|
-
if (isFunctionLike(current)) {
|
|
30148
|
+
if (isFunctionLike$1(current)) {
|
|
28750
30149
|
const resolvedName = inferFunctionName(current);
|
|
28751
30150
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
28752
30151
|
}
|
|
@@ -28767,7 +30166,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
28767
30166
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
28768
30167
|
let current = node.parent;
|
|
28769
30168
|
while (current) {
|
|
28770
|
-
if (isFunctionLike(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30169
|
+
if (isFunctionLike$1(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
28771
30170
|
current = current.parent ?? null;
|
|
28772
30171
|
}
|
|
28773
30172
|
return false;
|
|
@@ -29101,7 +30500,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
29101
30500
|
let foundAuthCall = false;
|
|
29102
30501
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
29103
30502
|
if (foundAuthCall) return;
|
|
29104
|
-
if (isFunctionLike(child)) return false;
|
|
30503
|
+
if (isFunctionLike$1(child)) return false;
|
|
29105
30504
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
29106
30505
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
29107
30506
|
});
|
|
@@ -30353,7 +31752,7 @@ const useLazyMotion = defineRule({
|
|
|
30353
31752
|
if (node.specifiers?.some((specifier) => {
|
|
30354
31753
|
if (!isNodeOfType(specifier, "ImportSpecifier")) return false;
|
|
30355
31754
|
if (specifier.importKind === "type") return false;
|
|
30356
|
-
return getImportedName(specifier) === "motion";
|
|
31755
|
+
return getImportedName$1(specifier) === "motion";
|
|
30357
31756
|
})) context.report({
|
|
30358
31757
|
node,
|
|
30359
31758
|
message: "Import \"m\" with LazyMotion instead of \"motion\" — saves ~30kb in bundle size"
|
|
@@ -30434,6 +31833,17 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
30434
31833
|
//#endregion
|
|
30435
31834
|
//#region src/plugin/rule-registry.ts
|
|
30436
31835
|
const reactDoctorRules = [
|
|
31836
|
+
{
|
|
31837
|
+
key: "react-doctor/activity-wraps-effect-heavy-subtree",
|
|
31838
|
+
id: "activity-wraps-effect-heavy-subtree",
|
|
31839
|
+
source: "react-doctor",
|
|
31840
|
+
originallyExternal: false,
|
|
31841
|
+
rule: {
|
|
31842
|
+
...activityWrapsEffectHeavySubtree,
|
|
31843
|
+
framework: "global",
|
|
31844
|
+
category: "State & Effects"
|
|
31845
|
+
}
|
|
31846
|
+
},
|
|
30437
31847
|
{
|
|
30438
31848
|
key: "react-doctor/advanced-event-handler-refs",
|
|
30439
31849
|
id: "advanced-event-handler-refs",
|
|
@@ -30819,6 +32229,17 @@ const reactDoctorRules = [
|
|
|
30819
32229
|
category: "Architecture"
|
|
30820
32230
|
}
|
|
30821
32231
|
},
|
|
32232
|
+
{
|
|
32233
|
+
key: "react-doctor/hooks-no-nan-in-deps",
|
|
32234
|
+
id: "hooks-no-nan-in-deps",
|
|
32235
|
+
source: "react-doctor",
|
|
32236
|
+
originallyExternal: false,
|
|
32237
|
+
rule: {
|
|
32238
|
+
...hooksNoNanInDeps,
|
|
32239
|
+
framework: "global",
|
|
32240
|
+
category: "State & Effects"
|
|
32241
|
+
}
|
|
32242
|
+
},
|
|
30822
32243
|
{
|
|
30823
32244
|
key: "react-doctor/html-has-lang",
|
|
30824
32245
|
id: "html-has-lang",
|
|
@@ -30830,6 +32251,39 @@ const reactDoctorRules = [
|
|
|
30830
32251
|
category: "Accessibility"
|
|
30831
32252
|
}
|
|
30832
32253
|
},
|
|
32254
|
+
{
|
|
32255
|
+
key: "react-doctor/html-no-invalid-paragraph-child",
|
|
32256
|
+
id: "html-no-invalid-paragraph-child",
|
|
32257
|
+
source: "react-doctor",
|
|
32258
|
+
originallyExternal: false,
|
|
32259
|
+
rule: {
|
|
32260
|
+
...htmlNoInvalidParagraphChild,
|
|
32261
|
+
framework: "global",
|
|
32262
|
+
category: "Correctness"
|
|
32263
|
+
}
|
|
32264
|
+
},
|
|
32265
|
+
{
|
|
32266
|
+
key: "react-doctor/html-no-invalid-table-nesting",
|
|
32267
|
+
id: "html-no-invalid-table-nesting",
|
|
32268
|
+
source: "react-doctor",
|
|
32269
|
+
originallyExternal: false,
|
|
32270
|
+
rule: {
|
|
32271
|
+
...htmlNoInvalidTableNesting,
|
|
32272
|
+
framework: "global",
|
|
32273
|
+
category: "Correctness"
|
|
32274
|
+
}
|
|
32275
|
+
},
|
|
32276
|
+
{
|
|
32277
|
+
key: "react-doctor/html-no-nested-interactive",
|
|
32278
|
+
id: "html-no-nested-interactive",
|
|
32279
|
+
source: "react-doctor",
|
|
32280
|
+
originallyExternal: false,
|
|
32281
|
+
rule: {
|
|
32282
|
+
...htmlNoNestedInteractive,
|
|
32283
|
+
framework: "global",
|
|
32284
|
+
category: "Correctness"
|
|
32285
|
+
}
|
|
32286
|
+
},
|
|
30833
32287
|
{
|
|
30834
32288
|
key: "react-doctor/iframe-has-title",
|
|
30835
32289
|
id: "iframe-has-title",
|
|
@@ -30874,6 +32328,50 @@ const reactDoctorRules = [
|
|
|
30874
32328
|
category: "Accessibility"
|
|
30875
32329
|
}
|
|
30876
32330
|
},
|
|
32331
|
+
{
|
|
32332
|
+
key: "react-doctor/jotai-derived-atom-returns-fresh-object",
|
|
32333
|
+
id: "jotai-derived-atom-returns-fresh-object",
|
|
32334
|
+
source: "react-doctor",
|
|
32335
|
+
originallyExternal: false,
|
|
32336
|
+
rule: {
|
|
32337
|
+
...jotaiDerivedAtomReturnsFreshObject,
|
|
32338
|
+
framework: "global",
|
|
32339
|
+
category: "State & Effects"
|
|
32340
|
+
}
|
|
32341
|
+
},
|
|
32342
|
+
{
|
|
32343
|
+
key: "react-doctor/jotai-select-atom-in-render-body",
|
|
32344
|
+
id: "jotai-select-atom-in-render-body",
|
|
32345
|
+
source: "react-doctor",
|
|
32346
|
+
originallyExternal: false,
|
|
32347
|
+
rule: {
|
|
32348
|
+
...jotaiSelectAtomInRenderBody,
|
|
32349
|
+
framework: "global",
|
|
32350
|
+
category: "State & Effects"
|
|
32351
|
+
}
|
|
32352
|
+
},
|
|
32353
|
+
{
|
|
32354
|
+
key: "react-doctor/jotai-tq-use-raw-query-atom",
|
|
32355
|
+
id: "jotai-tq-use-raw-query-atom",
|
|
32356
|
+
source: "react-doctor",
|
|
32357
|
+
originallyExternal: false,
|
|
32358
|
+
rule: {
|
|
32359
|
+
...jotaiTqUseRawQueryAtom,
|
|
32360
|
+
framework: "global",
|
|
32361
|
+
category: "State & Effects"
|
|
32362
|
+
}
|
|
32363
|
+
},
|
|
32364
|
+
{
|
|
32365
|
+
key: "react-doctor/js-async-reduce-without-awaited-acc",
|
|
32366
|
+
id: "js-async-reduce-without-awaited-acc",
|
|
32367
|
+
source: "react-doctor",
|
|
32368
|
+
originallyExternal: false,
|
|
32369
|
+
rule: {
|
|
32370
|
+
...jsAsyncReduceWithoutAwaitedAcc,
|
|
32371
|
+
framework: "global",
|
|
32372
|
+
category: "Performance"
|
|
32373
|
+
}
|
|
32374
|
+
},
|
|
30877
32375
|
{
|
|
30878
32376
|
key: "react-doctor/js-batch-dom-css",
|
|
30879
32377
|
id: "js-batch-dom-css",
|
|
@@ -32601,6 +34099,61 @@ const reactDoctorRules = [
|
|
|
32601
34099
|
category: "Architecture"
|
|
32602
34100
|
}
|
|
32603
34101
|
},
|
|
34102
|
+
{
|
|
34103
|
+
key: "react-doctor/preact-no-children-length",
|
|
34104
|
+
id: "preact-no-children-length",
|
|
34105
|
+
source: "react-doctor",
|
|
34106
|
+
originallyExternal: false,
|
|
34107
|
+
rule: {
|
|
34108
|
+
...preactNoChildrenLength,
|
|
34109
|
+
framework: "preact",
|
|
34110
|
+
category: "Preact"
|
|
34111
|
+
}
|
|
34112
|
+
},
|
|
34113
|
+
{
|
|
34114
|
+
key: "react-doctor/preact-no-react-hooks-import",
|
|
34115
|
+
id: "preact-no-react-hooks-import",
|
|
34116
|
+
source: "react-doctor",
|
|
34117
|
+
originallyExternal: false,
|
|
34118
|
+
rule: {
|
|
34119
|
+
...preactNoReactHooksImport,
|
|
34120
|
+
framework: "preact",
|
|
34121
|
+
category: "Preact"
|
|
34122
|
+
}
|
|
34123
|
+
},
|
|
34124
|
+
{
|
|
34125
|
+
key: "react-doctor/preact-no-render-arguments",
|
|
34126
|
+
id: "preact-no-render-arguments",
|
|
34127
|
+
source: "react-doctor",
|
|
34128
|
+
originallyExternal: false,
|
|
34129
|
+
rule: {
|
|
34130
|
+
...preactNoRenderArguments,
|
|
34131
|
+
framework: "preact",
|
|
34132
|
+
category: "Preact"
|
|
34133
|
+
}
|
|
34134
|
+
},
|
|
34135
|
+
{
|
|
34136
|
+
key: "react-doctor/preact-prefer-ondblclick",
|
|
34137
|
+
id: "preact-prefer-ondblclick",
|
|
34138
|
+
source: "react-doctor",
|
|
34139
|
+
originallyExternal: false,
|
|
34140
|
+
rule: {
|
|
34141
|
+
...preactPreferOndblclick,
|
|
34142
|
+
framework: "preact",
|
|
34143
|
+
category: "Preact"
|
|
34144
|
+
}
|
|
34145
|
+
},
|
|
34146
|
+
{
|
|
34147
|
+
key: "react-doctor/preact-prefer-oninput",
|
|
34148
|
+
id: "preact-prefer-oninput",
|
|
34149
|
+
source: "react-doctor",
|
|
34150
|
+
originallyExternal: false,
|
|
34151
|
+
rule: {
|
|
34152
|
+
...preactPreferOninput,
|
|
34153
|
+
framework: "preact",
|
|
34154
|
+
category: "Preact"
|
|
34155
|
+
}
|
|
34156
|
+
},
|
|
32604
34157
|
{
|
|
32605
34158
|
key: "react-doctor/prefer-dynamic-import",
|
|
32606
34159
|
id: "prefer-dynamic-import",
|
|
@@ -32634,6 +34187,17 @@ const reactDoctorRules = [
|
|
|
32634
34187
|
category: "Architecture"
|
|
32635
34188
|
}
|
|
32636
34189
|
},
|
|
34190
|
+
{
|
|
34191
|
+
key: "react-doctor/prefer-html-dialog",
|
|
34192
|
+
id: "prefer-html-dialog",
|
|
34193
|
+
source: "react-doctor",
|
|
34194
|
+
originallyExternal: false,
|
|
34195
|
+
rule: {
|
|
34196
|
+
...preferHtmlDialog,
|
|
34197
|
+
framework: "global",
|
|
34198
|
+
category: "Accessibility"
|
|
34199
|
+
}
|
|
34200
|
+
},
|
|
32637
34201
|
{
|
|
32638
34202
|
key: "react-doctor/prefer-tag-over-role",
|
|
32639
34203
|
id: "prefer-tag-over-role",
|
|
@@ -33035,6 +34599,18 @@ const reactDoctorRules = [
|
|
|
33035
34599
|
tags: [...new Set(["react-native", ...rnListDataMapped.tags ?? []])]
|
|
33036
34600
|
}
|
|
33037
34601
|
},
|
|
34602
|
+
{
|
|
34603
|
+
key: "react-doctor/rn-list-missing-estimated-item-size",
|
|
34604
|
+
id: "rn-list-missing-estimated-item-size",
|
|
34605
|
+
source: "react-doctor",
|
|
34606
|
+
originallyExternal: false,
|
|
34607
|
+
rule: {
|
|
34608
|
+
...rnListMissingEstimatedItemSize,
|
|
34609
|
+
framework: "react-native",
|
|
34610
|
+
category: "React Native",
|
|
34611
|
+
tags: [...new Set(["react-native", ...rnListMissingEstimatedItemSize.tags ?? []])]
|
|
34612
|
+
}
|
|
34613
|
+
},
|
|
33038
34614
|
{
|
|
33039
34615
|
key: "react-doctor/rn-list-recyclable-without-types",
|
|
33040
34616
|
id: "rn-list-recyclable-without-types",
|
|
@@ -33155,6 +34731,18 @@ const reactDoctorRules = [
|
|
|
33155
34731
|
tags: [...new Set(["react-native", ...rnNoRawText.tags ?? []])]
|
|
33156
34732
|
}
|
|
33157
34733
|
},
|
|
34734
|
+
{
|
|
34735
|
+
key: "react-doctor/rn-no-renderitem-key",
|
|
34736
|
+
id: "rn-no-renderitem-key",
|
|
34737
|
+
source: "react-doctor",
|
|
34738
|
+
originallyExternal: false,
|
|
34739
|
+
rule: {
|
|
34740
|
+
...rnNoRenderitemKey,
|
|
34741
|
+
framework: "react-native",
|
|
34742
|
+
category: "React Native",
|
|
34743
|
+
tags: [...new Set(["react-native", ...rnNoRenderitemKey.tags ?? []])]
|
|
34744
|
+
}
|
|
34745
|
+
},
|
|
33158
34746
|
{
|
|
33159
34747
|
key: "react-doctor/rn-no-scroll-state",
|
|
33160
34748
|
id: "rn-no-scroll-state",
|
|
@@ -33227,6 +34815,18 @@ const reactDoctorRules = [
|
|
|
33227
34815
|
tags: [...new Set(["react-native", ...rnPreferPressable.tags ?? []])]
|
|
33228
34816
|
}
|
|
33229
34817
|
},
|
|
34818
|
+
{
|
|
34819
|
+
key: "react-doctor/rn-prefer-pressable-over-gesture-detector",
|
|
34820
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
34821
|
+
source: "react-doctor",
|
|
34822
|
+
originallyExternal: false,
|
|
34823
|
+
rule: {
|
|
34824
|
+
...rnPreferPressableOverGestureDetector,
|
|
34825
|
+
framework: "react-native",
|
|
34826
|
+
category: "React Native",
|
|
34827
|
+
tags: [...new Set(["react-native", ...rnPreferPressableOverGestureDetector.tags ?? []])]
|
|
34828
|
+
}
|
|
34829
|
+
},
|
|
33230
34830
|
{
|
|
33231
34831
|
key: "react-doctor/rn-prefer-reanimated",
|
|
33232
34832
|
id: "rn-prefer-reanimated",
|
|
@@ -33263,6 +34863,18 @@ const reactDoctorRules = [
|
|
|
33263
34863
|
tags: [...new Set(["react-native", ...rnScrollviewDynamicPadding.tags ?? []])]
|
|
33264
34864
|
}
|
|
33265
34865
|
},
|
|
34866
|
+
{
|
|
34867
|
+
key: "react-doctor/rn-scrollview-flex-in-content-container",
|
|
34868
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
34869
|
+
source: "react-doctor",
|
|
34870
|
+
originallyExternal: false,
|
|
34871
|
+
rule: {
|
|
34872
|
+
...rnScrollviewFlexInContentContainer,
|
|
34873
|
+
framework: "react-native",
|
|
34874
|
+
category: "React Native",
|
|
34875
|
+
tags: [...new Set(["react-native", ...rnScrollviewFlexInContentContainer.tags ?? []])]
|
|
34876
|
+
}
|
|
34877
|
+
},
|
|
33266
34878
|
{
|
|
33267
34879
|
key: "react-doctor/rn-style-prefer-boxshadow",
|
|
33268
34880
|
id: "rn-style-prefer-boxshadow",
|
|
@@ -33696,7 +35308,7 @@ const appendNode = (builder, block, node) => {
|
|
|
33696
35308
|
};
|
|
33697
35309
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
33698
35310
|
builder.nodeBlock.set(node, block);
|
|
33699
|
-
if (isFunctionLike(node)) return;
|
|
35311
|
+
if (isFunctionLike$1(node)) return;
|
|
33700
35312
|
const record = node;
|
|
33701
35313
|
for (const key of Object.keys(record)) {
|
|
33702
35314
|
if (key === "parent") continue;
|
|
@@ -34034,7 +35646,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34034
35646
|
body: program.body
|
|
34035
35647
|
});
|
|
34036
35648
|
const visit = (node) => {
|
|
34037
|
-
if (isFunctionLike(node)) {
|
|
35649
|
+
if (isFunctionLike$1(node)) {
|
|
34038
35650
|
const body = node.body;
|
|
34039
35651
|
if (body) buildFor(node, body);
|
|
34040
35652
|
}
|
|
@@ -34051,7 +35663,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34051
35663
|
const enclosingFunction = (node) => {
|
|
34052
35664
|
let current = node;
|
|
34053
35665
|
while (current) {
|
|
34054
|
-
if (isFunctionLike(current)) return current;
|
|
35666
|
+
if (isFunctionLike$1(current)) return current;
|
|
34055
35667
|
if (isNodeOfType(current, "Program")) return current;
|
|
34056
35668
|
current = current.parent ?? null;
|
|
34057
35669
|
}
|
|
@@ -34273,6 +35885,7 @@ const NEXTJS_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramewor
|
|
|
34273
35885
|
const REACT_NATIVE_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("react-native")));
|
|
34274
35886
|
const TANSTACK_START_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-start")));
|
|
34275
35887
|
const TANSTACK_QUERY_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-query")));
|
|
35888
|
+
const PREACT_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("preact")));
|
|
34276
35889
|
const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES));
|
|
34277
35890
|
const ALL_REACT_DOCTOR_RULE_KEYS = new Set(REACT_DOCTOR_RULES.map((rule) => rule.key));
|
|
34278
35891
|
const FRAMEWORK_SPECIFIC_RULE_KEYS = collectFrameworkSpecificRuleKeys();
|
|
@@ -34281,6 +35894,6 @@ const REACT_COMPILER_RULES = toRuleMap(collectExternalRulesBySource("react-compi
|
|
|
34281
35894
|
//#region src/index.ts
|
|
34282
35895
|
var src_default = plugin;
|
|
34283
35896
|
//#endregion
|
|
34284
|
-
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, src_default as default, isReactNativeDependencyName };
|
|
35897
|
+
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, src_default as default, isReactNativeDependencyName };
|
|
34285
35898
|
|
|
34286
35899
|
//# sourceMappingURL=index.js.map
|