oxlint-plugin-react-doctor 0.2.9 → 0.2.11
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 +665 -11
- package/dist/index.js +2246 -629
- 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/",
|
|
@@ -481,7 +223,7 @@ const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
|
481
223
|
//#endregion
|
|
482
224
|
//#region src/plugin/utils/define-rule.ts
|
|
483
225
|
const wrapCreateForTestNoise = (create) => ((context) => {
|
|
484
|
-
if (isTestlikeFilename(context.
|
|
226
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
485
227
|
return create(context);
|
|
486
228
|
});
|
|
487
229
|
const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
|
|
@@ -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);
|
|
@@ -2983,7 +3127,7 @@ const asyncParallel = defineRule({
|
|
|
2983
3127
|
severity: "warn",
|
|
2984
3128
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
2985
3129
|
create: (context) => {
|
|
2986
|
-
const filename = normalizeFilename$1(context.
|
|
3130
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
2987
3131
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
2988
3132
|
let hasTestLibraryImport = false;
|
|
2989
3133
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -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
|
}
|
|
@@ -3186,7 +3330,7 @@ const buttonHasType = defineRule({
|
|
|
3186
3330
|
recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
|
|
3187
3331
|
create: (context) => {
|
|
3188
3332
|
const settings = resolveSettings$48(context.settings);
|
|
3189
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3333
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3190
3334
|
return {
|
|
3191
3335
|
JSXOpeningElement(node) {
|
|
3192
3336
|
if (isTestlikeFile) return;
|
|
@@ -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",
|
|
@@ -3386,7 +3530,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3386
3530
|
recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
|
|
3387
3531
|
category: "Accessibility",
|
|
3388
3532
|
create: (context) => {
|
|
3389
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3533
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3390
3534
|
return { JSXOpeningElement(node) {
|
|
3391
3535
|
if (isTestlikeFile) return;
|
|
3392
3536
|
const tag = getElementType(node, context.settings);
|
|
@@ -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"],
|
|
@@ -3547,7 +3791,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3547
3791
|
category: "Accessibility",
|
|
3548
3792
|
create: (context) => {
|
|
3549
3793
|
const settings = resolveSettings$46(context.settings);
|
|
3550
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3794
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3551
3795
|
return { JSXElement(node) {
|
|
3552
3796
|
if (isTestlikeFile) return;
|
|
3553
3797
|
const opening = node.openingElement;
|
|
@@ -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) {
|
|
@@ -7527,6 +8423,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
7527
8423
|
id: "js-tosorted-immutable",
|
|
7528
8424
|
tags: ["test-noise"],
|
|
7529
8425
|
severity: "warn",
|
|
8426
|
+
disabledBy: ["react-native"],
|
|
7530
8427
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
7531
8428
|
create: (context) => ({ CallExpression(node) {
|
|
7532
8429
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -7815,7 +8712,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
7815
8712
|
const settings = resolveSettings$34(context.settings);
|
|
7816
8713
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
7817
8714
|
const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
|
|
7818
|
-
const filename =
|
|
8715
|
+
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
7819
8716
|
const extensionOnly = path.extname(filename).slice(1);
|
|
7820
8717
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
7821
8718
|
let didReportMismatch = false;
|
|
@@ -8440,33 +9337,8 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
8440
9337
|
return best;
|
|
8441
9338
|
};
|
|
8442
9339
|
//#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
9340
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
8469
|
-
const buildMessage$
|
|
9341
|
+
const buildMessage$17 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
8470
9342
|
const DEFAULT_MAX_DEPTH = 14;
|
|
8471
9343
|
const resolveSettings$30 = (settings) => {
|
|
8472
9344
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -8533,7 +9405,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8533
9405
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
8534
9406
|
if (total > max) context.report({
|
|
8535
9407
|
node,
|
|
8536
|
-
message: buildMessage$
|
|
9408
|
+
message: buildMessage$17(total, max)
|
|
8537
9409
|
});
|
|
8538
9410
|
};
|
|
8539
9411
|
return {
|
|
@@ -8548,7 +9420,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8548
9420
|
});
|
|
8549
9421
|
//#endregion
|
|
8550
9422
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
8551
|
-
const MESSAGE$
|
|
9423
|
+
const MESSAGE$37 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
|
|
8552
9424
|
const LITERAL_TEXT_TAGS = new Set([
|
|
8553
9425
|
"code",
|
|
8554
9426
|
"pre",
|
|
@@ -8583,7 +9455,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
8583
9455
|
if (isInsideLiteralTextTag(node)) return;
|
|
8584
9456
|
context.report({
|
|
8585
9457
|
node,
|
|
8586
|
-
message: MESSAGE$
|
|
9458
|
+
message: MESSAGE$37
|
|
8587
9459
|
});
|
|
8588
9460
|
} })
|
|
8589
9461
|
});
|
|
@@ -8605,7 +9477,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
8605
9477
|
};
|
|
8606
9478
|
//#endregion
|
|
8607
9479
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
8608
|
-
const MESSAGE$
|
|
9480
|
+
const MESSAGE$36 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
|
|
8609
9481
|
const isConstructedValue = (expression) => {
|
|
8610
9482
|
const stripped = stripParenExpression(expression);
|
|
8611
9483
|
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;
|
|
@@ -8624,7 +9496,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
8624
9496
|
recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
|
|
8625
9497
|
category: "Performance",
|
|
8626
9498
|
create: (context) => {
|
|
8627
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9499
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
8628
9500
|
return { JSXOpeningElement(node) {
|
|
8629
9501
|
if (isTestlikeFile) return;
|
|
8630
9502
|
if (!isProviderName(node.name)) return;
|
|
@@ -8641,7 +9513,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
8641
9513
|
if (!isConstructedValue(innerExpression)) continue;
|
|
8642
9514
|
context.report({
|
|
8643
9515
|
node: attribute,
|
|
8644
|
-
message: MESSAGE$
|
|
9516
|
+
message: MESSAGE$36
|
|
8645
9517
|
});
|
|
8646
9518
|
}
|
|
8647
9519
|
} };
|
|
@@ -8724,7 +9596,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
8724
9596
|
};
|
|
8725
9597
|
//#endregion
|
|
8726
9598
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
8727
|
-
const MESSAGE$
|
|
9599
|
+
const MESSAGE$35 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
|
|
8728
9600
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
8729
9601
|
"icon",
|
|
8730
9602
|
"Icon",
|
|
@@ -8970,7 +9842,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
8970
9842
|
recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
|
|
8971
9843
|
category: "Performance",
|
|
8972
9844
|
create: (context) => {
|
|
8973
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9845
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
8974
9846
|
let memoRegistry = null;
|
|
8975
9847
|
return {
|
|
8976
9848
|
Program(node) {
|
|
@@ -8992,7 +9864,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
8992
9864
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
8993
9865
|
context.report({
|
|
8994
9866
|
node,
|
|
8995
|
-
message: MESSAGE$
|
|
9867
|
+
message: MESSAGE$35
|
|
8996
9868
|
});
|
|
8997
9869
|
}
|
|
8998
9870
|
};
|
|
@@ -9280,7 +10152,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
9280
10152
|
];
|
|
9281
10153
|
//#endregion
|
|
9282
10154
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
9283
|
-
const MESSAGE$
|
|
10155
|
+
const MESSAGE$34 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
|
|
9284
10156
|
const isDataArrayPropName = (propName) => {
|
|
9285
10157
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
9286
10158
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -9341,7 +10213,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
9341
10213
|
recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
|
|
9342
10214
|
category: "Performance",
|
|
9343
10215
|
create: (context) => {
|
|
9344
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10216
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9345
10217
|
let memoRegistry = null;
|
|
9346
10218
|
return {
|
|
9347
10219
|
Program(node) {
|
|
@@ -9363,7 +10235,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
9363
10235
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
9364
10236
|
context.report({
|
|
9365
10237
|
node,
|
|
9366
|
-
message: MESSAGE$
|
|
10238
|
+
message: MESSAGE$34
|
|
9367
10239
|
});
|
|
9368
10240
|
}
|
|
9369
10241
|
};
|
|
@@ -9621,7 +10493,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
9621
10493
|
]);
|
|
9622
10494
|
//#endregion
|
|
9623
10495
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
9624
|
-
const MESSAGE$
|
|
10496
|
+
const MESSAGE$33 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
|
|
9625
10497
|
const isAccessorPredicateName = (propName) => {
|
|
9626
10498
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
9627
10499
|
if (propName.length <= prefix.length) continue;
|
|
@@ -9803,7 +10675,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
9803
10675
|
recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
|
|
9804
10676
|
category: "Performance",
|
|
9805
10677
|
create: (context) => {
|
|
9806
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10678
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9807
10679
|
let memoRegistry = null;
|
|
9808
10680
|
return {
|
|
9809
10681
|
Program(node) {
|
|
@@ -9826,7 +10698,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
9826
10698
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
9827
10699
|
context.report({
|
|
9828
10700
|
node,
|
|
9829
|
-
message: MESSAGE$
|
|
10701
|
+
message: MESSAGE$33
|
|
9830
10702
|
});
|
|
9831
10703
|
}
|
|
9832
10704
|
};
|
|
@@ -10046,7 +10918,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
10046
10918
|
];
|
|
10047
10919
|
//#endregion
|
|
10048
10920
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
10049
|
-
const MESSAGE$
|
|
10921
|
+
const MESSAGE$32 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
|
|
10050
10922
|
const isConfigObjectPropName = (propName) => {
|
|
10051
10923
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
10052
10924
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10109,7 +10981,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10109
10981
|
recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
|
|
10110
10982
|
category: "Performance",
|
|
10111
10983
|
create: (context) => {
|
|
10112
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10984
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10113
10985
|
let memoRegistry = null;
|
|
10114
10986
|
return {
|
|
10115
10987
|
Program(node) {
|
|
@@ -10133,7 +11005,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10133
11005
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
10134
11006
|
context.report({
|
|
10135
11007
|
node,
|
|
10136
|
-
message: MESSAGE$
|
|
11008
|
+
message: MESSAGE$32
|
|
10137
11009
|
});
|
|
10138
11010
|
}
|
|
10139
11011
|
};
|
|
@@ -10141,7 +11013,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10141
11013
|
});
|
|
10142
11014
|
//#endregion
|
|
10143
11015
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
10144
|
-
const MESSAGE$
|
|
11016
|
+
const MESSAGE$31 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
|
|
10145
11017
|
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
11018
|
const resolveSettings$29 = (settings) => {
|
|
10147
11019
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -10181,7 +11053,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
10181
11053
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
10182
11054
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
10183
11055
|
node: attribute,
|
|
10184
|
-
message: MESSAGE$
|
|
11056
|
+
message: MESSAGE$31
|
|
10185
11057
|
});
|
|
10186
11058
|
}
|
|
10187
11059
|
} };
|
|
@@ -10469,7 +11341,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
10469
11341
|
});
|
|
10470
11342
|
//#endregion
|
|
10471
11343
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
10472
|
-
const buildMessage$
|
|
11344
|
+
const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
10473
11345
|
const KNOWN_GLOBALS = new Set([
|
|
10474
11346
|
"globalThis",
|
|
10475
11347
|
"window",
|
|
@@ -10504,7 +11376,7 @@ const jsxNoUndef = defineRule({
|
|
|
10504
11376
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
10505
11377
|
context.report({
|
|
10506
11378
|
node: node.name,
|
|
10507
|
-
message: buildMessage$
|
|
11379
|
+
message: buildMessage$16(rootIdentifier)
|
|
10508
11380
|
});
|
|
10509
11381
|
} })
|
|
10510
11382
|
});
|
|
@@ -10603,7 +11475,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
10603
11475
|
});
|
|
10604
11476
|
//#endregion
|
|
10605
11477
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
10606
|
-
const buildMessage$
|
|
11478
|
+
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
11479
|
const resolveSettings$26 = (settings) => {
|
|
10608
11480
|
const reactDoctor = settings?.["react-doctor"];
|
|
10609
11481
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -10719,7 +11591,7 @@ const jsxPascalCase = defineRule({
|
|
|
10719
11591
|
if (!isPascal && !isAllCaps) {
|
|
10720
11592
|
context.report({
|
|
10721
11593
|
node,
|
|
10722
|
-
message: buildMessage$
|
|
11594
|
+
message: buildMessage$15(segment, settings.allowAllCaps)
|
|
10723
11595
|
});
|
|
10724
11596
|
return;
|
|
10725
11597
|
}
|
|
@@ -10771,7 +11643,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
10771
11643
|
});
|
|
10772
11644
|
//#endregion
|
|
10773
11645
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
10774
|
-
const MESSAGE$
|
|
11646
|
+
const MESSAGE$30 = "JSX prop spreading is forbidden — list each prop explicitly.";
|
|
10775
11647
|
const resolveSettings$25 = (settings) => {
|
|
10776
11648
|
const reactDoctor = settings?.["react-doctor"];
|
|
10777
11649
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -10811,7 +11683,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
10811
11683
|
}
|
|
10812
11684
|
context.report({
|
|
10813
11685
|
node: attribute,
|
|
10814
|
-
message: MESSAGE$
|
|
11686
|
+
message: MESSAGE$30
|
|
10815
11687
|
});
|
|
10816
11688
|
}
|
|
10817
11689
|
} };
|
|
@@ -10914,7 +11786,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
10914
11786
|
category: "Accessibility",
|
|
10915
11787
|
create: (context) => {
|
|
10916
11788
|
const settings = resolveSettings$24(context.settings);
|
|
10917
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11789
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10918
11790
|
return { JSXElement(node) {
|
|
10919
11791
|
if (isTestlikeFile) return;
|
|
10920
11792
|
const opening = node.openingElement;
|
|
@@ -10966,7 +11838,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
10966
11838
|
});
|
|
10967
11839
|
//#endregion
|
|
10968
11840
|
//#region src/plugin/rules/a11y/lang.ts
|
|
10969
|
-
const MESSAGE$
|
|
11841
|
+
const MESSAGE$29 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
|
|
10970
11842
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
10971
11843
|
"aa",
|
|
10972
11844
|
"ab",
|
|
@@ -11177,7 +12049,7 @@ const lang = defineRule({
|
|
|
11177
12049
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
11178
12050
|
context.report({
|
|
11179
12051
|
node: langAttr,
|
|
11180
|
-
message: MESSAGE$
|
|
12052
|
+
message: MESSAGE$29
|
|
11181
12053
|
});
|
|
11182
12054
|
return;
|
|
11183
12055
|
}
|
|
@@ -11186,13 +12058,13 @@ const lang = defineRule({
|
|
|
11186
12058
|
if (value === null) return;
|
|
11187
12059
|
if (!isValidLangTag(value)) context.report({
|
|
11188
12060
|
node: langAttr,
|
|
11189
|
-
message: MESSAGE$
|
|
12061
|
+
message: MESSAGE$29
|
|
11190
12062
|
});
|
|
11191
12063
|
} })
|
|
11192
12064
|
});
|
|
11193
12065
|
//#endregion
|
|
11194
12066
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
11195
|
-
const MESSAGE$
|
|
12067
|
+
const MESSAGE$28 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
|
|
11196
12068
|
const DEFAULT_AUDIO = ["audio"];
|
|
11197
12069
|
const DEFAULT_VIDEO = ["video"];
|
|
11198
12070
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -11232,7 +12104,7 @@ const mediaHasCaption = defineRule({
|
|
|
11232
12104
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
11233
12105
|
context.report({
|
|
11234
12106
|
node: node.name,
|
|
11235
|
-
message: MESSAGE$
|
|
12107
|
+
message: MESSAGE$28
|
|
11236
12108
|
});
|
|
11237
12109
|
return;
|
|
11238
12110
|
}
|
|
@@ -11249,7 +12121,7 @@ const mediaHasCaption = defineRule({
|
|
|
11249
12121
|
return kindValue.value.toLowerCase() === "captions";
|
|
11250
12122
|
})) context.report({
|
|
11251
12123
|
node: node.name,
|
|
11252
|
-
message: MESSAGE$
|
|
12124
|
+
message: MESSAGE$28
|
|
11253
12125
|
});
|
|
11254
12126
|
} };
|
|
11255
12127
|
}
|
|
@@ -11452,7 +12324,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
11452
12324
|
severity: "warn",
|
|
11453
12325
|
recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
11454
12326
|
create: (context) => ({ Program(programNode) {
|
|
11455
|
-
const filename = normalizeFilename$1(context.
|
|
12327
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11456
12328
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
11457
12329
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
11458
12330
|
if (!programNode.body?.some((statement) => {
|
|
@@ -11517,7 +12389,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
11517
12389
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
11518
12390
|
const callback = getEffectCallback(node);
|
|
11519
12391
|
if (!callback || !containsFetchCall(callback)) return;
|
|
11520
|
-
const filename = normalizeFilename$1(context.
|
|
12392
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11521
12393
|
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
|
|
11522
12394
|
node,
|
|
11523
12395
|
message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
|
|
@@ -11550,7 +12422,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
11550
12422
|
severity: "warn",
|
|
11551
12423
|
recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
|
|
11552
12424
|
create: (context) => {
|
|
11553
|
-
const filename = normalizeFilename$1(context.
|
|
12425
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11554
12426
|
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
11555
12427
|
return { CallExpression(node) {
|
|
11556
12428
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -11619,7 +12491,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
11619
12491
|
recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
11620
12492
|
create: (context) => ({ ImportDeclaration(node) {
|
|
11621
12493
|
if (node.source?.value !== "next/head") return;
|
|
11622
|
-
const filename = normalizeFilename$1(context.
|
|
12494
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11623
12495
|
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
11624
12496
|
context.report({
|
|
11625
12497
|
node,
|
|
@@ -11636,7 +12508,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
11636
12508
|
severity: "warn",
|
|
11637
12509
|
recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
11638
12510
|
create: (context) => {
|
|
11639
|
-
const filename = normalizeFilename$1(context.
|
|
12511
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11640
12512
|
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
11641
12513
|
return { JSXOpeningElement(node) {
|
|
11642
12514
|
if (isOgRoute) return;
|
|
@@ -11940,7 +12812,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
11940
12812
|
};
|
|
11941
12813
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
11942
12814
|
if (remainingDepth <= 0) return [];
|
|
11943
|
-
if (isFunctionLike(expression)) return expression.body ? [expression.body] : [];
|
|
12815
|
+
if (isFunctionLike$1(expression)) return expression.body ? [expression.body] : [];
|
|
11944
12816
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
11945
12817
|
for (const callArgument of expression.arguments ?? []) {
|
|
11946
12818
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -11990,7 +12862,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
11990
12862
|
resolveBinding = buildProgramBindingLookup(node);
|
|
11991
12863
|
},
|
|
11992
12864
|
ExportNamedDeclaration(node) {
|
|
11993
|
-
const filename = normalizeFilename$1(context.
|
|
12865
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11994
12866
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
11995
12867
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
11996
12868
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -12020,14 +12892,6 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12020
12892
|
}
|
|
12021
12893
|
});
|
|
12022
12894
|
//#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
12895
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
12032
12896
|
const fileMentionsSuspense = (programNode) => {
|
|
12033
12897
|
let didSee = false;
|
|
@@ -12038,7 +12902,7 @@ const fileMentionsSuspense = (programNode) => {
|
|
|
12038
12902
|
return false;
|
|
12039
12903
|
}
|
|
12040
12904
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
12041
|
-
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense")) {
|
|
12905
|
+
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
|
|
12042
12906
|
didSee = true;
|
|
12043
12907
|
return false;
|
|
12044
12908
|
}
|
|
@@ -12071,7 +12935,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
12071
12935
|
});
|
|
12072
12936
|
//#endregion
|
|
12073
12937
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
12074
|
-
const MESSAGE$
|
|
12938
|
+
const MESSAGE$27 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
|
|
12075
12939
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
12076
12940
|
const noAccessKey = defineRule({
|
|
12077
12941
|
id: "no-access-key",
|
|
@@ -12087,7 +12951,7 @@ const noAccessKey = defineRule({
|
|
|
12087
12951
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
12088
12952
|
context.report({
|
|
12089
12953
|
node: accessKey,
|
|
12090
|
-
message: MESSAGE$
|
|
12954
|
+
message: MESSAGE$27
|
|
12091
12955
|
});
|
|
12092
12956
|
return;
|
|
12093
12957
|
}
|
|
@@ -12097,7 +12961,7 @@ const noAccessKey = defineRule({
|
|
|
12097
12961
|
if (isUndefinedIdentifier(expression)) return;
|
|
12098
12962
|
context.report({
|
|
12099
12963
|
node: accessKey,
|
|
12100
|
-
message: MESSAGE$
|
|
12964
|
+
message: MESSAGE$27
|
|
12101
12965
|
});
|
|
12102
12966
|
}
|
|
12103
12967
|
} })
|
|
@@ -12406,7 +13270,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
12406
13270
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
12407
13271
|
if (isNodeOfType(fn, "Identifier")) {
|
|
12408
13272
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
12409
|
-
if (definitionNode && isFunctionLike(definitionNode)) return definitionNode;
|
|
13273
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return definitionNode;
|
|
12410
13274
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12411
13275
|
const initializer = definitionNode.init;
|
|
12412
13276
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -12499,14 +13363,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
12499
13363
|
return node ?? null;
|
|
12500
13364
|
};
|
|
12501
13365
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
12502
|
-
if (isFunctionLike(node)) return true;
|
|
13366
|
+
if (isFunctionLike$1(node)) return true;
|
|
12503
13367
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
12504
13368
|
if (isNodeOfType(node, "Identifier")) {
|
|
12505
13369
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
12506
|
-
if (definitionNode && isFunctionLike(definitionNode)) return true;
|
|
13370
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return true;
|
|
12507
13371
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12508
13372
|
const initializer = definitionNode.init;
|
|
12509
|
-
return isFunctionLike(initializer);
|
|
13373
|
+
return isFunctionLike$1(initializer);
|
|
12510
13374
|
}
|
|
12511
13375
|
}
|
|
12512
13376
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -12516,7 +13380,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12516
13380
|
if (visited.has(node)) return false;
|
|
12517
13381
|
visited.add(node);
|
|
12518
13382
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
12519
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike(node)) return false;
|
|
13383
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
|
|
12520
13384
|
const record = node;
|
|
12521
13385
|
for (const [key, value] of Object.entries(record)) {
|
|
12522
13386
|
if (key === "parent") continue;
|
|
@@ -12528,7 +13392,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12528
13392
|
};
|
|
12529
13393
|
const hasCleanup = (analysis, node) => {
|
|
12530
13394
|
const fn = getEffectFn(analysis, node);
|
|
12531
|
-
if (!isFunctionLike(fn)) return false;
|
|
13395
|
+
if (!isFunctionLike$1(fn)) return false;
|
|
12532
13396
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
12533
13397
|
return hasCleanupReturn(analysis, fn.body);
|
|
12534
13398
|
};
|
|
@@ -12570,7 +13434,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
12570
13434
|
});
|
|
12571
13435
|
//#endregion
|
|
12572
13436
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
12573
|
-
const MESSAGE$
|
|
13437
|
+
const MESSAGE$26 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
|
|
12574
13438
|
const noAriaHiddenOnFocusable = defineRule({
|
|
12575
13439
|
id: "no-aria-hidden-on-focusable",
|
|
12576
13440
|
tags: ["react-jsx-only"],
|
|
@@ -12596,7 +13460,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
12596
13460
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
12597
13461
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
12598
13462
|
node: ariaHidden,
|
|
12599
|
-
message: MESSAGE$
|
|
13463
|
+
message: MESSAGE$26
|
|
12600
13464
|
});
|
|
12601
13465
|
} })
|
|
12602
13466
|
});
|
|
@@ -12876,7 +13740,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
12876
13740
|
let current = node;
|
|
12877
13741
|
while (current.parent) {
|
|
12878
13742
|
const parent = current.parent;
|
|
12879
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13743
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12880
13744
|
const callee = parent.callee;
|
|
12881
13745
|
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
13746
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -12895,7 +13759,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
12895
13759
|
let current = node;
|
|
12896
13760
|
while (current.parent) {
|
|
12897
13761
|
const parent = current.parent;
|
|
12898
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13762
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12899
13763
|
const callee = parent.callee;
|
|
12900
13764
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
12901
13765
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -12963,7 +13827,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
12963
13827
|
});
|
|
12964
13828
|
//#endregion
|
|
12965
13829
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
12966
|
-
const MESSAGE$
|
|
13830
|
+
const MESSAGE$25 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
|
|
12967
13831
|
const SECOND_INDEX_METHODS = new Set([
|
|
12968
13832
|
"every",
|
|
12969
13833
|
"filter",
|
|
@@ -13165,7 +14029,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13165
14029
|
}
|
|
13166
14030
|
context.report({
|
|
13167
14031
|
node: keyAttribute,
|
|
13168
|
-
message: MESSAGE$
|
|
14032
|
+
message: MESSAGE$25
|
|
13169
14033
|
});
|
|
13170
14034
|
},
|
|
13171
14035
|
CallExpression(node) {
|
|
@@ -13185,7 +14049,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13185
14049
|
if (propName !== "key") continue;
|
|
13186
14050
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
13187
14051
|
node: property,
|
|
13188
|
-
message: MESSAGE$
|
|
14052
|
+
message: MESSAGE$25
|
|
13189
14053
|
});
|
|
13190
14054
|
}
|
|
13191
14055
|
}
|
|
@@ -13193,7 +14057,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13193
14057
|
});
|
|
13194
14058
|
//#endregion
|
|
13195
14059
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
13196
|
-
const MESSAGE$
|
|
14060
|
+
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
14061
|
const resolveSettings$21 = (settings) => {
|
|
13198
14062
|
const reactDoctor = settings?.["react-doctor"];
|
|
13199
14063
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -13230,7 +14094,7 @@ const noAutofocus = defineRule({
|
|
|
13230
14094
|
category: "Accessibility",
|
|
13231
14095
|
create: (context) => {
|
|
13232
14096
|
const settings = resolveSettings$21(context.settings);
|
|
13233
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
14097
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
13234
14098
|
return { JSXOpeningElement(node) {
|
|
13235
14099
|
if (isTestlikeFile) return;
|
|
13236
14100
|
const autoFocusAttribute = node.attributes.find((attribute) => {
|
|
@@ -13248,7 +14112,7 @@ const noAutofocus = defineRule({
|
|
|
13248
14112
|
}
|
|
13249
14113
|
context.report({
|
|
13250
14114
|
node: autoFocusAttribute,
|
|
13251
|
-
message: MESSAGE$
|
|
14115
|
+
message: MESSAGE$24
|
|
13252
14116
|
});
|
|
13253
14117
|
} };
|
|
13254
14118
|
}
|
|
@@ -13604,7 +14468,7 @@ const noBarrelImport = defineRule({
|
|
|
13604
14468
|
if (didReportForFile) return;
|
|
13605
14469
|
const source = node.source?.value;
|
|
13606
14470
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
13607
|
-
const filename = normalizeFilename$1(context.
|
|
14471
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
13608
14472
|
if (!filename) return;
|
|
13609
14473
|
const importRequests = getRuntimeImportRequests(node);
|
|
13610
14474
|
if (importRequests.length === 0) return;
|
|
@@ -13739,7 +14603,7 @@ const noChainStateUpdates = defineRule({
|
|
|
13739
14603
|
});
|
|
13740
14604
|
//#endregion
|
|
13741
14605
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
13742
|
-
const MESSAGE$
|
|
14606
|
+
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
14607
|
const noChildrenProp = defineRule({
|
|
13744
14608
|
id: "no-children-prop",
|
|
13745
14609
|
severity: "warn",
|
|
@@ -13750,7 +14614,7 @@ const noChildrenProp = defineRule({
|
|
|
13750
14614
|
if (node.name.name !== "children") return;
|
|
13751
14615
|
context.report({
|
|
13752
14616
|
node: node.name,
|
|
13753
|
-
message: MESSAGE$
|
|
14617
|
+
message: MESSAGE$23
|
|
13754
14618
|
});
|
|
13755
14619
|
},
|
|
13756
14620
|
CallExpression(node) {
|
|
@@ -13763,90 +14627,15 @@ const noChildrenProp = defineRule({
|
|
|
13763
14627
|
const propertyKey = property.key;
|
|
13764
14628
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
13765
14629
|
node: propertyKey,
|
|
13766
|
-
message: MESSAGE$
|
|
14630
|
+
message: MESSAGE$23
|
|
13767
14631
|
});
|
|
13768
14632
|
}
|
|
13769
14633
|
}
|
|
13770
14634
|
})
|
|
13771
14635
|
});
|
|
13772
14636
|
//#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
14637
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
13849
|
-
const MESSAGE$
|
|
14638
|
+
const MESSAGE$22 = "`React.cloneElement` is uncommon and leads to fragile components.";
|
|
13850
14639
|
const noCloneElement = defineRule({
|
|
13851
14640
|
id: "no-clone-element",
|
|
13852
14641
|
severity: "warn",
|
|
@@ -13858,7 +14647,7 @@ const noCloneElement = defineRule({
|
|
|
13858
14647
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
13859
14648
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
13860
14649
|
node: callee,
|
|
13861
|
-
message: MESSAGE$
|
|
14650
|
+
message: MESSAGE$22
|
|
13862
14651
|
});
|
|
13863
14652
|
return;
|
|
13864
14653
|
}
|
|
@@ -13871,14 +14660,14 @@ const noCloneElement = defineRule({
|
|
|
13871
14660
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
13872
14661
|
context.report({
|
|
13873
14662
|
node: callee,
|
|
13874
|
-
message: MESSAGE$
|
|
14663
|
+
message: MESSAGE$22
|
|
13875
14664
|
});
|
|
13876
14665
|
}
|
|
13877
14666
|
} })
|
|
13878
14667
|
});
|
|
13879
14668
|
//#endregion
|
|
13880
14669
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
13881
|
-
const MESSAGE$
|
|
14670
|
+
const MESSAGE$21 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
|
|
13882
14671
|
const noDanger = defineRule({
|
|
13883
14672
|
id: "no-danger",
|
|
13884
14673
|
severity: "warn",
|
|
@@ -13889,7 +14678,7 @@ const noDanger = defineRule({
|
|
|
13889
14678
|
if (!propAttribute) return;
|
|
13890
14679
|
context.report({
|
|
13891
14680
|
node: propAttribute.name,
|
|
13892
|
-
message: MESSAGE$
|
|
14681
|
+
message: MESSAGE$21
|
|
13893
14682
|
});
|
|
13894
14683
|
},
|
|
13895
14684
|
CallExpression(node) {
|
|
@@ -13901,7 +14690,7 @@ const noDanger = defineRule({
|
|
|
13901
14690
|
const propertyKey = property.key;
|
|
13902
14691
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
13903
14692
|
node: propertyKey,
|
|
13904
|
-
message: MESSAGE$
|
|
14693
|
+
message: MESSAGE$21
|
|
13905
14694
|
});
|
|
13906
14695
|
}
|
|
13907
14696
|
}
|
|
@@ -13909,7 +14698,7 @@ const noDanger = defineRule({
|
|
|
13909
14698
|
});
|
|
13910
14699
|
//#endregion
|
|
13911
14700
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
13912
|
-
const MESSAGE$
|
|
14701
|
+
const MESSAGE$20 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
|
|
13913
14702
|
const isLineBreak = (child) => {
|
|
13914
14703
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
13915
14704
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -13978,7 +14767,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13978
14767
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
13979
14768
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
13980
14769
|
node: opening,
|
|
13981
|
-
message: MESSAGE$
|
|
14770
|
+
message: MESSAGE$20
|
|
13982
14771
|
});
|
|
13983
14772
|
},
|
|
13984
14773
|
CallExpression(node) {
|
|
@@ -13990,7 +14779,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13990
14779
|
if (!propsShape.hasDangerously) return;
|
|
13991
14780
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
13992
14781
|
node,
|
|
13993
|
-
message: MESSAGE$
|
|
14782
|
+
message: MESSAGE$20
|
|
13994
14783
|
});
|
|
13995
14784
|
}
|
|
13996
14785
|
})
|
|
@@ -14365,7 +15154,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
14365
15154
|
};
|
|
14366
15155
|
const getInlineFunctionNode = (node) => {
|
|
14367
15156
|
if (!node) return null;
|
|
14368
|
-
if (isFunctionLike(node)) return node;
|
|
15157
|
+
if (isFunctionLike$1(node)) return node;
|
|
14369
15158
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
14370
15159
|
for (const argument of node.arguments ?? []) {
|
|
14371
15160
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -14376,7 +15165,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
14376
15165
|
const getNearestComponentFunction = (node) => {
|
|
14377
15166
|
let cursor = node.parent ?? null;
|
|
14378
15167
|
while (cursor) {
|
|
14379
|
-
if (isFunctionLike(cursor)) return cursor;
|
|
15168
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
14380
15169
|
cursor = cursor.parent ?? null;
|
|
14381
15170
|
}
|
|
14382
15171
|
return null;
|
|
@@ -14557,7 +15346,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
14557
15346
|
//#endregion
|
|
14558
15347
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
14559
15348
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
14560
|
-
const MESSAGE$
|
|
15349
|
+
const MESSAGE$19 = "Do not use `this.setState` in `componentDidMount`.";
|
|
14561
15350
|
const resolveSettings$20 = (settings) => {
|
|
14562
15351
|
const reactDoctor = settings?.["react-doctor"];
|
|
14563
15352
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14575,7 +15364,7 @@ const noDidMountSetState = defineRule({
|
|
|
14575
15364
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14576
15365
|
context.report({
|
|
14577
15366
|
node: node.callee,
|
|
14578
|
-
message: MESSAGE$
|
|
15367
|
+
message: MESSAGE$19
|
|
14579
15368
|
});
|
|
14580
15369
|
} };
|
|
14581
15370
|
}
|
|
@@ -14583,7 +15372,7 @@ const noDidMountSetState = defineRule({
|
|
|
14583
15372
|
//#endregion
|
|
14584
15373
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
14585
15374
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
14586
|
-
const MESSAGE$
|
|
15375
|
+
const MESSAGE$18 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
|
|
14587
15376
|
const resolveSettings$19 = (settings) => {
|
|
14588
15377
|
const reactDoctor = settings?.["react-doctor"];
|
|
14589
15378
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14601,7 +15390,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
14601
15390
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14602
15391
|
context.report({
|
|
14603
15392
|
node: node.callee,
|
|
14604
|
-
message: MESSAGE$
|
|
15393
|
+
message: MESSAGE$18
|
|
14605
15394
|
});
|
|
14606
15395
|
} };
|
|
14607
15396
|
}
|
|
@@ -14624,7 +15413,7 @@ const isStateMemberExpression = (node) => {
|
|
|
14624
15413
|
};
|
|
14625
15414
|
//#endregion
|
|
14626
15415
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
14627
|
-
const MESSAGE$
|
|
15416
|
+
const MESSAGE$17 = "Never mutate `this.state` directly.";
|
|
14628
15417
|
const shouldIgnoreMutation = (node) => {
|
|
14629
15418
|
let isConstructor = false;
|
|
14630
15419
|
let isInsideCallExpression = false;
|
|
@@ -14646,7 +15435,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
14646
15435
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
14647
15436
|
context.report({
|
|
14648
15437
|
node: reportNode,
|
|
14649
|
-
message: MESSAGE$
|
|
15438
|
+
message: MESSAGE$17
|
|
14650
15439
|
});
|
|
14651
15440
|
};
|
|
14652
15441
|
const noDirectMutationState = defineRule({
|
|
@@ -14702,7 +15491,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
14702
15491
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
14703
15492
|
if (!node || typeof node !== "object") return;
|
|
14704
15493
|
let nextShadowedStateNames = shadowedStateNames;
|
|
14705
|
-
if (isFunctionLike(node)) {
|
|
15494
|
+
if (isFunctionLike$1(node)) {
|
|
14706
15495
|
const localBindings = collectFunctionLocalBindings(node);
|
|
14707
15496
|
if (localBindings.size > 0) {
|
|
14708
15497
|
const merged = new Set(shadowedStateNames);
|
|
@@ -14809,7 +15598,7 @@ const noDisabledZoom = defineRule({
|
|
|
14809
15598
|
});
|
|
14810
15599
|
//#endregion
|
|
14811
15600
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
14812
|
-
const buildMessage$
|
|
15601
|
+
const buildMessage$14 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
14813
15602
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
14814
15603
|
const resolveSettings$18 = (settings) => {
|
|
14815
15604
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -14829,7 +15618,7 @@ const noDistractingElements = defineRule({
|
|
|
14829
15618
|
const tag = getElementType(node, context.settings);
|
|
14830
15619
|
if (distractingTags.has(tag)) context.report({
|
|
14831
15620
|
node: node.name,
|
|
14832
|
-
message: buildMessage$
|
|
15621
|
+
message: buildMessage$14(tag)
|
|
14833
15622
|
});
|
|
14834
15623
|
} };
|
|
14835
15624
|
}
|
|
@@ -16159,7 +16948,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
16159
16948
|
"ReactDOM",
|
|
16160
16949
|
"ReactDom"
|
|
16161
16950
|
]);
|
|
16162
|
-
const MESSAGE$
|
|
16951
|
+
const MESSAGE$16 = "Unexpected call to `findDOMNode` — removed in React 19.";
|
|
16163
16952
|
const noFindDomNode = defineRule({
|
|
16164
16953
|
id: "no-find-dom-node",
|
|
16165
16954
|
severity: "warn",
|
|
@@ -16169,7 +16958,7 @@ const noFindDomNode = defineRule({
|
|
|
16169
16958
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
16170
16959
|
context.report({
|
|
16171
16960
|
node: callee,
|
|
16172
|
-
message: MESSAGE$
|
|
16961
|
+
message: MESSAGE$16
|
|
16173
16962
|
});
|
|
16174
16963
|
return;
|
|
16175
16964
|
}
|
|
@@ -16180,7 +16969,7 @@ const noFindDomNode = defineRule({
|
|
|
16180
16969
|
if (callee.property.name !== "findDOMNode") return;
|
|
16181
16970
|
context.report({
|
|
16182
16971
|
node: callee.property,
|
|
16183
|
-
message: MESSAGE$
|
|
16972
|
+
message: MESSAGE$16
|
|
16184
16973
|
});
|
|
16185
16974
|
}
|
|
16186
16975
|
} })
|
|
@@ -16197,7 +16986,7 @@ const noFlushSync = defineRule({
|
|
|
16197
16986
|
if (node.source?.value !== "react-dom") return;
|
|
16198
16987
|
for (const specifier of node.specifiers ?? []) {
|
|
16199
16988
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
16200
|
-
if (getImportedName(specifier) === "flushSync") context.report({
|
|
16989
|
+
if (getImportedName$1(specifier) === "flushSync") context.report({
|
|
16201
16990
|
node: specifier,
|
|
16202
16991
|
message: "flushSync from react-dom skips View Transition snapshots and concurrent rendering — prefer startTransition for non-urgent updates"
|
|
16203
16992
|
});
|
|
@@ -16239,6 +17028,64 @@ const noGenericHandlerNames = defineRule({
|
|
|
16239
17028
|
} })
|
|
16240
17029
|
});
|
|
16241
17030
|
//#endregion
|
|
17031
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
17032
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
17033
|
+
"FunctionDeclaration",
|
|
17034
|
+
"FunctionExpression",
|
|
17035
|
+
"ArrowFunctionExpression",
|
|
17036
|
+
"ClassDeclaration",
|
|
17037
|
+
"ClassExpression"
|
|
17038
|
+
]);
|
|
17039
|
+
const isReactImport$1 = (symbol) => {
|
|
17040
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
17041
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
17042
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
17043
|
+
return importDeclaration.source.value === "react";
|
|
17044
|
+
};
|
|
17045
|
+
const getImportedName = (symbol) => {
|
|
17046
|
+
if (symbol.kind !== "import") return null;
|
|
17047
|
+
if (!isReactImport$1(symbol)) return null;
|
|
17048
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
17049
|
+
};
|
|
17050
|
+
const isReactNamespaceImport = (symbol) => {
|
|
17051
|
+
if (symbol.kind !== "import") return false;
|
|
17052
|
+
if (!isReactImport$1(symbol)) return false;
|
|
17053
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
17054
|
+
};
|
|
17055
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
17056
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
17057
|
+
const symbol = scopes.symbolFor(callee);
|
|
17058
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
17059
|
+
};
|
|
17060
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
17061
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
17062
|
+
if (callee.computed) return false;
|
|
17063
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
17064
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
17065
|
+
if (callee.property.name !== "createElement") return false;
|
|
17066
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
17067
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
17068
|
+
};
|
|
17069
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
17070
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
17071
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
17072
|
+
};
|
|
17073
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
17074
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
17075
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
17076
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
17077
|
+
const nodeRecord = node;
|
|
17078
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
17079
|
+
if (key === "parent") continue;
|
|
17080
|
+
const child = nodeRecord[key];
|
|
17081
|
+
if (Array.isArray(child)) {
|
|
17082
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
17083
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
17084
|
+
}
|
|
17085
|
+
return false;
|
|
17086
|
+
};
|
|
17087
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
17088
|
+
//#endregion
|
|
16242
17089
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
16243
17090
|
const noGiantComponent = defineRule({
|
|
16244
17091
|
id: "no-giant-component",
|
|
@@ -16246,10 +17093,13 @@ const noGiantComponent = defineRule({
|
|
|
16246
17093
|
tags: ["test-noise", "react-jsx-only"],
|
|
16247
17094
|
recommendation: "Extract logical sections into focused components: `<UserHeader />`, `<UserActions />`, etc.",
|
|
16248
17095
|
create: (context) => {
|
|
16249
|
-
const
|
|
16250
|
-
if (!bodyNode.loc) return;
|
|
17096
|
+
const getOversizedComponentLineCount = (bodyNode) => {
|
|
17097
|
+
if (!bodyNode.loc) return null;
|
|
16251
17098
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
16252
|
-
|
|
17099
|
+
return lineCount > 300 ? lineCount : null;
|
|
17100
|
+
};
|
|
17101
|
+
const reportOversizedComponent = (nameNode, componentName, lineCount) => {
|
|
17102
|
+
context.report({
|
|
16253
17103
|
node: nameNode,
|
|
16254
17104
|
message: `Component "${componentName}" is ${lineCount} lines — consider breaking it into smaller focused components`
|
|
16255
17105
|
});
|
|
@@ -16257,12 +17107,18 @@ const noGiantComponent = defineRule({
|
|
|
16257
17107
|
return {
|
|
16258
17108
|
FunctionDeclaration(node) {
|
|
16259
17109
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
16260
|
-
|
|
17110
|
+
const lineCount = getOversizedComponentLineCount(node);
|
|
17111
|
+
if (lineCount === null) return;
|
|
17112
|
+
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
17113
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16261
17114
|
},
|
|
16262
17115
|
VariableDeclarator(node) {
|
|
16263
17116
|
if (!isComponentAssignment(node)) return;
|
|
16264
17117
|
if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
|
|
16265
|
-
|
|
17118
|
+
const lineCount = getOversizedComponentLineCount(node.init);
|
|
17119
|
+
if (lineCount === null) return;
|
|
17120
|
+
if (!functionContainsReactRenderOutput(node.init, context.scopes)) return;
|
|
17121
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16266
17122
|
}
|
|
16267
17123
|
};
|
|
16268
17124
|
}
|
|
@@ -16603,7 +17459,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
16603
17459
|
});
|
|
16604
17460
|
//#endregion
|
|
16605
17461
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
16606
|
-
const buildMessage$
|
|
17462
|
+
const buildMessage$13 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
16607
17463
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
16608
17464
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
16609
17465
|
tr: ["none", "presentation"],
|
|
@@ -16647,7 +17503,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
16647
17503
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
16648
17504
|
context.report({
|
|
16649
17505
|
node: roleAttribute,
|
|
16650
|
-
message: buildMessage$
|
|
17506
|
+
message: buildMessage$13(elementType, firstRole)
|
|
16651
17507
|
});
|
|
16652
17508
|
} };
|
|
16653
17509
|
}
|
|
@@ -16848,7 +17704,7 @@ const isInsideClassBody = (node) => {
|
|
|
16848
17704
|
let current = node.parent;
|
|
16849
17705
|
while (current) {
|
|
16850
17706
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
16851
|
-
if (isFunctionLike(current)) return false;
|
|
17707
|
+
if (isFunctionLike$1(current)) return false;
|
|
16852
17708
|
current = current.parent;
|
|
16853
17709
|
}
|
|
16854
17710
|
return false;
|
|
@@ -17091,7 +17947,7 @@ const noMoment = defineRule({
|
|
|
17091
17947
|
});
|
|
17092
17948
|
//#endregion
|
|
17093
17949
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17094
|
-
const buildMessage$
|
|
17950
|
+
const buildMessage$12 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17095
17951
|
const resolveSettings$16 = (settings) => {
|
|
17096
17952
|
const reactDoctor = settings?.["react-doctor"];
|
|
17097
17953
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -17391,7 +18247,7 @@ const noMultiComp = defineRule({
|
|
|
17391
18247
|
category: "Architecture",
|
|
17392
18248
|
create: (context) => {
|
|
17393
18249
|
const settings = resolveSettings$16(context.settings);
|
|
17394
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
18250
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
17395
18251
|
return { Program(node) {
|
|
17396
18252
|
if (isTestlikeFile) return;
|
|
17397
18253
|
const visitContext = {
|
|
@@ -17412,7 +18268,7 @@ const noMultiComp = defineRule({
|
|
|
17412
18268
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
17413
18269
|
for (const component of flagged.slice(1)) context.report({
|
|
17414
18270
|
node: component.reportNode,
|
|
17415
|
-
message: buildMessage$
|
|
18271
|
+
message: buildMessage$12(component.name)
|
|
17416
18272
|
});
|
|
17417
18273
|
} };
|
|
17418
18274
|
}
|
|
@@ -17490,7 +18346,7 @@ const noMutableInDeps = defineRule({
|
|
|
17490
18346
|
});
|
|
17491
18347
|
//#endregion
|
|
17492
18348
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
17493
|
-
const MESSAGE$
|
|
18349
|
+
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
18350
|
const MUTATING_ARRAY_METHODS = new Set([
|
|
17495
18351
|
"copyWithin",
|
|
17496
18352
|
"fill",
|
|
@@ -17687,7 +18543,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
17687
18543
|
reportedNodes.add(mutation.node);
|
|
17688
18544
|
context.report({
|
|
17689
18545
|
node: mutation.node,
|
|
17690
|
-
message: MESSAGE$
|
|
18546
|
+
message: MESSAGE$15
|
|
17691
18547
|
});
|
|
17692
18548
|
}
|
|
17693
18549
|
};
|
|
@@ -17773,7 +18629,7 @@ const noMutatingReducerState = defineRule({
|
|
|
17773
18629
|
});
|
|
17774
18630
|
//#endregion
|
|
17775
18631
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
17776
|
-
const buildMessage$
|
|
18632
|
+
const buildMessage$11 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
17777
18633
|
const noNamespace = defineRule({
|
|
17778
18634
|
id: "no-namespace",
|
|
17779
18635
|
severity: "warn",
|
|
@@ -17785,7 +18641,7 @@ const noNamespace = defineRule({
|
|
|
17785
18641
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
17786
18642
|
context.report({
|
|
17787
18643
|
node: namespaced,
|
|
17788
|
-
message: buildMessage$
|
|
18644
|
+
message: buildMessage$11(fullName)
|
|
17789
18645
|
});
|
|
17790
18646
|
},
|
|
17791
18647
|
CallExpression(node) {
|
|
@@ -17796,7 +18652,7 @@ const noNamespace = defineRule({
|
|
|
17796
18652
|
if (!firstArgument.value.includes(":")) return;
|
|
17797
18653
|
context.report({
|
|
17798
18654
|
node: firstArgument,
|
|
17799
|
-
message: buildMessage$
|
|
18655
|
+
message: buildMessage$11(firstArgument.value)
|
|
17800
18656
|
});
|
|
17801
18657
|
}
|
|
17802
18658
|
})
|
|
@@ -17840,7 +18696,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
17840
18696
|
});
|
|
17841
18697
|
//#endregion
|
|
17842
18698
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
17843
|
-
const buildMessage$
|
|
18699
|
+
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
18700
|
const INTERACTIVE_HANDLERS = [
|
|
17845
18701
|
"onClick",
|
|
17846
18702
|
"onMouseDown",
|
|
@@ -17866,13 +18722,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
17866
18722
|
}
|
|
17867
18723
|
context.report({
|
|
17868
18724
|
node: node.name,
|
|
17869
|
-
message: buildMessage$
|
|
18725
|
+
message: buildMessage$10(tag)
|
|
17870
18726
|
});
|
|
17871
18727
|
} })
|
|
17872
18728
|
});
|
|
17873
18729
|
//#endregion
|
|
17874
18730
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
17875
|
-
const buildMessage$
|
|
18731
|
+
const buildMessage$9 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
17876
18732
|
const DEFAULT_ALLOWED_ROLES = {
|
|
17877
18733
|
ul: [
|
|
17878
18734
|
"menu",
|
|
@@ -17936,14 +18792,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
17936
18792
|
if (!isInteractiveRole(firstRole)) return;
|
|
17937
18793
|
context.report({
|
|
17938
18794
|
node: roleAttribute,
|
|
17939
|
-
message: buildMessage$
|
|
18795
|
+
message: buildMessage$9(elementType, firstRole)
|
|
17940
18796
|
});
|
|
17941
18797
|
} };
|
|
17942
18798
|
}
|
|
17943
18799
|
});
|
|
17944
18800
|
//#endregion
|
|
17945
18801
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
17946
|
-
const MESSAGE$
|
|
18802
|
+
const MESSAGE$14 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
|
|
17947
18803
|
const resolveSettings$14 = (settings) => {
|
|
17948
18804
|
const reactDoctor = settings?.["react-doctor"];
|
|
17949
18805
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -17970,7 +18826,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17970
18826
|
if (numeric === null) {
|
|
17971
18827
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
17972
18828
|
node: tabIndex,
|
|
17973
|
-
message: MESSAGE$
|
|
18829
|
+
message: MESSAGE$14
|
|
17974
18830
|
});
|
|
17975
18831
|
return;
|
|
17976
18832
|
}
|
|
@@ -17983,7 +18839,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17983
18839
|
if (!roleAttribute) {
|
|
17984
18840
|
context.report({
|
|
17985
18841
|
node: tabIndex,
|
|
17986
|
-
message: MESSAGE$
|
|
18842
|
+
message: MESSAGE$14
|
|
17987
18843
|
});
|
|
17988
18844
|
return;
|
|
17989
18845
|
}
|
|
@@ -17997,7 +18853,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17997
18853
|
}
|
|
17998
18854
|
context.report({
|
|
17999
18855
|
node: tabIndex,
|
|
18000
|
-
message: MESSAGE$
|
|
18856
|
+
message: MESSAGE$14
|
|
18001
18857
|
});
|
|
18002
18858
|
} };
|
|
18003
18859
|
}
|
|
@@ -18558,7 +19414,7 @@ const noPureBlackBackground = defineRule({
|
|
|
18558
19414
|
});
|
|
18559
19415
|
//#endregion
|
|
18560
19416
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
18561
|
-
const MESSAGE$
|
|
19417
|
+
const MESSAGE$13 = "`React.Children` is uncommon and leads to fragile components.";
|
|
18562
19418
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
18563
19419
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
18564
19420
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -18583,13 +19439,13 @@ const noReactChildren = defineRule({
|
|
|
18583
19439
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
18584
19440
|
context.report({
|
|
18585
19441
|
node: calleeOuter,
|
|
18586
|
-
message: MESSAGE$
|
|
19442
|
+
message: MESSAGE$13
|
|
18587
19443
|
});
|
|
18588
19444
|
return;
|
|
18589
19445
|
}
|
|
18590
19446
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
18591
19447
|
node: calleeOuter,
|
|
18592
|
-
message: MESSAGE$
|
|
19448
|
+
message: MESSAGE$13
|
|
18593
19449
|
});
|
|
18594
19450
|
} })
|
|
18595
19451
|
});
|
|
@@ -18605,7 +19461,7 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
18605
19461
|
if (sourceValue !== source) return;
|
|
18606
19462
|
for (const specifier of node.specifiers ?? []) {
|
|
18607
19463
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18608
|
-
const importedName = getImportedName(specifier);
|
|
19464
|
+
const importedName = getImportedName$1(specifier);
|
|
18609
19465
|
if (!importedName) continue;
|
|
18610
19466
|
const message = messages.get(importedName);
|
|
18611
19467
|
if (message) context.report({
|
|
@@ -18657,7 +19513,7 @@ const buildTestUtilsMessage = (importedName) => {
|
|
|
18657
19513
|
const reportTestUtilsImports = (node, context) => {
|
|
18658
19514
|
for (const specifier of node.specifiers ?? []) {
|
|
18659
19515
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18660
|
-
const importedName = getImportedName(specifier) ?? "default";
|
|
19516
|
+
const importedName = getImportedName$1(specifier) ?? "default";
|
|
18661
19517
|
context.report({
|
|
18662
19518
|
node: specifier,
|
|
18663
19519
|
message: buildTestUtilsMessage(importedName)
|
|
@@ -18785,7 +19641,7 @@ const getTagsForRole = (role) => {
|
|
|
18785
19641
|
};
|
|
18786
19642
|
//#endregion
|
|
18787
19643
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
18788
|
-
const buildMessage$
|
|
19644
|
+
const buildMessage$8 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
18789
19645
|
const resolveSettings$13 = (settings) => {
|
|
18790
19646
|
const reactDoctor = settings?.["react-doctor"];
|
|
18791
19647
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -18808,14 +19664,14 @@ const noRedundantRoles = defineRule({
|
|
|
18808
19664
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
18809
19665
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
18810
19666
|
node: roleAttr,
|
|
18811
|
-
message: buildMessage$
|
|
19667
|
+
message: buildMessage$8(tag, role)
|
|
18812
19668
|
});
|
|
18813
19669
|
} };
|
|
18814
19670
|
}
|
|
18815
19671
|
});
|
|
18816
19672
|
//#endregion
|
|
18817
19673
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
18818
|
-
const buildMessage$
|
|
19674
|
+
const buildMessage$7 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
18819
19675
|
const isPureComponentSuper = (superClass) => {
|
|
18820
19676
|
if (!superClass) return false;
|
|
18821
19677
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -18847,7 +19703,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
18847
19703
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
18848
19704
|
context.report({
|
|
18849
19705
|
node: reportNode,
|
|
18850
|
-
message: buildMessage$
|
|
19706
|
+
message: buildMessage$7(className)
|
|
18851
19707
|
});
|
|
18852
19708
|
};
|
|
18853
19709
|
return {
|
|
@@ -18906,7 +19762,7 @@ const noRenderPropChildren = defineRule({
|
|
|
18906
19762
|
});
|
|
18907
19763
|
//#endregion
|
|
18908
19764
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
18909
|
-
const MESSAGE$
|
|
19765
|
+
const MESSAGE$12 = "Do not use the return value from `ReactDOM.render`.";
|
|
18910
19766
|
const isReactDomRenderCall = (node) => {
|
|
18911
19767
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
18912
19768
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -18929,7 +19785,7 @@ const noRenderReturnValue = defineRule({
|
|
|
18929
19785
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
18930
19786
|
context.report({
|
|
18931
19787
|
node: node.callee,
|
|
18932
|
-
message: MESSAGE$
|
|
19788
|
+
message: MESSAGE$12
|
|
18933
19789
|
});
|
|
18934
19790
|
} })
|
|
18935
19791
|
});
|
|
@@ -19324,7 +20180,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
19324
20180
|
const isInsideServerOnlyScope = (node) => {
|
|
19325
20181
|
let currentNode = node.parent ?? null;
|
|
19326
20182
|
while (currentNode) {
|
|
19327
|
-
if (isFunctionLike(currentNode)) {
|
|
20183
|
+
if (isFunctionLike$1(currentNode)) {
|
|
19328
20184
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
19329
20185
|
}
|
|
19330
20186
|
currentNode = currentNode.parent ?? null;
|
|
@@ -19338,7 +20194,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
19338
20194
|
severity: "warn",
|
|
19339
20195
|
recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
|
|
19340
20196
|
create: (context) => {
|
|
19341
|
-
const filename = normalizeFilename$1(context.
|
|
20197
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
19342
20198
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
19343
20199
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
19344
20200
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -19389,7 +20245,7 @@ const getParentComponent = (node) => {
|
|
|
19389
20245
|
};
|
|
19390
20246
|
//#endregion
|
|
19391
20247
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
19392
|
-
const MESSAGE$
|
|
20248
|
+
const MESSAGE$11 = "Do not use `this.setState` in components.";
|
|
19393
20249
|
const noSetState = defineRule({
|
|
19394
20250
|
id: "no-set-state",
|
|
19395
20251
|
severity: "warn",
|
|
@@ -19403,7 +20259,7 @@ const noSetState = defineRule({
|
|
|
19403
20259
|
if (!getParentComponent(node)) return;
|
|
19404
20260
|
context.report({
|
|
19405
20261
|
node: node.callee,
|
|
19406
|
-
message: MESSAGE$
|
|
20262
|
+
message: MESSAGE$11
|
|
19407
20263
|
});
|
|
19408
20264
|
} })
|
|
19409
20265
|
});
|
|
@@ -19562,7 +20418,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
19562
20418
|
};
|
|
19563
20419
|
//#endregion
|
|
19564
20420
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
19565
|
-
const MESSAGE$
|
|
20421
|
+
const MESSAGE$10 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
19566
20422
|
const DEFAULT_HANDLERS = [
|
|
19567
20423
|
"onClick",
|
|
19568
20424
|
"onMouseDown",
|
|
@@ -19593,7 +20449,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
19593
20449
|
category: "Accessibility",
|
|
19594
20450
|
create: (context) => {
|
|
19595
20451
|
const settings = resolveSettings$12(context.settings);
|
|
19596
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20452
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
19597
20453
|
return { JSXOpeningElement(node) {
|
|
19598
20454
|
if (isTestlikeFile) return;
|
|
19599
20455
|
let hasNonBlockerHandler = false;
|
|
@@ -19621,7 +20477,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
19621
20477
|
if (!roleAttribute || !roleAttribute.value) {
|
|
19622
20478
|
context.report({
|
|
19623
20479
|
node: node.name,
|
|
19624
|
-
message: MESSAGE$
|
|
20480
|
+
message: MESSAGE$10
|
|
19625
20481
|
});
|
|
19626
20482
|
return;
|
|
19627
20483
|
}
|
|
@@ -19631,14 +20487,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
19631
20487
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
19632
20488
|
context.report({
|
|
19633
20489
|
node: node.name,
|
|
19634
|
-
message: MESSAGE$
|
|
20490
|
+
message: MESSAGE$10
|
|
19635
20491
|
});
|
|
19636
20492
|
return;
|
|
19637
20493
|
}
|
|
19638
20494
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
19639
20495
|
context.report({
|
|
19640
20496
|
node: node.name,
|
|
19641
|
-
message: MESSAGE$
|
|
20497
|
+
message: MESSAGE$10
|
|
19642
20498
|
});
|
|
19643
20499
|
} };
|
|
19644
20500
|
}
|
|
@@ -19670,7 +20526,7 @@ const noStringRefs = defineRule({
|
|
|
19670
20526
|
recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
|
|
19671
20527
|
create: (context) => {
|
|
19672
20528
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
19673
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20529
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
19674
20530
|
return {
|
|
19675
20531
|
JSXAttribute(node) {
|
|
19676
20532
|
if (isTestlikeFile) return;
|
|
@@ -19694,7 +20550,7 @@ const noStringRefs = defineRule({
|
|
|
19694
20550
|
});
|
|
19695
20551
|
//#endregion
|
|
19696
20552
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
19697
|
-
const MESSAGE$
|
|
20553
|
+
const MESSAGE$9 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
19698
20554
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
19699
20555
|
let ancestor = node.parent;
|
|
19700
20556
|
while (ancestor) {
|
|
@@ -19762,7 +20618,7 @@ const noThisInSfc = defineRule({
|
|
|
19762
20618
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
19763
20619
|
context.report({
|
|
19764
20620
|
node,
|
|
19765
|
-
message: MESSAGE$
|
|
20621
|
+
message: MESSAGE$9
|
|
19766
20622
|
});
|
|
19767
20623
|
} };
|
|
19768
20624
|
}
|
|
@@ -19944,7 +20800,7 @@ const ESCAPED_VERSIONS = {
|
|
|
19944
20800
|
">": "`>` / `>`",
|
|
19945
20801
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
19946
20802
|
};
|
|
19947
|
-
const buildMessage$
|
|
20803
|
+
const buildMessage$6 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
19948
20804
|
const noUnescapedEntities = defineRule({
|
|
19949
20805
|
id: "no-unescaped-entities",
|
|
19950
20806
|
severity: "warn",
|
|
@@ -19955,7 +20811,7 @@ const noUnescapedEntities = defineRule({
|
|
|
19955
20811
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
19956
20812
|
context.report({
|
|
19957
20813
|
node,
|
|
19958
|
-
message: buildMessage$
|
|
20814
|
+
message: buildMessage$6(character)
|
|
19959
20815
|
});
|
|
19960
20816
|
return;
|
|
19961
20817
|
}
|
|
@@ -20976,7 +21832,7 @@ const SAFER_REPLACEMENT = {
|
|
|
20976
21832
|
componentWillUpdate: "componentDidUpdate",
|
|
20977
21833
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
20978
21834
|
};
|
|
20979
|
-
const buildMessage$
|
|
21835
|
+
const buildMessage$5 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
20980
21836
|
const resolveSettings$9 = (settings) => {
|
|
20981
21837
|
const reactDoctor = settings?.["react-doctor"];
|
|
20982
21838
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21024,7 +21880,7 @@ const noUnsafe = defineRule({
|
|
|
21024
21880
|
if (!getParentComponent(node)) return;
|
|
21025
21881
|
context.report({
|
|
21026
21882
|
node: node.key,
|
|
21027
|
-
message: buildMessage$
|
|
21883
|
+
message: buildMessage$5(name)
|
|
21028
21884
|
});
|
|
21029
21885
|
},
|
|
21030
21886
|
Property(node) {
|
|
@@ -21035,7 +21891,7 @@ const noUnsafe = defineRule({
|
|
|
21035
21891
|
if (isEs5Component(ancestor)) {
|
|
21036
21892
|
context.report({
|
|
21037
21893
|
node: node.key,
|
|
21038
|
-
message: buildMessage$
|
|
21894
|
+
message: buildMessage$5(name)
|
|
21039
21895
|
});
|
|
21040
21896
|
return;
|
|
21041
21897
|
}
|
|
@@ -21047,7 +21903,7 @@ const noUnsafe = defineRule({
|
|
|
21047
21903
|
});
|
|
21048
21904
|
//#endregion
|
|
21049
21905
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21050
|
-
const buildMessage$
|
|
21906
|
+
const buildMessage$4 = (parentName, isInProp, allowAsProps) => {
|
|
21051
21907
|
let message = "Don't define components inside another component";
|
|
21052
21908
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21053
21909
|
message += " — extract it to module scope.";
|
|
@@ -21116,7 +21972,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
21116
21972
|
const findEnclosingComponent = (node) => {
|
|
21117
21973
|
let walker = node.parent;
|
|
21118
21974
|
while (walker) {
|
|
21119
|
-
if (isFunctionLike(walker)) {
|
|
21975
|
+
if (isFunctionLike$1(walker)) {
|
|
21120
21976
|
const componentName = inferFunctionLikeName(walker);
|
|
21121
21977
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
21122
21978
|
component: walker,
|
|
@@ -21282,7 +22138,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
21282
22138
|
if (!enclosing) return;
|
|
21283
22139
|
context.report({
|
|
21284
22140
|
node: reportNode,
|
|
21285
|
-
message: buildMessage$
|
|
22141
|
+
message: buildMessage$4(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
21286
22142
|
});
|
|
21287
22143
|
};
|
|
21288
22144
|
const checkFunctionLike = (node) => {
|
|
@@ -21414,7 +22270,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
21414
22270
|
//#endregion
|
|
21415
22271
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
21416
22272
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
21417
|
-
const MESSAGE$
|
|
22273
|
+
const MESSAGE$8 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
21418
22274
|
const resolveSettings$7 = (settings) => {
|
|
21419
22275
|
const reactDoctor = settings?.["react-doctor"];
|
|
21420
22276
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -21447,7 +22303,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
21447
22303
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
21448
22304
|
context.report({
|
|
21449
22305
|
node: node.callee,
|
|
21450
|
-
message: MESSAGE$
|
|
22306
|
+
message: MESSAGE$8
|
|
21451
22307
|
});
|
|
21452
22308
|
} };
|
|
21453
22309
|
}
|
|
@@ -21816,7 +22672,7 @@ const onlyExportComponents = defineRule({
|
|
|
21816
22672
|
allowConstantExport: settings.allowConstantExport
|
|
21817
22673
|
};
|
|
21818
22674
|
return { Program(node) {
|
|
21819
|
-
if (!isFileNameAllowed(
|
|
22675
|
+
if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
|
|
21820
22676
|
const allNodes = collectAllNodes(node);
|
|
21821
22677
|
const exports = [];
|
|
21822
22678
|
let hasReactExport = false;
|
|
@@ -21985,6 +22841,214 @@ const onlyExportComponents = defineRule({
|
|
|
21985
22841
|
}
|
|
21986
22842
|
});
|
|
21987
22843
|
//#endregion
|
|
22844
|
+
//#region src/plugin/rules/preact/preact-no-children-length.ts
|
|
22845
|
+
const ARRAY_READ_METHOD_NAMES = new Set([
|
|
22846
|
+
"length",
|
|
22847
|
+
"map",
|
|
22848
|
+
"forEach",
|
|
22849
|
+
"filter",
|
|
22850
|
+
"find",
|
|
22851
|
+
"reduce",
|
|
22852
|
+
"some",
|
|
22853
|
+
"every",
|
|
22854
|
+
"flat",
|
|
22855
|
+
"flatMap",
|
|
22856
|
+
"indexOf",
|
|
22857
|
+
"includes",
|
|
22858
|
+
"slice",
|
|
22859
|
+
"concat",
|
|
22860
|
+
"join"
|
|
22861
|
+
]);
|
|
22862
|
+
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`.";
|
|
22863
|
+
const isDestructuredChildrenParam = (identifier) => {
|
|
22864
|
+
let cursor = identifier.parent;
|
|
22865
|
+
while (cursor) {
|
|
22866
|
+
if (isNodeOfType(cursor, "FunctionDeclaration") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "ArrowFunctionExpression")) {
|
|
22867
|
+
const firstParam = cursor.params[0];
|
|
22868
|
+
if (!firstParam || !isNodeOfType(firstParam, "ObjectPattern")) return false;
|
|
22869
|
+
return firstParam.properties.some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "children");
|
|
22870
|
+
}
|
|
22871
|
+
cursor = cursor.parent ?? null;
|
|
22872
|
+
}
|
|
22873
|
+
return false;
|
|
22874
|
+
};
|
|
22875
|
+
const isChildrenMemberExpression = (node) => {
|
|
22876
|
+
const object = node.object;
|
|
22877
|
+
if (!isNodeOfType(object, "MemberExpression")) return isNodeOfType(object, "Identifier") && object.name === "children" && isDestructuredChildrenParam(object);
|
|
22878
|
+
if (!isNodeOfType(object.property, "Identifier") || object.property.name !== "children") return false;
|
|
22879
|
+
const propsObject = object.object;
|
|
22880
|
+
if (isNodeOfType(propsObject, "Identifier") && propsObject.name === "props") return true;
|
|
22881
|
+
if (isNodeOfType(propsObject, "MemberExpression") && isNodeOfType(propsObject.property, "Identifier") && propsObject.property.name === "props" && isNodeOfType(propsObject.object, "ThisExpression")) return true;
|
|
22882
|
+
return false;
|
|
22883
|
+
};
|
|
22884
|
+
const preactNoChildrenLength = defineRule({
|
|
22885
|
+
id: "preact-no-children-length",
|
|
22886
|
+
requires: ["preact"],
|
|
22887
|
+
severity: "warn",
|
|
22888
|
+
recommendation: "Wrap with `toChildArray(children)` from `preact` before accessing array methods or `.length`.",
|
|
22889
|
+
create: (context) => ({ MemberExpression(node) {
|
|
22890
|
+
if (node.computed) return;
|
|
22891
|
+
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
22892
|
+
if (!ARRAY_READ_METHOD_NAMES.has(node.property.name)) return;
|
|
22893
|
+
if (!isChildrenMemberExpression(node)) return;
|
|
22894
|
+
context.report({
|
|
22895
|
+
node,
|
|
22896
|
+
message: CHILDREN_ARRAY_MESSAGE
|
|
22897
|
+
});
|
|
22898
|
+
} })
|
|
22899
|
+
});
|
|
22900
|
+
//#endregion
|
|
22901
|
+
//#region src/plugin/rules/preact/preact-no-react-hooks-import.ts
|
|
22902
|
+
const REACT_HOOK_NAMES = new Set([
|
|
22903
|
+
"useCallback",
|
|
22904
|
+
"useContext",
|
|
22905
|
+
"useDebugValue",
|
|
22906
|
+
"useDeferredValue",
|
|
22907
|
+
"useEffect",
|
|
22908
|
+
"useId",
|
|
22909
|
+
"useImperativeHandle",
|
|
22910
|
+
"useInsertionEffect",
|
|
22911
|
+
"useLayoutEffect",
|
|
22912
|
+
"useMemo",
|
|
22913
|
+
"useReducer",
|
|
22914
|
+
"useRef",
|
|
22915
|
+
"useState",
|
|
22916
|
+
"useSyncExternalStore",
|
|
22917
|
+
"useTransition"
|
|
22918
|
+
]);
|
|
22919
|
+
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.`;
|
|
22920
|
+
const preactNoReactHooksImport = defineRule({
|
|
22921
|
+
id: "preact-no-react-hooks-import",
|
|
22922
|
+
requires: ["pure-preact"],
|
|
22923
|
+
severity: "warn",
|
|
22924
|
+
recommendation: "Replace `from \"react\"` with `from \"preact/hooks\"` (or `from \"preact/compat\"` if other React API surface is needed).",
|
|
22925
|
+
create: (context) => ({ ImportDeclaration(node) {
|
|
22926
|
+
const source = node.source;
|
|
22927
|
+
if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
|
|
22928
|
+
const reactHookSpecifiers = [];
|
|
22929
|
+
for (const specifier of node.specifiers) {
|
|
22930
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
22931
|
+
const imported = specifier.imported;
|
|
22932
|
+
if (!isNodeOfType(imported, "Identifier")) continue;
|
|
22933
|
+
if (REACT_HOOK_NAMES.has(imported.name)) reactHookSpecifiers.push(specifier);
|
|
22934
|
+
}
|
|
22935
|
+
if (reactHookSpecifiers.length === 0) return;
|
|
22936
|
+
const importedNames = reactHookSpecifiers.map((specifier) => {
|
|
22937
|
+
const imported = specifier.imported;
|
|
22938
|
+
return isNodeOfType(imported, "Identifier") ? imported.name : "";
|
|
22939
|
+
});
|
|
22940
|
+
context.report({
|
|
22941
|
+
node,
|
|
22942
|
+
message: buildMessage$3(importedNames)
|
|
22943
|
+
});
|
|
22944
|
+
} })
|
|
22945
|
+
});
|
|
22946
|
+
//#endregion
|
|
22947
|
+
//#region src/plugin/rules/preact/preact-no-render-arguments.ts
|
|
22948
|
+
const PREACT_COMPONENT_NAMESPACES = new Set(["Preact"]);
|
|
22949
|
+
const PREACT_COMPONENT_NAMES = new Set(["Component", "PureComponent"]);
|
|
22950
|
+
const isPreactNamespaceComponentRef = (node) => {
|
|
22951
|
+
if (!isNodeOfType(node, "MemberExpression")) return false;
|
|
22952
|
+
if (!isNodeOfType(node.object, "Identifier")) return false;
|
|
22953
|
+
if (!PREACT_COMPONENT_NAMESPACES.has(node.object.name)) return false;
|
|
22954
|
+
if (!isNodeOfType(node.property, "Identifier")) return false;
|
|
22955
|
+
return PREACT_COMPONENT_NAMES.has(node.property.name);
|
|
22956
|
+
};
|
|
22957
|
+
const isPreactOrReactComponentClass = (node) => {
|
|
22958
|
+
if (isEs6Component(node)) return true;
|
|
22959
|
+
if (!isNodeOfType(node, "ClassDeclaration") && !isNodeOfType(node, "ClassExpression")) return false;
|
|
22960
|
+
const superClass = node.superClass;
|
|
22961
|
+
if (!superClass) return false;
|
|
22962
|
+
return isPreactNamespaceComponentRef(superClass);
|
|
22963
|
+
};
|
|
22964
|
+
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`.";
|
|
22965
|
+
const isInstanceMethodNamedRender = (node) => isNodeOfType(node, "MethodDefinition") && node.kind === "method" && node.static !== true && isNodeOfType(node.key, "Identifier") && node.key.name === "render";
|
|
22966
|
+
const isInsideEs6Component$1 = (methodDefinition) => {
|
|
22967
|
+
const classBody = methodDefinition.parent;
|
|
22968
|
+
if (!classBody || !isNodeOfType(classBody, "ClassBody")) return false;
|
|
22969
|
+
const owningClass = classBody.parent;
|
|
22970
|
+
if (!owningClass) return false;
|
|
22971
|
+
return isPreactOrReactComponentClass(owningClass);
|
|
22972
|
+
};
|
|
22973
|
+
const stripThisParameter = (params) => {
|
|
22974
|
+
const first = params[0];
|
|
22975
|
+
if (!first) return params;
|
|
22976
|
+
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
22977
|
+
return params;
|
|
22978
|
+
};
|
|
22979
|
+
const preactNoRenderArguments = defineRule({
|
|
22980
|
+
id: "preact-no-render-arguments",
|
|
22981
|
+
requires: ["preact"],
|
|
22982
|
+
severity: "warn",
|
|
22983
|
+
recommendation: "Read state/props from `this.props` / `this.state` inside `render()` instead of declaring positional parameters.",
|
|
22984
|
+
create: (context) => ({ MethodDefinition(node) {
|
|
22985
|
+
if (!isInstanceMethodNamedRender(node)) return;
|
|
22986
|
+
if (!isInsideEs6Component$1(node)) return;
|
|
22987
|
+
const renderFunction = node.value;
|
|
22988
|
+
if (!renderFunction || !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
22989
|
+
const firstParameter = stripThisParameter(renderFunction.params)[0];
|
|
22990
|
+
if (!firstParameter) return;
|
|
22991
|
+
context.report({
|
|
22992
|
+
node: firstParameter,
|
|
22993
|
+
message: RENDER_ARGUMENTS_MESSAGE
|
|
22994
|
+
});
|
|
22995
|
+
} })
|
|
22996
|
+
});
|
|
22997
|
+
//#endregion
|
|
22998
|
+
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
22999
|
+
const MESSAGE$7 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
|
|
23000
|
+
const preactPreferOndblclick = defineRule({
|
|
23001
|
+
id: "preact-prefer-ondblclick",
|
|
23002
|
+
requires: ["pure-preact"],
|
|
23003
|
+
severity: "warn",
|
|
23004
|
+
recommendation: "Rename the handler from `onDoubleClick` to `onDblClick` to match the DOM event name.",
|
|
23005
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23006
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23007
|
+
const tagName = node.name.name;
|
|
23008
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23009
|
+
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
23010
|
+
if (!onDoubleClickAttribute) return;
|
|
23011
|
+
context.report({
|
|
23012
|
+
node: onDoubleClickAttribute,
|
|
23013
|
+
message: MESSAGE$7
|
|
23014
|
+
});
|
|
23015
|
+
} })
|
|
23016
|
+
});
|
|
23017
|
+
//#endregion
|
|
23018
|
+
//#region src/plugin/rules/preact/preact-prefer-oninput.ts
|
|
23019
|
+
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.";
|
|
23020
|
+
const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
23021
|
+
"checkbox",
|
|
23022
|
+
"radio",
|
|
23023
|
+
"file"
|
|
23024
|
+
]);
|
|
23025
|
+
const isTextLikeInput = (openingElement) => {
|
|
23026
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
23027
|
+
const tagName = openingElement.name.name;
|
|
23028
|
+
if (tagName === "textarea") return true;
|
|
23029
|
+
if (tagName !== "input") return false;
|
|
23030
|
+
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
23031
|
+
if (!typeAttribute) return true;
|
|
23032
|
+
const typeValue = getJsxPropStringValue(typeAttribute);
|
|
23033
|
+
if (typeValue === null) return true;
|
|
23034
|
+
return !COMPAT_EXEMPT_INPUT_TYPES.has(typeValue);
|
|
23035
|
+
};
|
|
23036
|
+
const preactPreferOninput = defineRule({
|
|
23037
|
+
id: "preact-prefer-oninput",
|
|
23038
|
+
requires: ["pure-preact"],
|
|
23039
|
+
severity: "warn",
|
|
23040
|
+
recommendation: "Replace `onChange` with `onInput` on text-like inputs, or use `preact/compat` which remaps `onChange` automatically.",
|
|
23041
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23042
|
+
if (!isTextLikeInput(node)) return;
|
|
23043
|
+
const onChangeAttribute = findJsxAttribute(node.attributes, "onChange");
|
|
23044
|
+
if (!onChangeAttribute) return;
|
|
23045
|
+
context.report({
|
|
23046
|
+
node: onChangeAttribute,
|
|
23047
|
+
message: PREFER_ONINPUT_MESSAGE
|
|
23048
|
+
});
|
|
23049
|
+
} })
|
|
23050
|
+
});
|
|
23051
|
+
//#endregion
|
|
21988
23052
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
21989
23053
|
const preferDynamicImport = defineRule({
|
|
21990
23054
|
id: "prefer-dynamic-import",
|
|
@@ -22106,6 +23170,49 @@ const preferFunctionComponent = defineRule({
|
|
|
22106
23170
|
}
|
|
22107
23171
|
});
|
|
22108
23172
|
//#endregion
|
|
23173
|
+
//#region src/plugin/rules/a11y/prefer-html-dialog.ts
|
|
23174
|
+
const ROLE_DIALOG_VALUES = new Set(["dialog", "alertdialog"]);
|
|
23175
|
+
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.";
|
|
23176
|
+
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.";
|
|
23177
|
+
const isAriaModalTrue = (attribute) => {
|
|
23178
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
23179
|
+
if (stringValue !== null) return stringValue === "true";
|
|
23180
|
+
const value = attribute.value;
|
|
23181
|
+
if (!value) return true;
|
|
23182
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
23183
|
+
const expression = value.expression;
|
|
23184
|
+
if (isNodeOfType(expression, "Literal") && expression.value === true) return true;
|
|
23185
|
+
}
|
|
23186
|
+
return false;
|
|
23187
|
+
};
|
|
23188
|
+
const preferHtmlDialog = defineRule({
|
|
23189
|
+
id: "prefer-html-dialog",
|
|
23190
|
+
severity: "warn",
|
|
23191
|
+
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()`.",
|
|
23192
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23193
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23194
|
+
const tagName = node.name.name;
|
|
23195
|
+
if (tagName === "dialog") return;
|
|
23196
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23197
|
+
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
23198
|
+
if (roleAttribute) {
|
|
23199
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
23200
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
23201
|
+
context.report({
|
|
23202
|
+
node: roleAttribute,
|
|
23203
|
+
message: ROLE_DIALOG_MESSAGE
|
|
23204
|
+
});
|
|
23205
|
+
return;
|
|
23206
|
+
}
|
|
23207
|
+
}
|
|
23208
|
+
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
23209
|
+
if (ariaModalAttribute && isAriaModalTrue(ariaModalAttribute)) context.report({
|
|
23210
|
+
node: ariaModalAttribute,
|
|
23211
|
+
message: ARIA_MODAL_MESSAGE
|
|
23212
|
+
});
|
|
23213
|
+
} })
|
|
23214
|
+
});
|
|
23215
|
+
//#endregion
|
|
22109
23216
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
22110
23217
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
22111
23218
|
const preferTagOverRole = defineRule({
|
|
@@ -23086,7 +24193,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
23086
24193
|
category: "Performance",
|
|
23087
24194
|
recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
23088
24195
|
create: (context) => {
|
|
23089
|
-
const filename = context.
|
|
24196
|
+
const filename = context.filename;
|
|
23090
24197
|
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
|
|
23091
24198
|
return { JSXAttribute(node) {
|
|
23092
24199
|
if (isAutoGenerated) return;
|
|
@@ -23118,7 +24225,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
23118
24225
|
let found = false;
|
|
23119
24226
|
walkAst(functionBody, (child) => {
|
|
23120
24227
|
if (found) return;
|
|
23121
|
-
if (child !== functionBody && isFunctionLike(child)) return false;
|
|
24228
|
+
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
23122
24229
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
23123
24230
|
});
|
|
23124
24231
|
return found;
|
|
@@ -23137,7 +24244,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
23137
24244
|
let found = false;
|
|
23138
24245
|
walkAst(componentBody, (child) => {
|
|
23139
24246
|
if (found) return;
|
|
23140
|
-
if (!isFunctionLike(child)) return;
|
|
24247
|
+
if (!isFunctionLike$1(child)) return;
|
|
23141
24248
|
const functionBody = child.body;
|
|
23142
24249
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
23143
24250
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24209,6 +25316,55 @@ const rnListDataMapped = defineRule({
|
|
|
24209
25316
|
} })
|
|
24210
25317
|
});
|
|
24211
25318
|
//#endregion
|
|
25319
|
+
//#region src/plugin/rules/react-native/rn-list-missing-estimated-item-size.ts
|
|
25320
|
+
const RECYCLABLE_LIST_PACKAGES = {
|
|
25321
|
+
FlashList: ["@shopify/flash-list"],
|
|
25322
|
+
LegendList: ["@legendapp/list"]
|
|
25323
|
+
};
|
|
25324
|
+
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
25325
|
+
const isEmptyArrayLiteral = (node) => {
|
|
25326
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
25327
|
+
const expression = node.value.expression;
|
|
25328
|
+
return isNodeOfType(expression, "ArrayExpression") && (expression.elements?.length ?? 0) === 0;
|
|
25329
|
+
};
|
|
25330
|
+
const rnListMissingEstimatedItemSize = defineRule({
|
|
25331
|
+
id: "rn-list-missing-estimated-item-size",
|
|
25332
|
+
tags: ["test-noise"],
|
|
25333
|
+
requires: ["react-native"],
|
|
25334
|
+
severity: "warn",
|
|
25335
|
+
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",
|
|
25336
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
25337
|
+
const localElementName = resolveJsxElementName(node);
|
|
25338
|
+
if (!localElementName) return;
|
|
25339
|
+
let canonicalRecyclerName = null;
|
|
25340
|
+
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) if (packageSources.some((packageSource) => getImportedNameFromModule(node, localElementName, packageSource) === canonicalName)) {
|
|
25341
|
+
canonicalRecyclerName = canonicalName;
|
|
25342
|
+
break;
|
|
25343
|
+
}
|
|
25344
|
+
if (!canonicalRecyclerName) return;
|
|
25345
|
+
let hasSizingHint = false;
|
|
25346
|
+
let dataIsEmptyLiteral = false;
|
|
25347
|
+
let hasDataProp = false;
|
|
25348
|
+
for (const attribute of node.attributes ?? []) {
|
|
25349
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
25350
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
25351
|
+
const attributeName = attribute.name.name;
|
|
25352
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
25353
|
+
if (attributeName === "data") {
|
|
25354
|
+
hasDataProp = true;
|
|
25355
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
25356
|
+
}
|
|
25357
|
+
}
|
|
25358
|
+
if (hasSizingHint) return;
|
|
25359
|
+
if (dataIsEmptyLiteral) return;
|
|
25360
|
+
if (!hasDataProp) return;
|
|
25361
|
+
context.report({
|
|
25362
|
+
node,
|
|
25363
|
+
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`
|
|
25364
|
+
});
|
|
25365
|
+
} })
|
|
25366
|
+
});
|
|
25367
|
+
//#endregion
|
|
24212
25368
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
24213
25369
|
const RECYCLABLE_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
24214
25370
|
const rnListRecyclableWithoutTypes = defineRule({
|
|
@@ -24369,7 +25525,8 @@ const classifyPackagePlatform = (filename) => {
|
|
|
24369
25525
|
//#endregion
|
|
24370
25526
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
24371
25527
|
const isExpoManagedFileActive = (context) => {
|
|
24372
|
-
const
|
|
25528
|
+
const rawFilename = context.filename;
|
|
25529
|
+
const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
|
|
24373
25530
|
if (filename) {
|
|
24374
25531
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
24375
25532
|
if (packagePlatform === "expo") return true;
|
|
@@ -24396,7 +25553,7 @@ const rnNoDeprecatedModules = defineRule({
|
|
|
24396
25553
|
if (node.source?.value !== "react-native") return;
|
|
24397
25554
|
for (const specifier of node.specifiers ?? []) {
|
|
24398
25555
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
24399
|
-
const importedName = getImportedName(specifier);
|
|
25556
|
+
const importedName = getImportedName$1(specifier);
|
|
24400
25557
|
if (!importedName) continue;
|
|
24401
25558
|
const baseReplacement = DEPRECATED_RN_MODULE_REPLACEMENTS.get(importedName);
|
|
24402
25559
|
if (!baseReplacement) continue;
|
|
@@ -24850,6 +26007,80 @@ const rnNoRawText = defineRule({
|
|
|
24850
26007
|
}
|
|
24851
26008
|
});
|
|
24852
26009
|
//#endregion
|
|
26010
|
+
//#region src/plugin/rules/react-native/rn-no-renderitem-key.ts
|
|
26011
|
+
const collectTopLevelReturnExpressions = (functionNode) => {
|
|
26012
|
+
if (isNodeOfType(functionNode, "ArrowFunctionExpression") && functionNode.body) {
|
|
26013
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
|
|
26014
|
+
}
|
|
26015
|
+
const block = functionNode.body;
|
|
26016
|
+
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
26017
|
+
const returnExpressions = [];
|
|
26018
|
+
const visit = (node) => {
|
|
26019
|
+
if (FUNCTION_LIKE_TYPES$1.has(node.type)) return;
|
|
26020
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
26021
|
+
const nodeRecord = node;
|
|
26022
|
+
for (const fieldName of Object.keys(nodeRecord)) {
|
|
26023
|
+
if (fieldName === "parent") continue;
|
|
26024
|
+
const child = nodeRecord[fieldName];
|
|
26025
|
+
if (Array.isArray(child)) {
|
|
26026
|
+
for (const childItem of child) if (isAstNode(childItem)) visit(childItem);
|
|
26027
|
+
} else if (isAstNode(child)) visit(child);
|
|
26028
|
+
}
|
|
26029
|
+
};
|
|
26030
|
+
visit(block);
|
|
26031
|
+
return returnExpressions;
|
|
26032
|
+
};
|
|
26033
|
+
const collectReturnedJsxElements = (expression) => {
|
|
26034
|
+
const elements = [];
|
|
26035
|
+
const visit = (current) => {
|
|
26036
|
+
const unwrapped = stripParenExpression(current);
|
|
26037
|
+
if (isNodeOfType(unwrapped, "JSXElement")) {
|
|
26038
|
+
elements.push(unwrapped);
|
|
26039
|
+
return;
|
|
26040
|
+
}
|
|
26041
|
+
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
26042
|
+
visit(unwrapped.consequent);
|
|
26043
|
+
visit(unwrapped.alternate);
|
|
26044
|
+
return;
|
|
26045
|
+
}
|
|
26046
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) {
|
|
26047
|
+
visit(unwrapped.right);
|
|
26048
|
+
if (unwrapped.operator === "||" || unwrapped.operator === "??") visit(unwrapped.left);
|
|
26049
|
+
}
|
|
26050
|
+
};
|
|
26051
|
+
visit(expression);
|
|
26052
|
+
return elements;
|
|
26053
|
+
};
|
|
26054
|
+
const rnNoRenderitemKey = defineRule({
|
|
26055
|
+
id: "rn-no-renderitem-key",
|
|
26056
|
+
tags: ["test-noise"],
|
|
26057
|
+
requires: ["react-native"],
|
|
26058
|
+
severity: "warn",
|
|
26059
|
+
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`",
|
|
26060
|
+
create: (context) => ({ JSXAttribute(attributeNode) {
|
|
26061
|
+
if (!isNodeOfType(attributeNode.name, "JSXIdentifier") || !RENDER_ITEM_PROP_NAMES.has(attributeNode.name.name)) return;
|
|
26062
|
+
const openingElement = attributeNode.parent;
|
|
26063
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return;
|
|
26064
|
+
const listComponentName = resolveJsxElementName(openingElement);
|
|
26065
|
+
if (!listComponentName || !REACT_NATIVE_LIST_COMPONENTS.has(listComponentName)) return;
|
|
26066
|
+
if (!attributeNode.value || !isNodeOfType(attributeNode.value, "JSXExpressionContainer")) return;
|
|
26067
|
+
const renderFunction = attributeNode.value.expression;
|
|
26068
|
+
if (!isNodeOfType(renderFunction, "ArrowFunctionExpression") && !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
26069
|
+
const returnExpressions = collectTopLevelReturnExpressions(renderFunction);
|
|
26070
|
+
const renderPropName = attributeNode.name.name;
|
|
26071
|
+
for (const returnExpression of returnExpressions) {
|
|
26072
|
+
const returnedJsxElements = collectReturnedJsxElements(returnExpression);
|
|
26073
|
+
for (const jsxElement of returnedJsxElements) {
|
|
26074
|
+
if (!hasJsxKeyAttribute(jsxElement.openingElement)) continue;
|
|
26075
|
+
context.report({
|
|
26076
|
+
node: jsxElement.openingElement,
|
|
26077
|
+
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`
|
|
26078
|
+
});
|
|
26079
|
+
}
|
|
26080
|
+
}
|
|
26081
|
+
} })
|
|
26082
|
+
});
|
|
26083
|
+
//#endregion
|
|
24853
26084
|
//#region src/plugin/rules/react-native/rn-no-scroll-state.ts
|
|
24854
26085
|
const SET_STATE_PATTERN = /^set[A-Z]/;
|
|
24855
26086
|
const findSetStateInBody = (body) => {
|
|
@@ -25004,7 +26235,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
25004
26235
|
if (node.source?.value !== "react-native") return;
|
|
25005
26236
|
for (const specifier of node.specifiers ?? []) {
|
|
25006
26237
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25007
|
-
const importedName = getImportedName(specifier);
|
|
26238
|
+
const importedName = getImportedName$1(specifier);
|
|
25008
26239
|
if (importedName !== "Image" && importedName !== "ImageBackground") continue;
|
|
25009
26240
|
context.report({
|
|
25010
26241
|
node: specifier,
|
|
@@ -25034,7 +26265,7 @@ const rnPreferPressable = defineRule({
|
|
|
25034
26265
|
if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
|
|
25035
26266
|
for (const specifier of node.specifiers ?? []) {
|
|
25036
26267
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25037
|
-
const importedName = getImportedName(specifier);
|
|
26268
|
+
const importedName = getImportedName$1(specifier);
|
|
25038
26269
|
if (!importedName || !TOUCHABLE_COMPONENTS.has(importedName)) continue;
|
|
25039
26270
|
context.report({
|
|
25040
26271
|
node: specifier,
|
|
@@ -25044,6 +26275,81 @@ const rnPreferPressable = defineRule({
|
|
|
25044
26275
|
} })
|
|
25045
26276
|
});
|
|
25046
26277
|
//#endregion
|
|
26278
|
+
//#region src/plugin/rules/react-native/rn-prefer-pressable-over-gesture-detector.ts
|
|
26279
|
+
const COMPOSING_CHAIN_METHOD_NAMES = new Set([
|
|
26280
|
+
"simultaneousWithExternalGesture",
|
|
26281
|
+
"requireExternalGestureToFail",
|
|
26282
|
+
"blocksExternalGesture"
|
|
26283
|
+
]);
|
|
26284
|
+
const analyzeGestureChain = (expression) => {
|
|
26285
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
26286
|
+
const chainMethodNames = [];
|
|
26287
|
+
let numberOfTapsArgument = null;
|
|
26288
|
+
let cursor = expression;
|
|
26289
|
+
while (cursor && isNodeOfType(cursor, "CallExpression")) {
|
|
26290
|
+
const callExpression = cursor;
|
|
26291
|
+
const callee = callExpression.callee;
|
|
26292
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
26293
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
26294
|
+
const methodName = callee.property.name;
|
|
26295
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
26296
|
+
factoryName: methodName,
|
|
26297
|
+
chainMethodNames,
|
|
26298
|
+
numberOfTapsArgument
|
|
26299
|
+
};
|
|
26300
|
+
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
26301
|
+
chainMethodNames.push(methodName);
|
|
26302
|
+
cursor = callee.object;
|
|
26303
|
+
}
|
|
26304
|
+
return null;
|
|
26305
|
+
};
|
|
26306
|
+
const isTapChainEligibleForPressable = (chain) => {
|
|
26307
|
+
if (chain.factoryName !== "Tap") return false;
|
|
26308
|
+
for (const methodName of chain.chainMethodNames) if (COMPOSING_CHAIN_METHOD_NAMES.has(methodName)) return false;
|
|
26309
|
+
const tapsArg = chain.numberOfTapsArgument;
|
|
26310
|
+
if (tapsArg !== null) {
|
|
26311
|
+
if (!isNodeOfType(tapsArg, "Literal")) return false;
|
|
26312
|
+
if (typeof tapsArg.value !== "number") return false;
|
|
26313
|
+
if (tapsArg.value !== 1) return false;
|
|
26314
|
+
}
|
|
26315
|
+
return true;
|
|
26316
|
+
};
|
|
26317
|
+
const rnPreferPressableOverGestureDetector = defineRule({
|
|
26318
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
26319
|
+
tags: ["test-noise"],
|
|
26320
|
+
requires: ["react-native"],
|
|
26321
|
+
severity: "warn",
|
|
26322
|
+
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",
|
|
26323
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26324
|
+
if (resolveJsxElementName(node) !== "GestureDetector") return;
|
|
26325
|
+
if (!isImportedFromModule(node, "GestureDetector", "react-native-gesture-handler")) return;
|
|
26326
|
+
let gestureExpression = null;
|
|
26327
|
+
for (const attribute of node.attributes ?? []) {
|
|
26328
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26329
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
26330
|
+
if (attribute.name.name !== "gesture") continue;
|
|
26331
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) continue;
|
|
26332
|
+
gestureExpression = attribute.value.expression;
|
|
26333
|
+
break;
|
|
26334
|
+
}
|
|
26335
|
+
if (!gestureExpression) return;
|
|
26336
|
+
const resolvedExpression = stripParenExpression(gestureExpression);
|
|
26337
|
+
let chainExpression = resolvedExpression;
|
|
26338
|
+
if (isNodeOfType(resolvedExpression, "Identifier")) {
|
|
26339
|
+
const binding = findVariableInitializer(node, resolvedExpression.name);
|
|
26340
|
+
if (!binding || !binding.initializer) return;
|
|
26341
|
+
chainExpression = stripParenExpression(binding.initializer);
|
|
26342
|
+
}
|
|
26343
|
+
const chain = analyzeGestureChain(chainExpression);
|
|
26344
|
+
if (!chain) return;
|
|
26345
|
+
if (!isTapChainEligibleForPressable(chain)) return;
|
|
26346
|
+
context.report({
|
|
26347
|
+
node,
|
|
26348
|
+
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)"
|
|
26349
|
+
});
|
|
26350
|
+
} })
|
|
26351
|
+
});
|
|
26352
|
+
//#endregion
|
|
25047
26353
|
//#region src/plugin/rules/react-native/rn-prefer-reanimated.ts
|
|
25048
26354
|
const JS_THREAD_ANIMATION_IMPORTS = new Set(["Animated", "LayoutAnimation"]);
|
|
25049
26355
|
const rnPreferReanimated = defineRule({
|
|
@@ -25056,7 +26362,7 @@ const rnPreferReanimated = defineRule({
|
|
|
25056
26362
|
if (node.source?.value !== "react-native") return;
|
|
25057
26363
|
for (const specifier of node.specifiers ?? []) {
|
|
25058
26364
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25059
|
-
const importedName = getImportedName(specifier);
|
|
26365
|
+
const importedName = getImportedName$1(specifier);
|
|
25060
26366
|
if (!importedName || !JS_THREAD_ANIMATION_IMPORTS.has(importedName)) continue;
|
|
25061
26367
|
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
26368
|
context.report({
|
|
@@ -25187,6 +26493,101 @@ const rnScrollviewDynamicPadding = defineRule({
|
|
|
25187
26493
|
} })
|
|
25188
26494
|
});
|
|
25189
26495
|
//#endregion
|
|
26496
|
+
//#region src/plugin/rules/react-native/rn-scrollview-flex-in-content-container.ts
|
|
26497
|
+
const VIRTUALIZED_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
26498
|
+
const getStaticMemberKeyName = (expression) => {
|
|
26499
|
+
if (!expression.computed) {
|
|
26500
|
+
if (isNodeOfType(expression.property, "Identifier")) return expression.property.name;
|
|
26501
|
+
return null;
|
|
26502
|
+
}
|
|
26503
|
+
if (isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "string") return expression.property.value;
|
|
26504
|
+
return null;
|
|
26505
|
+
};
|
|
26506
|
+
const isStyleSheetCreateCallExpression = (expression) => {
|
|
26507
|
+
if (!expression) return false;
|
|
26508
|
+
const callExpression = stripParenExpression(expression);
|
|
26509
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26510
|
+
const callee = callExpression.callee;
|
|
26511
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "StyleSheet" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "create";
|
|
26512
|
+
};
|
|
26513
|
+
const resolveContentContainerStyleObject = (attribute) => {
|
|
26514
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) return null;
|
|
26515
|
+
if (attribute.name.name !== "contentContainerStyle") return null;
|
|
26516
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
26517
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
26518
|
+
if (isNodeOfType(expression, "ObjectExpression")) return expression;
|
|
26519
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
26520
|
+
const styleObjectKeyName = getStaticMemberKeyName(expression);
|
|
26521
|
+
if (!styleObjectKeyName) return null;
|
|
26522
|
+
const styleObjectIdentifierName = getRootIdentifierName(expression);
|
|
26523
|
+
if (!styleObjectIdentifierName) return null;
|
|
26524
|
+
const binding = findVariableInitializer(expression, styleObjectIdentifierName);
|
|
26525
|
+
if (!binding || !binding.initializer) return null;
|
|
26526
|
+
if (!isStyleSheetCreateCallExpression(binding.initializer)) return null;
|
|
26527
|
+
const argument = stripParenExpression(binding.initializer).arguments?.[0];
|
|
26528
|
+
if (!isNodeOfType(argument, "ObjectExpression")) return null;
|
|
26529
|
+
for (const property of argument.properties ?? []) {
|
|
26530
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26531
|
+
if (property.computed) continue;
|
|
26532
|
+
let matchesKey = false;
|
|
26533
|
+
if (isNodeOfType(property.key, "Identifier")) matchesKey = property.key.name === styleObjectKeyName;
|
|
26534
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") matchesKey = property.key.value === styleObjectKeyName;
|
|
26535
|
+
if (!matchesKey) continue;
|
|
26536
|
+
const propertyValue = stripParenExpression(property.value);
|
|
26537
|
+
if (isNodeOfType(propertyValue, "ObjectExpression")) return propertyValue;
|
|
26538
|
+
return null;
|
|
26539
|
+
}
|
|
26540
|
+
}
|
|
26541
|
+
return null;
|
|
26542
|
+
};
|
|
26543
|
+
const collectStyleKeyNames = (objectExpression) => {
|
|
26544
|
+
const names = /* @__PURE__ */ new Set();
|
|
26545
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26546
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26547
|
+
if (property.computed) continue;
|
|
26548
|
+
if (isNodeOfType(property.key, "Identifier")) names.add(property.key.name);
|
|
26549
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") names.add(property.key.value);
|
|
26550
|
+
}
|
|
26551
|
+
return names;
|
|
26552
|
+
};
|
|
26553
|
+
const findFlexShorthandProperty = (objectExpression) => {
|
|
26554
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26555
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26556
|
+
if (property.computed) continue;
|
|
26557
|
+
if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "flex") continue;
|
|
26558
|
+
const value = property.value;
|
|
26559
|
+
if (!isNodeOfType(value, "Literal")) return null;
|
|
26560
|
+
if (typeof value.value !== "number" || value.value <= 0) return null;
|
|
26561
|
+
return property;
|
|
26562
|
+
}
|
|
26563
|
+
return null;
|
|
26564
|
+
};
|
|
26565
|
+
const rnScrollviewFlexInContentContainer = defineRule({
|
|
26566
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
26567
|
+
tags: ["test-noise"],
|
|
26568
|
+
requires: ["react-native"],
|
|
26569
|
+
severity: "warn",
|
|
26570
|
+
recommendation: "Use `flexGrow: 1` on `contentContainerStyle` — RN's `flex: 1` shorthand sets `flexBasis: 0` and collapses the container on small devices",
|
|
26571
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26572
|
+
const elementName = resolveJsxElementName(node);
|
|
26573
|
+
if (!elementName) return;
|
|
26574
|
+
if (!SCROLLVIEW_NAMES.has(elementName) && !VIRTUALIZED_LIST_NAMES.has(elementName)) return;
|
|
26575
|
+
for (const attribute of node.attributes ?? []) {
|
|
26576
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26577
|
+
const objectExpression = resolveContentContainerStyleObject(attribute);
|
|
26578
|
+
if (!objectExpression) continue;
|
|
26579
|
+
const keyNames = collectStyleKeyNames(objectExpression);
|
|
26580
|
+
if (keyNames.has("flexGrow") || keyNames.has("flexBasis")) continue;
|
|
26581
|
+
const flexProperty = findFlexShorthandProperty(objectExpression);
|
|
26582
|
+
if (!flexProperty) continue;
|
|
26583
|
+
context.report({
|
|
26584
|
+
node: flexProperty,
|
|
26585
|
+
message: `<${elementName}> contentContainerStyle uses \`flex: <number>\` — RN's flex shorthand sets flexBasis: 0 and collapses the container on small devices. Use \`flexGrow: 1\` instead`
|
|
26586
|
+
});
|
|
26587
|
+
}
|
|
26588
|
+
} })
|
|
26589
|
+
});
|
|
26590
|
+
//#endregion
|
|
25190
26591
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
25191
26592
|
const LEGACY_SHADOW_KEYS = new Set([
|
|
25192
26593
|
"shadowColor",
|
|
@@ -28746,7 +30147,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
28746
30147
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
28747
30148
|
let current = node.parent;
|
|
28748
30149
|
while (current) {
|
|
28749
|
-
if (isFunctionLike(current)) {
|
|
30150
|
+
if (isFunctionLike$1(current)) {
|
|
28750
30151
|
const resolvedName = inferFunctionName(current);
|
|
28751
30152
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
28752
30153
|
}
|
|
@@ -28767,7 +30168,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
28767
30168
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
28768
30169
|
let current = node.parent;
|
|
28769
30170
|
while (current) {
|
|
28770
|
-
if (isFunctionLike(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30171
|
+
if (isFunctionLike$1(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
28771
30172
|
current = current.parent ?? null;
|
|
28772
30173
|
}
|
|
28773
30174
|
return false;
|
|
@@ -29101,7 +30502,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
29101
30502
|
let foundAuthCall = false;
|
|
29102
30503
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
29103
30504
|
if (foundAuthCall) return;
|
|
29104
|
-
if (isFunctionLike(child)) return false;
|
|
30505
|
+
if (isFunctionLike$1(child)) return false;
|
|
29105
30506
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
29106
30507
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
29107
30508
|
});
|
|
@@ -29293,7 +30694,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
29293
30694
|
let isServerSideFile = false;
|
|
29294
30695
|
return {
|
|
29295
30696
|
Program(node) {
|
|
29296
|
-
const filename = normalizeFilename$1(context.
|
|
30697
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29297
30698
|
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
29298
30699
|
isServerSideFile = false;
|
|
29299
30700
|
return;
|
|
@@ -29406,7 +30807,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
29406
30807
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
|
|
29407
30808
|
},
|
|
29408
30809
|
ExportDefaultDeclaration(node) {
|
|
29409
|
-
const filename = normalizeFilename$1(context.
|
|
30810
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29410
30811
|
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
29411
30812
|
const declaration = node.declaration;
|
|
29412
30813
|
if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
|
|
@@ -29957,7 +31358,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29957
31358
|
};
|
|
29958
31359
|
return {
|
|
29959
31360
|
Program(node) {
|
|
29960
|
-
const filename = context.
|
|
31361
|
+
const filename = context.filename ?? "";
|
|
29961
31362
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29962
31363
|
const statements = node.body ?? [];
|
|
29963
31364
|
for (const statement of statements) collectImportBindings(statement);
|
|
@@ -29967,17 +31368,17 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29967
31368
|
}
|
|
29968
31369
|
},
|
|
29969
31370
|
ImportDeclaration(node) {
|
|
29970
|
-
const filename = context.
|
|
31371
|
+
const filename = context.filename ?? "";
|
|
29971
31372
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29972
31373
|
collectImportBindings(node);
|
|
29973
31374
|
},
|
|
29974
31375
|
VariableDeclarator(node) {
|
|
29975
|
-
const filename = context.
|
|
31376
|
+
const filename = context.filename ?? "";
|
|
29976
31377
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29977
31378
|
collectVariableAlias(node);
|
|
29978
31379
|
},
|
|
29979
31380
|
JSXOpeningElement(node) {
|
|
29980
|
-
const filename = normalizeFilename$1(context.
|
|
31381
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29981
31382
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29982
31383
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
29983
31384
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -29992,7 +31393,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29992
31393
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
29993
31394
|
},
|
|
29994
31395
|
"Program:exit"(programNode) {
|
|
29995
|
-
const filename = normalizeFilename$1(context.
|
|
31396
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29996
31397
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29997
31398
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
29998
31399
|
node: programNode,
|
|
@@ -30011,7 +31412,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
30011
31412
|
severity: "warn",
|
|
30012
31413
|
recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
30013
31414
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
30014
|
-
const filename = normalizeFilename$1(context.
|
|
31415
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30015
31416
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30016
31417
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
30017
31418
|
const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
|
|
@@ -30085,7 +31486,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
30085
31486
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && typeof node.name.name === "string" && node.name.name.startsWith("on") && UPPERCASE_PATTERN.test(node.name.name.charAt(2));
|
|
30086
31487
|
return {
|
|
30087
31488
|
CallExpression(node) {
|
|
30088
|
-
const filename = normalizeFilename$1(context.
|
|
31489
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30089
31490
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30090
31491
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
30091
31492
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
@@ -30095,17 +31496,17 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
30095
31496
|
});
|
|
30096
31497
|
},
|
|
30097
31498
|
"CallExpression:exit"(node) {
|
|
30098
|
-
const filename = normalizeFilename$1(context.
|
|
31499
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30099
31500
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30100
31501
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
30101
31502
|
},
|
|
30102
31503
|
JSXAttribute(node) {
|
|
30103
|
-
const filename = normalizeFilename$1(context.
|
|
31504
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30104
31505
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30105
31506
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
30106
31507
|
},
|
|
30107
31508
|
"JSXAttribute:exit"(node) {
|
|
30108
|
-
const filename = normalizeFilename$1(context.
|
|
31509
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30109
31510
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30110
31511
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
30111
31512
|
}
|
|
@@ -30186,7 +31587,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
30186
31587
|
severity: "warn",
|
|
30187
31588
|
recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
30188
31589
|
create: (context) => ({ CallExpression(node) {
|
|
30189
|
-
const filename = normalizeFilename$1(context.
|
|
31590
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30190
31591
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30191
31592
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
30192
31593
|
const callback = node.arguments?.[0];
|
|
@@ -30353,7 +31754,7 @@ const useLazyMotion = defineRule({
|
|
|
30353
31754
|
if (node.specifiers?.some((specifier) => {
|
|
30354
31755
|
if (!isNodeOfType(specifier, "ImportSpecifier")) return false;
|
|
30355
31756
|
if (specifier.importKind === "type") return false;
|
|
30356
|
-
return getImportedName(specifier) === "motion";
|
|
31757
|
+
return getImportedName$1(specifier) === "motion";
|
|
30357
31758
|
})) context.report({
|
|
30358
31759
|
node,
|
|
30359
31760
|
message: "Import \"m\" with LazyMotion instead of \"motion\" — saves ~30kb in bundle size"
|
|
@@ -30434,6 +31835,17 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
30434
31835
|
//#endregion
|
|
30435
31836
|
//#region src/plugin/rule-registry.ts
|
|
30436
31837
|
const reactDoctorRules = [
|
|
31838
|
+
{
|
|
31839
|
+
key: "react-doctor/activity-wraps-effect-heavy-subtree",
|
|
31840
|
+
id: "activity-wraps-effect-heavy-subtree",
|
|
31841
|
+
source: "react-doctor",
|
|
31842
|
+
originallyExternal: false,
|
|
31843
|
+
rule: {
|
|
31844
|
+
...activityWrapsEffectHeavySubtree,
|
|
31845
|
+
framework: "global",
|
|
31846
|
+
category: "State & Effects"
|
|
31847
|
+
}
|
|
31848
|
+
},
|
|
30437
31849
|
{
|
|
30438
31850
|
key: "react-doctor/advanced-event-handler-refs",
|
|
30439
31851
|
id: "advanced-event-handler-refs",
|
|
@@ -30819,6 +32231,17 @@ const reactDoctorRules = [
|
|
|
30819
32231
|
category: "Architecture"
|
|
30820
32232
|
}
|
|
30821
32233
|
},
|
|
32234
|
+
{
|
|
32235
|
+
key: "react-doctor/hooks-no-nan-in-deps",
|
|
32236
|
+
id: "hooks-no-nan-in-deps",
|
|
32237
|
+
source: "react-doctor",
|
|
32238
|
+
originallyExternal: false,
|
|
32239
|
+
rule: {
|
|
32240
|
+
...hooksNoNanInDeps,
|
|
32241
|
+
framework: "global",
|
|
32242
|
+
category: "State & Effects"
|
|
32243
|
+
}
|
|
32244
|
+
},
|
|
30822
32245
|
{
|
|
30823
32246
|
key: "react-doctor/html-has-lang",
|
|
30824
32247
|
id: "html-has-lang",
|
|
@@ -30830,6 +32253,39 @@ const reactDoctorRules = [
|
|
|
30830
32253
|
category: "Accessibility"
|
|
30831
32254
|
}
|
|
30832
32255
|
},
|
|
32256
|
+
{
|
|
32257
|
+
key: "react-doctor/html-no-invalid-paragraph-child",
|
|
32258
|
+
id: "html-no-invalid-paragraph-child",
|
|
32259
|
+
source: "react-doctor",
|
|
32260
|
+
originallyExternal: false,
|
|
32261
|
+
rule: {
|
|
32262
|
+
...htmlNoInvalidParagraphChild,
|
|
32263
|
+
framework: "global",
|
|
32264
|
+
category: "Correctness"
|
|
32265
|
+
}
|
|
32266
|
+
},
|
|
32267
|
+
{
|
|
32268
|
+
key: "react-doctor/html-no-invalid-table-nesting",
|
|
32269
|
+
id: "html-no-invalid-table-nesting",
|
|
32270
|
+
source: "react-doctor",
|
|
32271
|
+
originallyExternal: false,
|
|
32272
|
+
rule: {
|
|
32273
|
+
...htmlNoInvalidTableNesting,
|
|
32274
|
+
framework: "global",
|
|
32275
|
+
category: "Correctness"
|
|
32276
|
+
}
|
|
32277
|
+
},
|
|
32278
|
+
{
|
|
32279
|
+
key: "react-doctor/html-no-nested-interactive",
|
|
32280
|
+
id: "html-no-nested-interactive",
|
|
32281
|
+
source: "react-doctor",
|
|
32282
|
+
originallyExternal: false,
|
|
32283
|
+
rule: {
|
|
32284
|
+
...htmlNoNestedInteractive,
|
|
32285
|
+
framework: "global",
|
|
32286
|
+
category: "Correctness"
|
|
32287
|
+
}
|
|
32288
|
+
},
|
|
30833
32289
|
{
|
|
30834
32290
|
key: "react-doctor/iframe-has-title",
|
|
30835
32291
|
id: "iframe-has-title",
|
|
@@ -30874,6 +32330,50 @@ const reactDoctorRules = [
|
|
|
30874
32330
|
category: "Accessibility"
|
|
30875
32331
|
}
|
|
30876
32332
|
},
|
|
32333
|
+
{
|
|
32334
|
+
key: "react-doctor/jotai-derived-atom-returns-fresh-object",
|
|
32335
|
+
id: "jotai-derived-atom-returns-fresh-object",
|
|
32336
|
+
source: "react-doctor",
|
|
32337
|
+
originallyExternal: false,
|
|
32338
|
+
rule: {
|
|
32339
|
+
...jotaiDerivedAtomReturnsFreshObject,
|
|
32340
|
+
framework: "global",
|
|
32341
|
+
category: "State & Effects"
|
|
32342
|
+
}
|
|
32343
|
+
},
|
|
32344
|
+
{
|
|
32345
|
+
key: "react-doctor/jotai-select-atom-in-render-body",
|
|
32346
|
+
id: "jotai-select-atom-in-render-body",
|
|
32347
|
+
source: "react-doctor",
|
|
32348
|
+
originallyExternal: false,
|
|
32349
|
+
rule: {
|
|
32350
|
+
...jotaiSelectAtomInRenderBody,
|
|
32351
|
+
framework: "global",
|
|
32352
|
+
category: "State & Effects"
|
|
32353
|
+
}
|
|
32354
|
+
},
|
|
32355
|
+
{
|
|
32356
|
+
key: "react-doctor/jotai-tq-use-raw-query-atom",
|
|
32357
|
+
id: "jotai-tq-use-raw-query-atom",
|
|
32358
|
+
source: "react-doctor",
|
|
32359
|
+
originallyExternal: false,
|
|
32360
|
+
rule: {
|
|
32361
|
+
...jotaiTqUseRawQueryAtom,
|
|
32362
|
+
framework: "global",
|
|
32363
|
+
category: "State & Effects"
|
|
32364
|
+
}
|
|
32365
|
+
},
|
|
32366
|
+
{
|
|
32367
|
+
key: "react-doctor/js-async-reduce-without-awaited-acc",
|
|
32368
|
+
id: "js-async-reduce-without-awaited-acc",
|
|
32369
|
+
source: "react-doctor",
|
|
32370
|
+
originallyExternal: false,
|
|
32371
|
+
rule: {
|
|
32372
|
+
...jsAsyncReduceWithoutAwaitedAcc,
|
|
32373
|
+
framework: "global",
|
|
32374
|
+
category: "Performance"
|
|
32375
|
+
}
|
|
32376
|
+
},
|
|
30877
32377
|
{
|
|
30878
32378
|
key: "react-doctor/js-batch-dom-css",
|
|
30879
32379
|
id: "js-batch-dom-css",
|
|
@@ -32601,6 +34101,61 @@ const reactDoctorRules = [
|
|
|
32601
34101
|
category: "Architecture"
|
|
32602
34102
|
}
|
|
32603
34103
|
},
|
|
34104
|
+
{
|
|
34105
|
+
key: "react-doctor/preact-no-children-length",
|
|
34106
|
+
id: "preact-no-children-length",
|
|
34107
|
+
source: "react-doctor",
|
|
34108
|
+
originallyExternal: false,
|
|
34109
|
+
rule: {
|
|
34110
|
+
...preactNoChildrenLength,
|
|
34111
|
+
framework: "preact",
|
|
34112
|
+
category: "Preact"
|
|
34113
|
+
}
|
|
34114
|
+
},
|
|
34115
|
+
{
|
|
34116
|
+
key: "react-doctor/preact-no-react-hooks-import",
|
|
34117
|
+
id: "preact-no-react-hooks-import",
|
|
34118
|
+
source: "react-doctor",
|
|
34119
|
+
originallyExternal: false,
|
|
34120
|
+
rule: {
|
|
34121
|
+
...preactNoReactHooksImport,
|
|
34122
|
+
framework: "preact",
|
|
34123
|
+
category: "Preact"
|
|
34124
|
+
}
|
|
34125
|
+
},
|
|
34126
|
+
{
|
|
34127
|
+
key: "react-doctor/preact-no-render-arguments",
|
|
34128
|
+
id: "preact-no-render-arguments",
|
|
34129
|
+
source: "react-doctor",
|
|
34130
|
+
originallyExternal: false,
|
|
34131
|
+
rule: {
|
|
34132
|
+
...preactNoRenderArguments,
|
|
34133
|
+
framework: "preact",
|
|
34134
|
+
category: "Preact"
|
|
34135
|
+
}
|
|
34136
|
+
},
|
|
34137
|
+
{
|
|
34138
|
+
key: "react-doctor/preact-prefer-ondblclick",
|
|
34139
|
+
id: "preact-prefer-ondblclick",
|
|
34140
|
+
source: "react-doctor",
|
|
34141
|
+
originallyExternal: false,
|
|
34142
|
+
rule: {
|
|
34143
|
+
...preactPreferOndblclick,
|
|
34144
|
+
framework: "preact",
|
|
34145
|
+
category: "Preact"
|
|
34146
|
+
}
|
|
34147
|
+
},
|
|
34148
|
+
{
|
|
34149
|
+
key: "react-doctor/preact-prefer-oninput",
|
|
34150
|
+
id: "preact-prefer-oninput",
|
|
34151
|
+
source: "react-doctor",
|
|
34152
|
+
originallyExternal: false,
|
|
34153
|
+
rule: {
|
|
34154
|
+
...preactPreferOninput,
|
|
34155
|
+
framework: "preact",
|
|
34156
|
+
category: "Preact"
|
|
34157
|
+
}
|
|
34158
|
+
},
|
|
32604
34159
|
{
|
|
32605
34160
|
key: "react-doctor/prefer-dynamic-import",
|
|
32606
34161
|
id: "prefer-dynamic-import",
|
|
@@ -32634,6 +34189,17 @@ const reactDoctorRules = [
|
|
|
32634
34189
|
category: "Architecture"
|
|
32635
34190
|
}
|
|
32636
34191
|
},
|
|
34192
|
+
{
|
|
34193
|
+
key: "react-doctor/prefer-html-dialog",
|
|
34194
|
+
id: "prefer-html-dialog",
|
|
34195
|
+
source: "react-doctor",
|
|
34196
|
+
originallyExternal: false,
|
|
34197
|
+
rule: {
|
|
34198
|
+
...preferHtmlDialog,
|
|
34199
|
+
framework: "global",
|
|
34200
|
+
category: "Accessibility"
|
|
34201
|
+
}
|
|
34202
|
+
},
|
|
32637
34203
|
{
|
|
32638
34204
|
key: "react-doctor/prefer-tag-over-role",
|
|
32639
34205
|
id: "prefer-tag-over-role",
|
|
@@ -33035,6 +34601,18 @@ const reactDoctorRules = [
|
|
|
33035
34601
|
tags: [...new Set(["react-native", ...rnListDataMapped.tags ?? []])]
|
|
33036
34602
|
}
|
|
33037
34603
|
},
|
|
34604
|
+
{
|
|
34605
|
+
key: "react-doctor/rn-list-missing-estimated-item-size",
|
|
34606
|
+
id: "rn-list-missing-estimated-item-size",
|
|
34607
|
+
source: "react-doctor",
|
|
34608
|
+
originallyExternal: false,
|
|
34609
|
+
rule: {
|
|
34610
|
+
...rnListMissingEstimatedItemSize,
|
|
34611
|
+
framework: "react-native",
|
|
34612
|
+
category: "React Native",
|
|
34613
|
+
tags: [...new Set(["react-native", ...rnListMissingEstimatedItemSize.tags ?? []])]
|
|
34614
|
+
}
|
|
34615
|
+
},
|
|
33038
34616
|
{
|
|
33039
34617
|
key: "react-doctor/rn-list-recyclable-without-types",
|
|
33040
34618
|
id: "rn-list-recyclable-without-types",
|
|
@@ -33155,6 +34733,18 @@ const reactDoctorRules = [
|
|
|
33155
34733
|
tags: [...new Set(["react-native", ...rnNoRawText.tags ?? []])]
|
|
33156
34734
|
}
|
|
33157
34735
|
},
|
|
34736
|
+
{
|
|
34737
|
+
key: "react-doctor/rn-no-renderitem-key",
|
|
34738
|
+
id: "rn-no-renderitem-key",
|
|
34739
|
+
source: "react-doctor",
|
|
34740
|
+
originallyExternal: false,
|
|
34741
|
+
rule: {
|
|
34742
|
+
...rnNoRenderitemKey,
|
|
34743
|
+
framework: "react-native",
|
|
34744
|
+
category: "React Native",
|
|
34745
|
+
tags: [...new Set(["react-native", ...rnNoRenderitemKey.tags ?? []])]
|
|
34746
|
+
}
|
|
34747
|
+
},
|
|
33158
34748
|
{
|
|
33159
34749
|
key: "react-doctor/rn-no-scroll-state",
|
|
33160
34750
|
id: "rn-no-scroll-state",
|
|
@@ -33227,6 +34817,18 @@ const reactDoctorRules = [
|
|
|
33227
34817
|
tags: [...new Set(["react-native", ...rnPreferPressable.tags ?? []])]
|
|
33228
34818
|
}
|
|
33229
34819
|
},
|
|
34820
|
+
{
|
|
34821
|
+
key: "react-doctor/rn-prefer-pressable-over-gesture-detector",
|
|
34822
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
34823
|
+
source: "react-doctor",
|
|
34824
|
+
originallyExternal: false,
|
|
34825
|
+
rule: {
|
|
34826
|
+
...rnPreferPressableOverGestureDetector,
|
|
34827
|
+
framework: "react-native",
|
|
34828
|
+
category: "React Native",
|
|
34829
|
+
tags: [...new Set(["react-native", ...rnPreferPressableOverGestureDetector.tags ?? []])]
|
|
34830
|
+
}
|
|
34831
|
+
},
|
|
33230
34832
|
{
|
|
33231
34833
|
key: "react-doctor/rn-prefer-reanimated",
|
|
33232
34834
|
id: "rn-prefer-reanimated",
|
|
@@ -33263,6 +34865,18 @@ const reactDoctorRules = [
|
|
|
33263
34865
|
tags: [...new Set(["react-native", ...rnScrollviewDynamicPadding.tags ?? []])]
|
|
33264
34866
|
}
|
|
33265
34867
|
},
|
|
34868
|
+
{
|
|
34869
|
+
key: "react-doctor/rn-scrollview-flex-in-content-container",
|
|
34870
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
34871
|
+
source: "react-doctor",
|
|
34872
|
+
originallyExternal: false,
|
|
34873
|
+
rule: {
|
|
34874
|
+
...rnScrollviewFlexInContentContainer,
|
|
34875
|
+
framework: "react-native",
|
|
34876
|
+
category: "React Native",
|
|
34877
|
+
tags: [...new Set(["react-native", ...rnScrollviewFlexInContentContainer.tags ?? []])]
|
|
34878
|
+
}
|
|
34879
|
+
},
|
|
33266
34880
|
{
|
|
33267
34881
|
key: "react-doctor/rn-style-prefer-boxshadow",
|
|
33268
34882
|
id: "rn-style-prefer-boxshadow",
|
|
@@ -33642,7 +35256,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
|
|
|
33642
35256
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
33643
35257
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
33644
35258
|
const isReactNativeFileActive = (context) => {
|
|
33645
|
-
const rawFilename = context.
|
|
35259
|
+
const rawFilename = context.filename;
|
|
33646
35260
|
if (!rawFilename) return true;
|
|
33647
35261
|
const filename = normalizeFilename$1(rawFilename);
|
|
33648
35262
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
@@ -33696,7 +35310,7 @@ const appendNode = (builder, block, node) => {
|
|
|
33696
35310
|
};
|
|
33697
35311
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
33698
35312
|
builder.nodeBlock.set(node, block);
|
|
33699
|
-
if (isFunctionLike(node)) return;
|
|
35313
|
+
if (isFunctionLike$1(node)) return;
|
|
33700
35314
|
const record = node;
|
|
33701
35315
|
for (const key of Object.keys(record)) {
|
|
33702
35316
|
if (key === "parent") continue;
|
|
@@ -34034,7 +35648,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34034
35648
|
body: program.body
|
|
34035
35649
|
});
|
|
34036
35650
|
const visit = (node) => {
|
|
34037
|
-
if (isFunctionLike(node)) {
|
|
35651
|
+
if (isFunctionLike$1(node)) {
|
|
34038
35652
|
const body = node.body;
|
|
34039
35653
|
if (body) buildFor(node, body);
|
|
34040
35654
|
}
|
|
@@ -34051,7 +35665,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34051
35665
|
const enclosingFunction = (node) => {
|
|
34052
35666
|
let current = node;
|
|
34053
35667
|
while (current) {
|
|
34054
|
-
if (isFunctionLike(current)) return current;
|
|
35668
|
+
if (isFunctionLike$1(current)) return current;
|
|
34055
35669
|
if (isNodeOfType(current, "Program")) return current;
|
|
34056
35670
|
current = current.parent ?? null;
|
|
34057
35671
|
}
|
|
@@ -34130,7 +35744,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
34130
35744
|
};
|
|
34131
35745
|
const enrichedContext = {
|
|
34132
35746
|
report: baseContext.report,
|
|
34133
|
-
|
|
35747
|
+
get filename() {
|
|
35748
|
+
return baseContext.filename ?? baseContext.getFilename?.();
|
|
35749
|
+
},
|
|
34134
35750
|
settings: baseContext.settings,
|
|
34135
35751
|
get scopes() {
|
|
34136
35752
|
return getScopes();
|
|
@@ -34273,6 +35889,7 @@ const NEXTJS_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramewor
|
|
|
34273
35889
|
const REACT_NATIVE_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("react-native")));
|
|
34274
35890
|
const TANSTACK_START_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-start")));
|
|
34275
35891
|
const TANSTACK_QUERY_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-query")));
|
|
35892
|
+
const PREACT_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("preact")));
|
|
34276
35893
|
const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES));
|
|
34277
35894
|
const ALL_REACT_DOCTOR_RULE_KEYS = new Set(REACT_DOCTOR_RULES.map((rule) => rule.key));
|
|
34278
35895
|
const FRAMEWORK_SPECIFIC_RULE_KEYS = collectFrameworkSpecificRuleKeys();
|
|
@@ -34281,6 +35898,6 @@ const REACT_COMPILER_RULES = toRuleMap(collectExternalRulesBySource("react-compi
|
|
|
34281
35898
|
//#region src/index.ts
|
|
34282
35899
|
var src_default = plugin;
|
|
34283
35900
|
//#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 };
|
|
35901
|
+
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
35902
|
|
|
34286
35903
|
//# sourceMappingURL=index.js.map
|