oxlint-plugin-react-doctor 0.2.9 → 0.2.11-dev.f036b0f
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 -45
- package/dist/index.js +2302 -790
- 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",
|
|
@@ -669,6 +813,12 @@ const getElementType = (openingElement, settings) => {
|
|
|
669
813
|
return baseName;
|
|
670
814
|
};
|
|
671
815
|
//#endregion
|
|
816
|
+
//#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
|
|
817
|
+
const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
818
|
+
if (!rawFilename) return false;
|
|
819
|
+
return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
|
|
820
|
+
};
|
|
821
|
+
//#endregion
|
|
672
822
|
//#region src/plugin/utils/is-hidden-from-screen-reader.ts
|
|
673
823
|
const isHiddenFromScreenReader = (openingElement, settings) => {
|
|
674
824
|
if (getElementType(openingElement, settings).toLowerCase() === "input") {
|
|
@@ -839,6 +989,7 @@ const altText = defineRule({
|
|
|
839
989
|
recommendation: "Provide `alt` (or aria-label / aria-labelledby) for non-decorative images.",
|
|
840
990
|
category: "Accessibility",
|
|
841
991
|
create: (context) => {
|
|
992
|
+
if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
|
|
842
993
|
const settings = resolveSettings$53(context.settings);
|
|
843
994
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
844
995
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
@@ -875,7 +1026,7 @@ const altText = defineRule({
|
|
|
875
1026
|
});
|
|
876
1027
|
//#endregion
|
|
877
1028
|
//#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
|
|
878
|
-
const buildMessage$
|
|
1029
|
+
const buildMessage$28 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
|
|
879
1030
|
const DEFAULT_AMBIGUOUS = [
|
|
880
1031
|
"click here",
|
|
881
1032
|
"here",
|
|
@@ -932,14 +1083,14 @@ const anchorAmbiguousText = defineRule({
|
|
|
932
1083
|
const normalized = normalizeText(accessibleText);
|
|
933
1084
|
if (ambiguousSet.has(normalized)) context.report({
|
|
934
1085
|
node: node.openingElement.name,
|
|
935
|
-
message: buildMessage$
|
|
1086
|
+
message: buildMessage$28(normalized)
|
|
936
1087
|
});
|
|
937
1088
|
} };
|
|
938
1089
|
}
|
|
939
1090
|
});
|
|
940
1091
|
//#endregion
|
|
941
1092
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
942
|
-
const MESSAGE$
|
|
1093
|
+
const MESSAGE$47 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
|
|
943
1094
|
const anchorHasContent = defineRule({
|
|
944
1095
|
id: "anchor-has-content",
|
|
945
1096
|
tags: ["react-jsx-only"],
|
|
@@ -954,7 +1105,7 @@ const anchorHasContent = defineRule({
|
|
|
954
1105
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
955
1106
|
context.report({
|
|
956
1107
|
node: opening.name,
|
|
957
|
-
message: MESSAGE$
|
|
1108
|
+
message: MESSAGE$47
|
|
958
1109
|
});
|
|
959
1110
|
} })
|
|
960
1111
|
});
|
|
@@ -1347,7 +1498,7 @@ const parseJsxValue = (value) => {
|
|
|
1347
1498
|
};
|
|
1348
1499
|
//#endregion
|
|
1349
1500
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
1350
|
-
const MESSAGE$
|
|
1501
|
+
const MESSAGE$46 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
|
|
1351
1502
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
1352
1503
|
id: "aria-activedescendant-has-tabindex",
|
|
1353
1504
|
tags: ["react-jsx-only"],
|
|
@@ -1364,14 +1515,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
1364
1515
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
1365
1516
|
context.report({
|
|
1366
1517
|
node: node.name,
|
|
1367
|
-
message: MESSAGE$
|
|
1518
|
+
message: MESSAGE$46
|
|
1368
1519
|
});
|
|
1369
1520
|
return;
|
|
1370
1521
|
}
|
|
1371
1522
|
if (isInteractiveElement(tag, node)) return;
|
|
1372
1523
|
context.report({
|
|
1373
1524
|
node: node.name,
|
|
1374
|
-
message: MESSAGE$
|
|
1525
|
+
message: MESSAGE$46
|
|
1375
1526
|
});
|
|
1376
1527
|
} })
|
|
1377
1528
|
});
|
|
@@ -1511,7 +1662,7 @@ const ARIA_PROPERTIES = new Map([
|
|
|
1511
1662
|
const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
|
|
1512
1663
|
//#endregion
|
|
1513
1664
|
//#region src/plugin/rules/a11y/aria-props.ts
|
|
1514
|
-
const buildMessage$
|
|
1665
|
+
const buildMessage$27 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
|
|
1515
1666
|
const ariaProps = defineRule({
|
|
1516
1667
|
id: "aria-props",
|
|
1517
1668
|
tags: ["react-jsx-only"],
|
|
@@ -1524,7 +1675,7 @@ const ariaProps = defineRule({
|
|
|
1524
1675
|
if (!name || !name.startsWith("aria-")) return;
|
|
1525
1676
|
if (!isValidAriaProperty(name)) context.report({
|
|
1526
1677
|
node: node.name,
|
|
1527
|
-
message: buildMessage$
|
|
1678
|
+
message: buildMessage$27(name)
|
|
1528
1679
|
});
|
|
1529
1680
|
} })
|
|
1530
1681
|
});
|
|
@@ -1675,7 +1826,7 @@ const buildExpectedDescription = (propType) => {
|
|
|
1675
1826
|
case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
|
|
1676
1827
|
}
|
|
1677
1828
|
};
|
|
1678
|
-
const buildMessage$
|
|
1829
|
+
const buildMessage$26 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
|
|
1679
1830
|
const allowNoneValue = (propType) => {
|
|
1680
1831
|
switch (propType.kind) {
|
|
1681
1832
|
case "boolean":
|
|
@@ -1808,13 +1959,13 @@ const ariaProptypes = defineRule({
|
|
|
1808
1959
|
if (!node.value) {
|
|
1809
1960
|
if (!allowNoneValue(propType)) context.report({
|
|
1810
1961
|
node,
|
|
1811
|
-
message: buildMessage$
|
|
1962
|
+
message: buildMessage$26(propName, propType)
|
|
1812
1963
|
});
|
|
1813
1964
|
return;
|
|
1814
1965
|
}
|
|
1815
1966
|
if (!isValidValueForType(propType, node.value)) context.report({
|
|
1816
1967
|
node,
|
|
1817
|
-
message: buildMessage$
|
|
1968
|
+
message: buildMessage$26(propName, propType)
|
|
1818
1969
|
});
|
|
1819
1970
|
} })
|
|
1820
1971
|
});
|
|
@@ -2126,7 +2277,7 @@ const ariaRole = defineRule({
|
|
|
2126
2277
|
});
|
|
2127
2278
|
//#endregion
|
|
2128
2279
|
//#region src/plugin/rules/a11y/aria-unsupported-elements.ts
|
|
2129
|
-
const buildMessage$
|
|
2280
|
+
const buildMessage$25 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
|
|
2130
2281
|
const ariaUnsupportedElements = defineRule({
|
|
2131
2282
|
id: "aria-unsupported-elements",
|
|
2132
2283
|
tags: ["react-jsx-only"],
|
|
@@ -2143,7 +2294,7 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2143
2294
|
if (!attrName) continue;
|
|
2144
2295
|
if (attrName.startsWith("aria-") || attrName === "role") context.report({
|
|
2145
2296
|
node: attribute,
|
|
2146
|
-
message: buildMessage$
|
|
2297
|
+
message: buildMessage$25(tag, attrName)
|
|
2147
2298
|
});
|
|
2148
2299
|
}
|
|
2149
2300
|
} })
|
|
@@ -2398,7 +2549,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2398
2549
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2399
2550
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2400
2551
|
*/
|
|
2401
|
-
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2552
|
+
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2402
2553
|
//#endregion
|
|
2403
2554
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2404
2555
|
/**
|
|
@@ -2421,7 +2572,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2421
2572
|
let firstAwait = null;
|
|
2422
2573
|
walkAst(block, (child) => {
|
|
2423
2574
|
if (firstAwait) return false;
|
|
2424
|
-
if (child !== block && isFunctionLike(child)) return false;
|
|
2575
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
2425
2576
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2426
2577
|
});
|
|
2427
2578
|
return firstAwait;
|
|
@@ -2568,6 +2719,17 @@ const asyncAwaitInLoop = defineRule({
|
|
|
2568
2719
|
}
|
|
2569
2720
|
});
|
|
2570
2721
|
//#endregion
|
|
2722
|
+
//#region src/plugin/constants/ts-type-position-keys.ts
|
|
2723
|
+
const TYPE_POSITION_CHILD_KEYS = new Set([
|
|
2724
|
+
"implements",
|
|
2725
|
+
"returnType",
|
|
2726
|
+
"superTypeArguments",
|
|
2727
|
+
"superTypeParameters",
|
|
2728
|
+
"typeAnnotation",
|
|
2729
|
+
"typeArguments",
|
|
2730
|
+
"typeParameters"
|
|
2731
|
+
]);
|
|
2732
|
+
//#endregion
|
|
2571
2733
|
//#region src/plugin/utils/collect-pattern-names.ts
|
|
2572
2734
|
const collectPatternNames = (pattern, into) => {
|
|
2573
2735
|
if (!pattern) return;
|
|
@@ -2597,14 +2759,6 @@ const collectPatternNames = (pattern, into) => {
|
|
|
2597
2759
|
};
|
|
2598
2760
|
//#endregion
|
|
2599
2761
|
//#region src/plugin/utils/collect-reference-identifier-names.ts
|
|
2600
|
-
const TYPE_POSITION_KEYS = new Set([
|
|
2601
|
-
"typeAnnotation",
|
|
2602
|
-
"typeParameters",
|
|
2603
|
-
"typeArguments",
|
|
2604
|
-
"returnType",
|
|
2605
|
-
"superTypeArguments",
|
|
2606
|
-
"superTypeParameters"
|
|
2607
|
-
]);
|
|
2608
2762
|
const collectScopedReferencesInPattern = (pattern, into, shadowed) => {
|
|
2609
2763
|
if (!pattern) return;
|
|
2610
2764
|
if (isNodeOfType(pattern, "Identifier")) return;
|
|
@@ -2665,7 +2819,7 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
2665
2819
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
2666
2820
|
for (const [key, child] of Object.entries(node)) {
|
|
2667
2821
|
if (key === "parent") continue;
|
|
2668
|
-
if (
|
|
2822
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
2669
2823
|
if (Array.isArray(child)) {
|
|
2670
2824
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
2671
2825
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -2873,13 +3027,13 @@ const asyncDeferAwait = defineRule({
|
|
|
2873
3027
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
2874
3028
|
if (!functionBody) return;
|
|
2875
3029
|
walkAst(functionBody, (descendant) => {
|
|
2876
|
-
if (isFunctionLike(descendant)) return false;
|
|
3030
|
+
if (isFunctionLike$1(descendant)) return false;
|
|
2877
3031
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
2878
3032
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
2879
3033
|
});
|
|
2880
3034
|
};
|
|
2881
3035
|
const enterFunction = (node) => {
|
|
2882
|
-
if (!isFunctionLike(node)) return;
|
|
3036
|
+
if (!isFunctionLike$1(node)) return;
|
|
2883
3037
|
if (!node.async) return;
|
|
2884
3038
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
2885
3039
|
inspectAllStatementBlocks(node.body);
|
|
@@ -2983,7 +3137,7 @@ const asyncParallel = defineRule({
|
|
|
2983
3137
|
severity: "warn",
|
|
2984
3138
|
recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
2985
3139
|
create: (context) => {
|
|
2986
|
-
const filename = normalizeFilename$1(context.
|
|
3140
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
2987
3141
|
const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
|
|
2988
3142
|
let hasTestLibraryImport = false;
|
|
2989
3143
|
const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
|
|
@@ -3010,7 +3164,7 @@ const asyncParallel = defineRule({
|
|
|
3010
3164
|
});
|
|
3011
3165
|
//#endregion
|
|
3012
3166
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
3013
|
-
const buildMessage$
|
|
3167
|
+
const buildMessage$24 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
|
|
3014
3168
|
const AUTOFILL_TOKENS = new Set([
|
|
3015
3169
|
"off",
|
|
3016
3170
|
"on",
|
|
@@ -3098,7 +3252,7 @@ const autocompleteValid = defineRule({
|
|
|
3098
3252
|
if (!AUTOFILL_TOKENS.has(token)) {
|
|
3099
3253
|
context.report({
|
|
3100
3254
|
node: attribute,
|
|
3101
|
-
message: buildMessage$
|
|
3255
|
+
message: buildMessage$24(value)
|
|
3102
3256
|
});
|
|
3103
3257
|
return;
|
|
3104
3258
|
}
|
|
@@ -3186,7 +3340,7 @@ const buttonHasType = defineRule({
|
|
|
3186
3340
|
recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
|
|
3187
3341
|
create: (context) => {
|
|
3188
3342
|
const settings = resolveSettings$48(context.settings);
|
|
3189
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3343
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3190
3344
|
return {
|
|
3191
3345
|
JSXOpeningElement(node) {
|
|
3192
3346
|
if (isTestlikeFile) return;
|
|
@@ -3373,7 +3527,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
3373
3527
|
//#endregion
|
|
3374
3528
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
3375
3529
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3376
|
-
const MESSAGE$
|
|
3530
|
+
const MESSAGE$45 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
|
|
3377
3531
|
const KEY_HANDLERS = [
|
|
3378
3532
|
"onKeyUp",
|
|
3379
3533
|
"onKeyDown",
|
|
@@ -3386,7 +3540,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3386
3540
|
recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
|
|
3387
3541
|
category: "Accessibility",
|
|
3388
3542
|
create: (context) => {
|
|
3389
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3543
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3390
3544
|
return { JSXOpeningElement(node) {
|
|
3391
3545
|
if (isTestlikeFile) return;
|
|
3392
3546
|
const tag = getElementType(node, context.settings);
|
|
@@ -3404,7 +3558,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
3404
3558
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
3405
3559
|
context.report({
|
|
3406
3560
|
node: node.name,
|
|
3407
|
-
message: MESSAGE$
|
|
3561
|
+
message: MESSAGE$45
|
|
3408
3562
|
});
|
|
3409
3563
|
} };
|
|
3410
3564
|
}
|
|
@@ -3489,14 +3643,42 @@ const isReactComponentName = (name) => {
|
|
|
3489
3643
|
return firstCharacter >= 65 && firstCharacter <= 90;
|
|
3490
3644
|
};
|
|
3491
3645
|
//#endregion
|
|
3646
|
+
//#region src/plugin/utils/strip-paren-expression.ts
|
|
3647
|
+
const TS_WRAPPER_TYPES = new Set([
|
|
3648
|
+
"ParenthesizedExpression",
|
|
3649
|
+
"TSAsExpression",
|
|
3650
|
+
"TSSatisfiesExpression",
|
|
3651
|
+
"TSTypeAssertion",
|
|
3652
|
+
"TSNonNullExpression",
|
|
3653
|
+
"TSInstantiationExpression"
|
|
3654
|
+
]);
|
|
3655
|
+
const stripParenExpression = (node) => {
|
|
3656
|
+
let current = node;
|
|
3657
|
+
while (true) {
|
|
3658
|
+
if (TS_WRAPPER_TYPES.has(current.type) && "expression" in current && current.expression) {
|
|
3659
|
+
current = current.expression;
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
if (isNodeOfType(current, "ChainExpression") && current.expression) {
|
|
3663
|
+
current = current.expression;
|
|
3664
|
+
continue;
|
|
3665
|
+
}
|
|
3666
|
+
break;
|
|
3667
|
+
}
|
|
3668
|
+
return current;
|
|
3669
|
+
};
|
|
3670
|
+
//#endregion
|
|
3492
3671
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
3493
|
-
const MESSAGE$
|
|
3672
|
+
const MESSAGE$44 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3494
3673
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
3495
3674
|
const DEFAULT_LABELLING_PROPS = [
|
|
3496
3675
|
"alt",
|
|
3497
3676
|
"aria-label",
|
|
3498
3677
|
"aria-labelledby"
|
|
3499
3678
|
];
|
|
3679
|
+
const ID_ATTRIBUTE = "id";
|
|
3680
|
+
const HTML_FOR_ATTRIBUTE = "htmlFor";
|
|
3681
|
+
const LABEL_ELEMENT = "label";
|
|
3500
3682
|
const DEFAULT_DEPTH = 5;
|
|
3501
3683
|
const MAX_DEPTH = 25;
|
|
3502
3684
|
const resolveSettings$46 = (settings) => {
|
|
@@ -3524,6 +3706,29 @@ const hasLabellingProp = (attributes, customAttributes) => {
|
|
|
3524
3706
|
}
|
|
3525
3707
|
return false;
|
|
3526
3708
|
};
|
|
3709
|
+
const toAttributeMatchKey = (kind, value) => {
|
|
3710
|
+
const trimmedValue = value.trim();
|
|
3711
|
+
return trimmedValue.length > 0 ? `${kind}:${trimmedValue}` : null;
|
|
3712
|
+
};
|
|
3713
|
+
const getLiteralAttributeMatchKey = (value) => {
|
|
3714
|
+
if (typeof value === "string") return toAttributeMatchKey("literal", value);
|
|
3715
|
+
if (typeof value === "number") return toAttributeMatchKey("literal", String(value));
|
|
3716
|
+
return null;
|
|
3717
|
+
};
|
|
3718
|
+
const getAttributeMatchKey = (attribute) => {
|
|
3719
|
+
if (!attribute?.value) return null;
|
|
3720
|
+
const value = attribute.value;
|
|
3721
|
+
if (isNodeOfType(value, "Literal")) return getLiteralAttributeMatchKey(value.value);
|
|
3722
|
+
if (!isNodeOfType(value, "JSXExpressionContainer")) return null;
|
|
3723
|
+
const expression = value.expression;
|
|
3724
|
+
if (isNodeOfType(expression, "Identifier")) return toAttributeMatchKey("identifier", expression.name);
|
|
3725
|
+
if (isNodeOfType(expression, "Literal")) return getLiteralAttributeMatchKey(expression.value);
|
|
3726
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
3727
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
3728
|
+
return staticValue === null ? null : toAttributeMatchKey("literal", staticValue);
|
|
3729
|
+
}
|
|
3730
|
+
return null;
|
|
3731
|
+
};
|
|
3527
3732
|
const checkChildForLabel = (child, currentDepth, context) => {
|
|
3528
3733
|
if (currentDepth > context.depth) return false;
|
|
3529
3734
|
if (isNodeOfType(child, "JSXExpressionContainer")) return true;
|
|
@@ -3539,6 +3744,55 @@ const checkChildForLabel = (child, currentDepth, context) => {
|
|
|
3539
3744
|
}
|
|
3540
3745
|
return false;
|
|
3541
3746
|
};
|
|
3747
|
+
const hasAccessibleLabelText = (element, context) => {
|
|
3748
|
+
if (hasLabellingProp(element.openingElement.attributes, context.customAttributes)) return true;
|
|
3749
|
+
return element.children.some((child) => checkChildForLabel(child, 1, context));
|
|
3750
|
+
};
|
|
3751
|
+
const isFunctionBoundary = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
3752
|
+
const hasAncestorLabel = (element, context) => {
|
|
3753
|
+
let current = element.parent;
|
|
3754
|
+
while (current) {
|
|
3755
|
+
if (isFunctionBoundary(current)) break;
|
|
3756
|
+
if (isNodeOfType(current, "JSXElement")) {
|
|
3757
|
+
if (getElementType(current.openingElement, context.settings) === LABEL_ELEMENT && hasAccessibleLabelText(current, context)) return true;
|
|
3758
|
+
}
|
|
3759
|
+
current = current.parent ?? null;
|
|
3760
|
+
}
|
|
3761
|
+
return false;
|
|
3762
|
+
};
|
|
3763
|
+
const findEnclosingJsxTreeRoot = (element) => {
|
|
3764
|
+
let root = element;
|
|
3765
|
+
let current = element.parent;
|
|
3766
|
+
while (current) {
|
|
3767
|
+
if (isFunctionBoundary(current)) break;
|
|
3768
|
+
if (isNodeOfType(current, "JSXElement") || isNodeOfType(current, "JSXFragment")) root = current;
|
|
3769
|
+
current = current.parent ?? null;
|
|
3770
|
+
}
|
|
3771
|
+
return root;
|
|
3772
|
+
};
|
|
3773
|
+
const collectJsxFromExpression = (rawExpression) => {
|
|
3774
|
+
const expression = stripParenExpression(rawExpression);
|
|
3775
|
+
if (isNodeOfType(expression, "JSXElement") || isNodeOfType(expression, "JSXFragment")) return [expression];
|
|
3776
|
+
if (isNodeOfType(expression, "LogicalExpression")) return [...collectJsxFromExpression(expression.left), ...collectJsxFromExpression(expression.right)];
|
|
3777
|
+
if (isNodeOfType(expression, "ConditionalExpression")) return [...collectJsxFromExpression(expression.consequent), ...collectJsxFromExpression(expression.alternate)];
|
|
3778
|
+
return [];
|
|
3779
|
+
};
|
|
3780
|
+
const searchForHtmlForLabel = (node, controlIdKey, context) => {
|
|
3781
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return collectJsxFromExpression(node.expression).some((jsxNode) => searchForHtmlForLabel(jsxNode, controlIdKey, context));
|
|
3782
|
+
const children = isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment") ? node.children : [];
|
|
3783
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
3784
|
+
if (getElementType(node.openingElement, context.settings) === LABEL_ELEMENT) {
|
|
3785
|
+
if (getAttributeMatchKey(hasJsxPropIgnoreCase(node.openingElement.attributes, HTML_FOR_ATTRIBUTE)) === controlIdKey && hasAccessibleLabelText(node, context)) return true;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
for (const child of children) if (searchForHtmlForLabel(child, controlIdKey, context)) return true;
|
|
3789
|
+
return false;
|
|
3790
|
+
};
|
|
3791
|
+
const hasHtmlForLabel = (element, context) => {
|
|
3792
|
+
const controlIdKey = getAttributeMatchKey(hasJsxPropIgnoreCase(element.openingElement.attributes, ID_ATTRIBUTE));
|
|
3793
|
+
if (controlIdKey === null) return false;
|
|
3794
|
+
return searchForHtmlForLabel(findEnclosingJsxTreeRoot(element), controlIdKey, context);
|
|
3795
|
+
};
|
|
3542
3796
|
const controlHasAssociatedLabel = defineRule({
|
|
3543
3797
|
id: "control-has-associated-label",
|
|
3544
3798
|
tags: ["react-jsx-only"],
|
|
@@ -3547,7 +3801,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3547
3801
|
category: "Accessibility",
|
|
3548
3802
|
create: (context) => {
|
|
3549
3803
|
const settings = resolveSettings$46(context.settings);
|
|
3550
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
3804
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3551
3805
|
return { JSXElement(node) {
|
|
3552
3806
|
if (isTestlikeFile) return;
|
|
3553
3807
|
const opening = node.openingElement;
|
|
@@ -3570,10 +3824,12 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
3570
3824
|
controlComponents: settings.controlComponents,
|
|
3571
3825
|
settings: context.settings
|
|
3572
3826
|
};
|
|
3827
|
+
if (hasAncestorLabel(node, checkContext)) return;
|
|
3828
|
+
if (hasHtmlForLabel(node, checkContext)) return;
|
|
3573
3829
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
3574
3830
|
context.report({
|
|
3575
3831
|
node: opening,
|
|
3576
|
-
message: MESSAGE$
|
|
3832
|
+
message: MESSAGE$44
|
|
3577
3833
|
});
|
|
3578
3834
|
} };
|
|
3579
3835
|
}
|
|
@@ -3863,25 +4119,6 @@ const noVagueButtonLabel = defineRule({
|
|
|
3863
4119
|
} })
|
|
3864
4120
|
});
|
|
3865
4121
|
//#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
4122
|
//#region src/plugin/utils/is-es5-component.ts
|
|
3886
4123
|
const PRAGMA$2 = "React";
|
|
3887
4124
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -3916,7 +4153,7 @@ const isEs6Component = (node) => {
|
|
|
3916
4153
|
};
|
|
3917
4154
|
//#endregion
|
|
3918
4155
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
3919
|
-
const MESSAGE$
|
|
4156
|
+
const MESSAGE$43 = "Component is missing a `displayName` — assign one for easier debugging.";
|
|
3920
4157
|
const resolveSettings$45 = (settings) => {
|
|
3921
4158
|
const reactDoctor = settings?.["react-doctor"];
|
|
3922
4159
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
@@ -4112,7 +4349,7 @@ const displayName = defineRule({
|
|
|
4112
4349
|
const reportAt = (node) => {
|
|
4113
4350
|
context.report({
|
|
4114
4351
|
node,
|
|
4115
|
-
message: MESSAGE$
|
|
4352
|
+
message: MESSAGE$43
|
|
4116
4353
|
});
|
|
4117
4354
|
};
|
|
4118
4355
|
return {
|
|
@@ -4235,7 +4472,7 @@ const displayName = defineRule({
|
|
|
4235
4472
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4236
4473
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4237
4474
|
if (!node || typeof node !== "object") return;
|
|
4238
|
-
if (isFunctionLike(node)) return;
|
|
4475
|
+
if (isFunctionLike$1(node)) return;
|
|
4239
4476
|
visitor(node);
|
|
4240
4477
|
const nodeRecord = node;
|
|
4241
4478
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4323,7 +4560,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4323
4560
|
let didFindRelease = false;
|
|
4324
4561
|
walkAst(node, (child) => {
|
|
4325
4562
|
if (didFindRelease) return false;
|
|
4326
|
-
if (child !== node && isFunctionLike(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4563
|
+
if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4327
4564
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4328
4565
|
didFindRelease = true;
|
|
4329
4566
|
return false;
|
|
@@ -4332,7 +4569,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4332
4569
|
return didFindRelease;
|
|
4333
4570
|
};
|
|
4334
4571
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4335
|
-
if (!isFunctionLike(node)) return false;
|
|
4572
|
+
if (!isFunctionLike$1(node)) return false;
|
|
4336
4573
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4337
4574
|
};
|
|
4338
4575
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
@@ -4574,7 +4811,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4574
4811
|
};
|
|
4575
4812
|
const isFunctionBodyBlock = (block) => {
|
|
4576
4813
|
if (!block.parent) return false;
|
|
4577
|
-
return isFunctionLike(block.parent);
|
|
4814
|
+
return isFunctionLike$1(block.parent);
|
|
4578
4815
|
};
|
|
4579
4816
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4580
4817
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -4701,8 +4938,35 @@ const inferReferenceFlag = (identifier) => {
|
|
|
4701
4938
|
const setNodeScope = (node, state) => {
|
|
4702
4939
|
state.nodeScope.set(node, state.currentScope);
|
|
4703
4940
|
};
|
|
4941
|
+
const walkParameterReferences = (pattern, state) => {
|
|
4942
|
+
if (isNodeOfType(pattern, "AssignmentPattern")) {
|
|
4943
|
+
walkParameterReferences(pattern.left, state);
|
|
4944
|
+
const defaultValue = pattern.right ?? null;
|
|
4945
|
+
if (defaultValue) walk(defaultValue, state);
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
if (isNodeOfType(pattern, "ObjectPattern")) {
|
|
4949
|
+
for (const property of pattern.properties) {
|
|
4950
|
+
const propertyNode = property;
|
|
4951
|
+
if (isNodeOfType(propertyNode, "RestElement")) {
|
|
4952
|
+
walkParameterReferences(propertyNode.argument, state);
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
if (!isNodeOfType(propertyNode, "Property")) continue;
|
|
4956
|
+
const propertyDetail = propertyNode;
|
|
4957
|
+
if (propertyDetail.computed) walk(propertyDetail.key, state);
|
|
4958
|
+
walkParameterReferences(propertyDetail.value, state);
|
|
4959
|
+
}
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4962
|
+
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
4963
|
+
for (const element of pattern.elements) if (element) walkParameterReferences(element, state);
|
|
4964
|
+
return;
|
|
4965
|
+
}
|
|
4966
|
+
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
4967
|
+
};
|
|
4704
4968
|
const walk = (node, state) => {
|
|
4705
|
-
if (isFunctionLike(node)) {
|
|
4969
|
+
if (isFunctionLike$1(node)) {
|
|
4706
4970
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
4707
4971
|
setNodeScope(node, state);
|
|
4708
4972
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -4716,7 +4980,9 @@ const walk = (node, state) => {
|
|
|
4716
4980
|
});
|
|
4717
4981
|
tagAsBinding(state, node.id);
|
|
4718
4982
|
}
|
|
4719
|
-
|
|
4983
|
+
const functionParams = node.params ?? [];
|
|
4984
|
+
handleFunctionParameters(functionParams, fnScope, state);
|
|
4985
|
+
for (const param of functionParams) walkParameterReferences(param, state);
|
|
4720
4986
|
const body = node.body;
|
|
4721
4987
|
if (body) walk(body, state);
|
|
4722
4988
|
popScope(state);
|
|
@@ -4758,6 +5024,7 @@ const walk = (node, state) => {
|
|
|
4758
5024
|
const nodeRecord = node;
|
|
4759
5025
|
for (const key of Object.keys(nodeRecord)) {
|
|
4760
5026
|
if (key === "parent") continue;
|
|
5027
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4761
5028
|
const child = nodeRecord[key];
|
|
4762
5029
|
if (Array.isArray(child)) {
|
|
4763
5030
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -4820,6 +5087,7 @@ const walk = (node, state) => {
|
|
|
4820
5087
|
const nodeRecord = node;
|
|
4821
5088
|
for (const key of Object.keys(nodeRecord)) {
|
|
4822
5089
|
if (key === "parent") continue;
|
|
5090
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4823
5091
|
const child = nodeRecord[key];
|
|
4824
5092
|
if (Array.isArray(child)) {
|
|
4825
5093
|
for (const item of child) if (isAstNode(item)) walk(item, state);
|
|
@@ -4939,20 +5207,12 @@ const isAstDescendant = (inner, outer) => {
|
|
|
4939
5207
|
};
|
|
4940
5208
|
//#endregion
|
|
4941
5209
|
//#region src/plugin/semantic/closure-captures.ts
|
|
4942
|
-
const TYPE_ONLY_CHILD_KEYS = new Set([
|
|
4943
|
-
"implements",
|
|
4944
|
-
"returnType",
|
|
4945
|
-
"superTypeArguments",
|
|
4946
|
-
"typeAnnotation",
|
|
4947
|
-
"typeArguments",
|
|
4948
|
-
"typeParameters"
|
|
4949
|
-
]);
|
|
4950
5210
|
const closureCaptures = (functionNode, scopes) => {
|
|
4951
5211
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
4952
5212
|
const out = [];
|
|
4953
5213
|
const seen = /* @__PURE__ */ new Set();
|
|
4954
5214
|
const visit = (node) => {
|
|
4955
|
-
if (node !== functionNode && isFunctionLike(node)) {
|
|
5215
|
+
if (node !== functionNode && isFunctionLike$1(node)) {
|
|
4956
5216
|
const innerCaptures = closureCaptures(node, scopes);
|
|
4957
5217
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
4958
5218
|
if (!seen.has(reference.id)) {
|
|
@@ -4974,7 +5234,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
4974
5234
|
const record = node;
|
|
4975
5235
|
for (const key of Object.keys(record)) {
|
|
4976
5236
|
if (key === "parent") continue;
|
|
4977
|
-
if (
|
|
5237
|
+
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4978
5238
|
const child = record[key];
|
|
4979
5239
|
if (Array.isArray(child)) {
|
|
4980
5240
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
@@ -5337,33 +5597,6 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
5337
5597
|
if (!depKey) continue;
|
|
5338
5598
|
keys.add(depKey);
|
|
5339
5599
|
}
|
|
5340
|
-
const functionParams = callback.params ?? [];
|
|
5341
|
-
for (const param of functionParams) {
|
|
5342
|
-
if (!isNodeOfType(param, "AssignmentPattern")) continue;
|
|
5343
|
-
const visitDefaultValue = (node) => {
|
|
5344
|
-
if (isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression")) {
|
|
5345
|
-
const depKey = stringifyMemberChain(node);
|
|
5346
|
-
if (depKey) keys.add(depKey);
|
|
5347
|
-
}
|
|
5348
|
-
const reference = scopes.referenceFor(node);
|
|
5349
|
-
if (reference?.resolvedSymbol) {
|
|
5350
|
-
const symbol = reference.resolvedSymbol;
|
|
5351
|
-
if (!isOutsideAllFunctions(symbol)) {
|
|
5352
|
-
const depKey = computeDepKey(reference);
|
|
5353
|
-
if (depKey) keys.add(depKey);
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5356
|
-
const record = node;
|
|
5357
|
-
for (const key of Object.keys(record)) {
|
|
5358
|
-
if (key === "parent") continue;
|
|
5359
|
-
const child = record[key];
|
|
5360
|
-
if (Array.isArray(child)) {
|
|
5361
|
-
for (const item of child) if (isAstNode(item)) visitDefaultValue(item);
|
|
5362
|
-
} else if (isAstNode(child)) visitDefaultValue(child);
|
|
5363
|
-
}
|
|
5364
|
-
};
|
|
5365
|
-
visitDefaultValue(param.right);
|
|
5366
|
-
}
|
|
5367
5600
|
return {
|
|
5368
5601
|
keys,
|
|
5369
5602
|
stableCapturedNames
|
|
@@ -5973,7 +6206,7 @@ const flattenJsxName$1 = (name) => {
|
|
|
5973
6206
|
return "";
|
|
5974
6207
|
};
|
|
5975
6208
|
const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
|
|
5976
|
-
const buildMessage$
|
|
6209
|
+
const buildMessage$23 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
|
|
5977
6210
|
const forbidComponentProps = defineRule({
|
|
5978
6211
|
id: "forbid-component-props",
|
|
5979
6212
|
severity: "warn",
|
|
@@ -5999,7 +6232,7 @@ const forbidComponentProps = defineRule({
|
|
|
5999
6232
|
if (!isForbiddenForTag(entry, tag)) continue;
|
|
6000
6233
|
context.report({
|
|
6001
6234
|
node: attribute,
|
|
6002
|
-
message: buildMessage$
|
|
6235
|
+
message: buildMessage$23(propName, entry.message)
|
|
6003
6236
|
});
|
|
6004
6237
|
break;
|
|
6005
6238
|
}
|
|
@@ -6009,7 +6242,7 @@ const forbidComponentProps = defineRule({
|
|
|
6009
6242
|
});
|
|
6010
6243
|
//#endregion
|
|
6011
6244
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
6012
|
-
const buildMessage$
|
|
6245
|
+
const buildMessage$22 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
|
|
6013
6246
|
const resolveSettings$43 = (settings) => {
|
|
6014
6247
|
const reactDoctor = settings?.["react-doctor"];
|
|
6015
6248
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
@@ -6047,7 +6280,7 @@ const forbidDomProps = defineRule({
|
|
|
6047
6280
|
if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
|
|
6048
6281
|
context.report({
|
|
6049
6282
|
node: attribute.name,
|
|
6050
|
-
message: buildMessage$
|
|
6283
|
+
message: buildMessage$22(propName, descriptor.message)
|
|
6051
6284
|
});
|
|
6052
6285
|
}
|
|
6053
6286
|
} };
|
|
@@ -6117,7 +6350,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
6117
6350
|
};
|
|
6118
6351
|
//#endregion
|
|
6119
6352
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
6120
|
-
const buildMessage$
|
|
6353
|
+
const buildMessage$21 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
|
|
6121
6354
|
const resolveSettings$42 = (settings) => {
|
|
6122
6355
|
const reactDoctor = settings?.["react-doctor"];
|
|
6123
6356
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
@@ -6141,7 +6374,7 @@ const forbidElements = defineRule({
|
|
|
6141
6374
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
6142
6375
|
context.report({
|
|
6143
6376
|
node: node.name,
|
|
6144
|
-
message: buildMessage$
|
|
6377
|
+
message: buildMessage$21(fullName, forbidMap.get(fullName))
|
|
6145
6378
|
});
|
|
6146
6379
|
},
|
|
6147
6380
|
CallExpression(node) {
|
|
@@ -6161,7 +6394,7 @@ const forbidElements = defineRule({
|
|
|
6161
6394
|
if (!elementName || !forbidMap.has(elementName)) return;
|
|
6162
6395
|
context.report({
|
|
6163
6396
|
node: firstArgument,
|
|
6164
|
-
message: buildMessage$
|
|
6397
|
+
message: buildMessage$21(elementName, forbidMap.get(elementName))
|
|
6165
6398
|
});
|
|
6166
6399
|
}
|
|
6167
6400
|
};
|
|
@@ -6169,7 +6402,7 @@ const forbidElements = defineRule({
|
|
|
6169
6402
|
});
|
|
6170
6403
|
//#endregion
|
|
6171
6404
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
6172
|
-
const MESSAGE$
|
|
6405
|
+
const MESSAGE$42 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
|
|
6173
6406
|
const forwardRefUsesRef = defineRule({
|
|
6174
6407
|
id: "forward-ref-uses-ref",
|
|
6175
6408
|
severity: "warn",
|
|
@@ -6188,13 +6421,13 @@ const forwardRefUsesRef = defineRule({
|
|
|
6188
6421
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
6189
6422
|
context.report({
|
|
6190
6423
|
node: inner,
|
|
6191
|
-
message: MESSAGE$
|
|
6424
|
+
message: MESSAGE$42
|
|
6192
6425
|
});
|
|
6193
6426
|
} })
|
|
6194
6427
|
});
|
|
6195
6428
|
//#endregion
|
|
6196
6429
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
6197
|
-
const MESSAGE$
|
|
6430
|
+
const MESSAGE$41 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
|
|
6198
6431
|
const DEFAULT_HEADING_TAGS = [
|
|
6199
6432
|
"h1",
|
|
6200
6433
|
"h2",
|
|
@@ -6226,7 +6459,7 @@ const headingHasContent = defineRule({
|
|
|
6226
6459
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6227
6460
|
context.report({
|
|
6228
6461
|
node,
|
|
6229
|
-
message: MESSAGE$
|
|
6462
|
+
message: MESSAGE$41
|
|
6230
6463
|
});
|
|
6231
6464
|
} };
|
|
6232
6465
|
}
|
|
@@ -6327,8 +6560,42 @@ const hookUseState = defineRule({
|
|
|
6327
6560
|
}
|
|
6328
6561
|
});
|
|
6329
6562
|
//#endregion
|
|
6563
|
+
//#region src/plugin/rules/state-and-effects/hooks-no-nan-in-deps.ts
|
|
6564
|
+
const HOOKS_WITH_DEP_ARRAY = new Set([
|
|
6565
|
+
"useEffect",
|
|
6566
|
+
"useLayoutEffect",
|
|
6567
|
+
"useInsertionEffect",
|
|
6568
|
+
"useCallback",
|
|
6569
|
+
"useMemo",
|
|
6570
|
+
"useImperativeHandle"
|
|
6571
|
+
]);
|
|
6572
|
+
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.";
|
|
6573
|
+
const isNanLiteral = (node) => {
|
|
6574
|
+
if (isNodeOfType(node, "Identifier") && node.name === "NaN") return true;
|
|
6575
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "Number" && isNodeOfType(node.property, "Identifier") && node.property.name === "NaN") return true;
|
|
6576
|
+
return false;
|
|
6577
|
+
};
|
|
6578
|
+
const hooksNoNanInDeps = defineRule({
|
|
6579
|
+
id: "hooks-no-nan-in-deps",
|
|
6580
|
+
severity: "warn",
|
|
6581
|
+
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.",
|
|
6582
|
+
create: (context) => ({ CallExpression(node) {
|
|
6583
|
+
if (!isHookCall$1(node, HOOKS_WITH_DEP_ARRAY)) return;
|
|
6584
|
+
const depsIndex = getCalleeName$1(node) === "useImperativeHandle" ? 2 : 1;
|
|
6585
|
+
const depsArgument = node.arguments[depsIndex];
|
|
6586
|
+
if (!depsArgument || !isNodeOfType(depsArgument, "ArrayExpression")) return;
|
|
6587
|
+
for (const element of depsArgument.elements) {
|
|
6588
|
+
if (!element) continue;
|
|
6589
|
+
if (isNanLiteral(element)) context.report({
|
|
6590
|
+
node: element,
|
|
6591
|
+
message: NAN_MESSAGE
|
|
6592
|
+
});
|
|
6593
|
+
}
|
|
6594
|
+
} })
|
|
6595
|
+
});
|
|
6596
|
+
//#endregion
|
|
6330
6597
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
6331
|
-
const MESSAGE$
|
|
6598
|
+
const MESSAGE$40 = "`<html>` element must have a non-empty `lang` attribute.";
|
|
6332
6599
|
const resolveSettings$39 = (settings) => {
|
|
6333
6600
|
const reactDoctor = settings?.["react-doctor"];
|
|
6334
6601
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -6375,7 +6642,7 @@ const htmlHasLang = defineRule({
|
|
|
6375
6642
|
if (!lang) {
|
|
6376
6643
|
context.report({
|
|
6377
6644
|
node: node.name,
|
|
6378
|
-
message: MESSAGE$
|
|
6645
|
+
message: MESSAGE$40
|
|
6379
6646
|
});
|
|
6380
6647
|
return;
|
|
6381
6648
|
}
|
|
@@ -6383,20 +6650,224 @@ const htmlHasLang = defineRule({
|
|
|
6383
6650
|
if (verdict === "missing" || verdict === "empty") {
|
|
6384
6651
|
context.report({
|
|
6385
6652
|
node: lang,
|
|
6386
|
-
message: MESSAGE$
|
|
6653
|
+
message: MESSAGE$40
|
|
6387
6654
|
});
|
|
6388
6655
|
return;
|
|
6389
6656
|
}
|
|
6390
6657
|
if (hasSpread && !lang) context.report({
|
|
6391
6658
|
node: node.name,
|
|
6392
|
-
message: MESSAGE$
|
|
6659
|
+
message: MESSAGE$40
|
|
6393
6660
|
});
|
|
6394
6661
|
} };
|
|
6395
6662
|
}
|
|
6396
6663
|
});
|
|
6397
6664
|
//#endregion
|
|
6665
|
+
//#region src/plugin/rules/correctness/html-no-invalid-paragraph-child.ts
|
|
6666
|
+
const BLOCK_LEVEL_ELEMENTS = new Set([
|
|
6667
|
+
"address",
|
|
6668
|
+
"article",
|
|
6669
|
+
"aside",
|
|
6670
|
+
"blockquote",
|
|
6671
|
+
"details",
|
|
6672
|
+
"div",
|
|
6673
|
+
"dl",
|
|
6674
|
+
"fieldset",
|
|
6675
|
+
"figcaption",
|
|
6676
|
+
"figure",
|
|
6677
|
+
"footer",
|
|
6678
|
+
"form",
|
|
6679
|
+
"h1",
|
|
6680
|
+
"h2",
|
|
6681
|
+
"h3",
|
|
6682
|
+
"h4",
|
|
6683
|
+
"h5",
|
|
6684
|
+
"h6",
|
|
6685
|
+
"header",
|
|
6686
|
+
"hgroup",
|
|
6687
|
+
"hr",
|
|
6688
|
+
"main",
|
|
6689
|
+
"menu",
|
|
6690
|
+
"nav",
|
|
6691
|
+
"ol",
|
|
6692
|
+
"p",
|
|
6693
|
+
"pre",
|
|
6694
|
+
"search",
|
|
6695
|
+
"section",
|
|
6696
|
+
"table",
|
|
6697
|
+
"ul"
|
|
6698
|
+
]);
|
|
6699
|
+
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.`;
|
|
6700
|
+
const isParagraphElement = (candidate) => {
|
|
6701
|
+
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6702
|
+
const opening = candidate.openingElement;
|
|
6703
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
6704
|
+
return opening.name.name === "p";
|
|
6705
|
+
};
|
|
6706
|
+
const findEnclosingParagraph = (openingElement) => {
|
|
6707
|
+
const owningElement = openingElement.parent;
|
|
6708
|
+
if (!owningElement) return null;
|
|
6709
|
+
let ancestor = owningElement.parent;
|
|
6710
|
+
while (ancestor) {
|
|
6711
|
+
if (isParagraphElement(ancestor)) return ancestor;
|
|
6712
|
+
ancestor = ancestor.parent ?? null;
|
|
6713
|
+
}
|
|
6714
|
+
return null;
|
|
6715
|
+
};
|
|
6716
|
+
const htmlNoInvalidParagraphChild = defineRule({
|
|
6717
|
+
id: "html-no-invalid-paragraph-child",
|
|
6718
|
+
severity: "warn",
|
|
6719
|
+
recommendation: "Replace the surrounding `<p>` with a `<div>`, or hoist the block-level child outside the paragraph.",
|
|
6720
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
6721
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
6722
|
+
const childTagName = node.name.name;
|
|
6723
|
+
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
6724
|
+
if (!findEnclosingParagraph(node)) return;
|
|
6725
|
+
context.report({
|
|
6726
|
+
node: node.name,
|
|
6727
|
+
message: buildMessage$20(childTagName)
|
|
6728
|
+
});
|
|
6729
|
+
} })
|
|
6730
|
+
});
|
|
6731
|
+
//#endregion
|
|
6732
|
+
//#region src/plugin/rules/correctness/html-no-invalid-table-nesting.ts
|
|
6733
|
+
const TABLE_ELEMENTS = new Set([
|
|
6734
|
+
"table",
|
|
6735
|
+
"thead",
|
|
6736
|
+
"tbody",
|
|
6737
|
+
"tfoot",
|
|
6738
|
+
"tr",
|
|
6739
|
+
"td",
|
|
6740
|
+
"th"
|
|
6741
|
+
]);
|
|
6742
|
+
const ROW_GROUPS = new Set([
|
|
6743
|
+
"thead",
|
|
6744
|
+
"tbody",
|
|
6745
|
+
"tfoot"
|
|
6746
|
+
]);
|
|
6747
|
+
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).`;
|
|
6748
|
+
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.";
|
|
6749
|
+
const getHostTagName = (jsxElement) => {
|
|
6750
|
+
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
6751
|
+
const opening = jsxElement.openingElement;
|
|
6752
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
6753
|
+
const tagName = opening.name.name;
|
|
6754
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
6755
|
+
return tagName;
|
|
6756
|
+
};
|
|
6757
|
+
const findClosestHostAncestor = (jsxElement) => {
|
|
6758
|
+
let ancestor = jsxElement.parent;
|
|
6759
|
+
while (ancestor) {
|
|
6760
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
6761
|
+
const opening = ancestor.openingElement;
|
|
6762
|
+
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
6763
|
+
const ancestorTag = opening.name.name;
|
|
6764
|
+
if (ancestorTag.length === 0) {
|
|
6765
|
+
ancestor = ancestor.parent ?? null;
|
|
6766
|
+
continue;
|
|
6767
|
+
}
|
|
6768
|
+
if (ancestorTag[0] === ancestorTag[0].toLowerCase()) return {
|
|
6769
|
+
kind: "host",
|
|
6770
|
+
tagName: ancestorTag,
|
|
6771
|
+
element: ancestor
|
|
6772
|
+
};
|
|
6773
|
+
return { kind: "opaque" };
|
|
6774
|
+
}
|
|
6775
|
+
return { kind: "opaque" };
|
|
6776
|
+
}
|
|
6777
|
+
ancestor = ancestor.parent ?? null;
|
|
6778
|
+
}
|
|
6779
|
+
return { kind: "none" };
|
|
6780
|
+
};
|
|
6781
|
+
const NESTED_TABLE_BOUNDARY_CELLS = new Set(["td", "th"]);
|
|
6782
|
+
const findEnclosingTable = (jsxElement) => {
|
|
6783
|
+
let ancestor = jsxElement.parent;
|
|
6784
|
+
while (ancestor) {
|
|
6785
|
+
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
6786
|
+
const tag = getHostTagName(ancestor);
|
|
6787
|
+
if (tag === "table") return ancestor;
|
|
6788
|
+
if (tag !== null && NESTED_TABLE_BOUNDARY_CELLS.has(tag)) return null;
|
|
6789
|
+
if (tag === null) return null;
|
|
6790
|
+
}
|
|
6791
|
+
ancestor = ancestor.parent ?? null;
|
|
6792
|
+
}
|
|
6793
|
+
return null;
|
|
6794
|
+
};
|
|
6795
|
+
const htmlNoInvalidTableNesting = defineRule({
|
|
6796
|
+
id: "html-no-invalid-table-nesting",
|
|
6797
|
+
severity: "warn",
|
|
6798
|
+
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.",
|
|
6799
|
+
create: (context) => ({ JSXElement(node) {
|
|
6800
|
+
const tagName = getHostTagName(node);
|
|
6801
|
+
if (!tagName || !TABLE_ELEMENTS.has(tagName)) return;
|
|
6802
|
+
if (tagName === "table") {
|
|
6803
|
+
if (findEnclosingTable(node)) context.report({
|
|
6804
|
+
node: node.openingElement.name,
|
|
6805
|
+
message: buildNestedTableMessage()
|
|
6806
|
+
});
|
|
6807
|
+
return;
|
|
6808
|
+
}
|
|
6809
|
+
const closestHost = findClosestHostAncestor(node);
|
|
6810
|
+
if (closestHost.kind !== "host") return;
|
|
6811
|
+
const actualParent = closestHost.tagName;
|
|
6812
|
+
if (ROW_GROUPS.has(tagName)) {
|
|
6813
|
+
if (actualParent !== "table") context.report({
|
|
6814
|
+
node: node.openingElement.name,
|
|
6815
|
+
message: buildMessage$19(tagName, "`<table>`", actualParent)
|
|
6816
|
+
});
|
|
6817
|
+
return;
|
|
6818
|
+
}
|
|
6819
|
+
if (tagName === "tr") {
|
|
6820
|
+
if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
|
|
6821
|
+
node: node.openingElement.name,
|
|
6822
|
+
message: buildMessage$19(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
|
|
6823
|
+
});
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
if (tagName === "td" || tagName === "th") {
|
|
6827
|
+
if (actualParent !== "tr") context.report({
|
|
6828
|
+
node: node.openingElement.name,
|
|
6829
|
+
message: buildMessage$19(tagName, "`<tr>`", actualParent)
|
|
6830
|
+
});
|
|
6831
|
+
}
|
|
6832
|
+
} })
|
|
6833
|
+
});
|
|
6834
|
+
//#endregion
|
|
6835
|
+
//#region src/plugin/rules/correctness/html-no-nested-interactive.ts
|
|
6836
|
+
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.`;
|
|
6837
|
+
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
6838
|
+
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
6839
|
+
const opening = candidate.openingElement;
|
|
6840
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
6841
|
+
return opening.name.name === tagName;
|
|
6842
|
+
};
|
|
6843
|
+
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
6844
|
+
const owningElement = openingElement.parent;
|
|
6845
|
+
if (!owningElement) return null;
|
|
6846
|
+
let ancestor = owningElement.parent;
|
|
6847
|
+
while (ancestor) {
|
|
6848
|
+
if (isJsxElementWithTagName(ancestor, tagName)) return ancestor;
|
|
6849
|
+
ancestor = ancestor.parent ?? null;
|
|
6850
|
+
}
|
|
6851
|
+
return null;
|
|
6852
|
+
};
|
|
6853
|
+
const htmlNoNestedInteractive = defineRule({
|
|
6854
|
+
id: "html-no-nested-interactive",
|
|
6855
|
+
severity: "warn",
|
|
6856
|
+
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>`).",
|
|
6857
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
6858
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
6859
|
+
const tagName = node.name.name;
|
|
6860
|
+
if (tagName !== "a" && tagName !== "button") return;
|
|
6861
|
+
if (!findEnclosingSameTag(node, tagName)) return;
|
|
6862
|
+
context.report({
|
|
6863
|
+
node: node.name,
|
|
6864
|
+
message: buildMessage$18(tagName)
|
|
6865
|
+
});
|
|
6866
|
+
} })
|
|
6867
|
+
});
|
|
6868
|
+
//#endregion
|
|
6398
6869
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
6399
|
-
const MESSAGE$
|
|
6870
|
+
const MESSAGE$39 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
|
|
6400
6871
|
const evaluateTitleValue = (value) => {
|
|
6401
6872
|
if (!value) return "missing";
|
|
6402
6873
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -6435,14 +6906,14 @@ const iframeHasTitle = defineRule({
|
|
|
6435
6906
|
if (!titleAttr) {
|
|
6436
6907
|
if (hasSpread || tag === "iframe") context.report({
|
|
6437
6908
|
node: node.name,
|
|
6438
|
-
message: MESSAGE$
|
|
6909
|
+
message: MESSAGE$39
|
|
6439
6910
|
});
|
|
6440
6911
|
return;
|
|
6441
6912
|
}
|
|
6442
6913
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
6443
6914
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
6444
6915
|
node: titleAttr,
|
|
6445
|
-
message: MESSAGE$
|
|
6916
|
+
message: MESSAGE$39
|
|
6446
6917
|
});
|
|
6447
6918
|
} })
|
|
6448
6919
|
});
|
|
@@ -6545,7 +7016,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
6545
7016
|
});
|
|
6546
7017
|
//#endregion
|
|
6547
7018
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
6548
|
-
const MESSAGE$
|
|
7019
|
+
const MESSAGE$38 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
|
|
6549
7020
|
const DEFAULT_COMPONENTS = ["img"];
|
|
6550
7021
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
6551
7022
|
"image",
|
|
@@ -6607,7 +7078,7 @@ const imgRedundantAlt = defineRule({
|
|
|
6607
7078
|
if (!altAttribute) return;
|
|
6608
7079
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
6609
7080
|
node: altAttribute,
|
|
6610
|
-
message: MESSAGE$
|
|
7081
|
+
message: MESSAGE$38
|
|
6611
7082
|
});
|
|
6612
7083
|
} };
|
|
6613
7084
|
}
|
|
@@ -6720,6 +7191,437 @@ const interactiveSupportsFocus = defineRule({
|
|
|
6720
7191
|
}
|
|
6721
7192
|
});
|
|
6722
7193
|
//#endregion
|
|
7194
|
+
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
7195
|
+
const collectFromProgram = (programRoot) => {
|
|
7196
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
7197
|
+
const visit = (node) => {
|
|
7198
|
+
if (node.type === "ImportDeclaration" && "source" in node && node.source) {
|
|
7199
|
+
const source = node.source.value;
|
|
7200
|
+
if (typeof source !== "string") return;
|
|
7201
|
+
if ("specifiers" in node && Array.isArray(node.specifiers)) for (const specifier of node.specifiers) {
|
|
7202
|
+
if (!("local" in specifier) || !specifier.local) continue;
|
|
7203
|
+
const local = specifier.local;
|
|
7204
|
+
if (typeof local.name !== "string") continue;
|
|
7205
|
+
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
7206
|
+
source,
|
|
7207
|
+
imported: null,
|
|
7208
|
+
isDefault: true,
|
|
7209
|
+
isNamespace: false
|
|
7210
|
+
});
|
|
7211
|
+
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
7212
|
+
source,
|
|
7213
|
+
imported: null,
|
|
7214
|
+
isDefault: false,
|
|
7215
|
+
isNamespace: true
|
|
7216
|
+
});
|
|
7217
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
7218
|
+
const importedNode = specifier.imported;
|
|
7219
|
+
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
7220
|
+
lookup.set(local.name, {
|
|
7221
|
+
source,
|
|
7222
|
+
imported: importedName,
|
|
7223
|
+
isDefault: false,
|
|
7224
|
+
isNamespace: false
|
|
7225
|
+
});
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
return;
|
|
7229
|
+
}
|
|
7230
|
+
const nodeRecord = node;
|
|
7231
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
7232
|
+
if (key === "parent") continue;
|
|
7233
|
+
const child = nodeRecord[key];
|
|
7234
|
+
if (Array.isArray(child)) {
|
|
7235
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
7236
|
+
} else if (isAstNode(child)) visit(child);
|
|
7237
|
+
}
|
|
7238
|
+
};
|
|
7239
|
+
visit(programRoot);
|
|
7240
|
+
return lookup;
|
|
7241
|
+
};
|
|
7242
|
+
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
7243
|
+
const getImportLookup = (node) => {
|
|
7244
|
+
const programRoot = findProgramRoot(node);
|
|
7245
|
+
if (!programRoot) return null;
|
|
7246
|
+
let cached = importLookupCache.get(programRoot);
|
|
7247
|
+
if (!cached) {
|
|
7248
|
+
cached = collectFromProgram(programRoot);
|
|
7249
|
+
importLookupCache.set(programRoot, cached);
|
|
7250
|
+
}
|
|
7251
|
+
return cached;
|
|
7252
|
+
};
|
|
7253
|
+
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7254
|
+
const lookup = getImportLookup(contextNode);
|
|
7255
|
+
if (!lookup) return false;
|
|
7256
|
+
const info = lookup.get(localIdentifierName);
|
|
7257
|
+
if (!info) return false;
|
|
7258
|
+
return info.source === moduleSource;
|
|
7259
|
+
};
|
|
7260
|
+
const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
7261
|
+
const lookup = getImportLookup(contextNode);
|
|
7262
|
+
if (!lookup) return null;
|
|
7263
|
+
const info = lookup.get(localIdentifierName);
|
|
7264
|
+
if (!info) return null;
|
|
7265
|
+
if (info.source !== moduleSource) return null;
|
|
7266
|
+
return info.imported;
|
|
7267
|
+
};
|
|
7268
|
+
//#endregion
|
|
7269
|
+
//#region src/plugin/rules/jotai/jotai-derived-atom-returns-fresh-object.ts
|
|
7270
|
+
const isAtomFromJotai = (callExpression) => {
|
|
7271
|
+
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
7272
|
+
const localName = callExpression.callee.name;
|
|
7273
|
+
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7274
|
+
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7275
|
+
};
|
|
7276
|
+
const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7277
|
+
const getFirstParameterName = (fn) => {
|
|
7278
|
+
const parameters = fn.params ?? [];
|
|
7279
|
+
if (parameters.length !== 1) return null;
|
|
7280
|
+
const first = parameters[0];
|
|
7281
|
+
return isNodeOfType(first, "Identifier") ? first.name : null;
|
|
7282
|
+
};
|
|
7283
|
+
const FRESH_ARRAY_INSTANCE_METHODS = new Set([
|
|
7284
|
+
"filter",
|
|
7285
|
+
"map",
|
|
7286
|
+
"flatMap",
|
|
7287
|
+
"slice",
|
|
7288
|
+
"concat",
|
|
7289
|
+
"flat",
|
|
7290
|
+
"toSorted",
|
|
7291
|
+
"toReversed",
|
|
7292
|
+
"toSpliced",
|
|
7293
|
+
"with",
|
|
7294
|
+
"sort",
|
|
7295
|
+
"reverse"
|
|
7296
|
+
]);
|
|
7297
|
+
const FRESH_STATIC_OBJECT_CALLS = {
|
|
7298
|
+
Object: new Set([
|
|
7299
|
+
"keys",
|
|
7300
|
+
"values",
|
|
7301
|
+
"entries",
|
|
7302
|
+
"fromEntries",
|
|
7303
|
+
"assign",
|
|
7304
|
+
"create"
|
|
7305
|
+
]),
|
|
7306
|
+
Array: new Set(["from", "of"])
|
|
7307
|
+
};
|
|
7308
|
+
const freshFromObjectLiteral = (expression) => {
|
|
7309
|
+
if (isNodeOfType(expression, "ObjectExpression")) return {
|
|
7310
|
+
kind: "object",
|
|
7311
|
+
reportNode: expression
|
|
7312
|
+
};
|
|
7313
|
+
if (isNodeOfType(expression, "ArrayExpression")) return {
|
|
7314
|
+
kind: "array",
|
|
7315
|
+
reportNode: expression
|
|
7316
|
+
};
|
|
7317
|
+
return null;
|
|
7318
|
+
};
|
|
7319
|
+
const freshFromMethodChain = (expression) => {
|
|
7320
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
7321
|
+
const callee = expression.callee;
|
|
7322
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
7323
|
+
if (callee.computed) return null;
|
|
7324
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
7325
|
+
const methodName = callee.property.name;
|
|
7326
|
+
if (FRESH_ARRAY_INSTANCE_METHODS.has(methodName)) return {
|
|
7327
|
+
kind: "array",
|
|
7328
|
+
reportNode: expression
|
|
7329
|
+
};
|
|
7330
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
7331
|
+
if (FRESH_STATIC_OBJECT_CALLS[callee.object.name]?.has(methodName)) return {
|
|
7332
|
+
kind: callee.object.name === "Array" || methodName === "keys" || methodName === "values" || methodName === "entries" ? "array" : "object",
|
|
7333
|
+
reportNode: expression
|
|
7334
|
+
};
|
|
7335
|
+
}
|
|
7336
|
+
return null;
|
|
7337
|
+
};
|
|
7338
|
+
const classifyReturnedExpression = (expression) => {
|
|
7339
|
+
if (!expression) return null;
|
|
7340
|
+
const inner = stripParenExpression(expression);
|
|
7341
|
+
const literalReturn = freshFromObjectLiteral(inner);
|
|
7342
|
+
if (literalReturn) return literalReturn;
|
|
7343
|
+
return freshFromMethodChain(inner);
|
|
7344
|
+
};
|
|
7345
|
+
const collectTopLevelReturnExpressions$1 = (block) => {
|
|
7346
|
+
const returns = [];
|
|
7347
|
+
walkAst(block, (child) => {
|
|
7348
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) return false;
|
|
7349
|
+
if (isNodeOfType(child, "ReturnStatement")) returns.push(child.argument);
|
|
7350
|
+
});
|
|
7351
|
+
return returns;
|
|
7352
|
+
};
|
|
7353
|
+
const getFreshReturnForFunction = (fn) => {
|
|
7354
|
+
const body = fn.body;
|
|
7355
|
+
if (!body) return null;
|
|
7356
|
+
if (!isNodeOfType(body, "BlockStatement")) return classifyReturnedExpression(body);
|
|
7357
|
+
const returnExpressions = collectTopLevelReturnExpressions$1(body);
|
|
7358
|
+
if (returnExpressions.length === 0) return null;
|
|
7359
|
+
let firstFresh = null;
|
|
7360
|
+
for (const returnArgument of returnExpressions) {
|
|
7361
|
+
const classification = classifyReturnedExpression(returnArgument);
|
|
7362
|
+
if (!classification) return null;
|
|
7363
|
+
if (!firstFresh) firstFresh = classification;
|
|
7364
|
+
}
|
|
7365
|
+
return firstFresh;
|
|
7366
|
+
};
|
|
7367
|
+
const functionBodyReferencesGetParameter = (fn, getParameterName) => {
|
|
7368
|
+
const body = fn.body;
|
|
7369
|
+
if (!body) return false;
|
|
7370
|
+
let found = false;
|
|
7371
|
+
walkAst(body, (child) => {
|
|
7372
|
+
if (found) return false;
|
|
7373
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) {
|
|
7374
|
+
if (child !== fn) return false;
|
|
7375
|
+
}
|
|
7376
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7377
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
7378
|
+
if (child.callee.name === getParameterName) {
|
|
7379
|
+
found = true;
|
|
7380
|
+
return false;
|
|
7381
|
+
}
|
|
7382
|
+
});
|
|
7383
|
+
return found;
|
|
7384
|
+
};
|
|
7385
|
+
const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
7386
|
+
id: "jotai-derived-atom-returns-fresh-object",
|
|
7387
|
+
severity: "warn",
|
|
7388
|
+
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",
|
|
7389
|
+
create: (context) => ({ CallExpression(node) {
|
|
7390
|
+
if (!isAtomFromJotai(node)) return;
|
|
7391
|
+
const args = node.arguments ?? [];
|
|
7392
|
+
if (args.length === 0) return;
|
|
7393
|
+
const reader = args[0];
|
|
7394
|
+
if (!isFunctionLike(reader)) return;
|
|
7395
|
+
const getParameterName = getFirstParameterName(reader);
|
|
7396
|
+
if (!getParameterName) return;
|
|
7397
|
+
const freshReturn = getFreshReturnForFunction(reader);
|
|
7398
|
+
if (!freshReturn) return;
|
|
7399
|
+
if (!functionBodyReferencesGetParameter(reader, getParameterName)) return;
|
|
7400
|
+
const shape = freshReturn.kind === "object" ? "object" : "array";
|
|
7401
|
+
context.report({
|
|
7402
|
+
node: freshReturn.reportNode,
|
|
7403
|
+
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)\``
|
|
7404
|
+
});
|
|
7405
|
+
} })
|
|
7406
|
+
});
|
|
7407
|
+
//#endregion
|
|
7408
|
+
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
7409
|
+
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
7410
|
+
const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
|
|
7411
|
+
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
7412
|
+
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
7413
|
+
const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
7414
|
+
const isImportedSelectAtom = (callExpression) => {
|
|
7415
|
+
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
7416
|
+
const localName = callExpression.callee.name;
|
|
7417
|
+
for (const source of JOTAI_SELECT_ATOM_SOURCES) {
|
|
7418
|
+
if (!isImportedFromModule(callExpression, localName, source)) continue;
|
|
7419
|
+
if (getImportedNameFromModule(callExpression, localName, source) === "selectAtom") return true;
|
|
7420
|
+
}
|
|
7421
|
+
return false;
|
|
7422
|
+
};
|
|
7423
|
+
const isCallbackOfMemoizingHook = (functionNode) => {
|
|
7424
|
+
const callParent = functionNode.parent;
|
|
7425
|
+
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
7426
|
+
if (!isNodeOfType(callParent.callee, "Identifier")) return false;
|
|
7427
|
+
if (!MEMOIZING_HOOK_NAMES.has(callParent.callee.name)) return false;
|
|
7428
|
+
return callParent.arguments?.[0] === functionNode;
|
|
7429
|
+
};
|
|
7430
|
+
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
7431
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
7432
|
+
const declaredName = functionNode.id.name;
|
|
7433
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
|
|
7434
|
+
}
|
|
7435
|
+
let cursor = functionNode.parent ?? null;
|
|
7436
|
+
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
7437
|
+
if (!cursor) return false;
|
|
7438
|
+
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
7439
|
+
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
7440
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN.test(cursor.id.name);
|
|
7441
|
+
};
|
|
7442
|
+
const jotaiSelectAtomInRenderBody = defineRule({
|
|
7443
|
+
id: "jotai-select-atom-in-render-body",
|
|
7444
|
+
severity: "error",
|
|
7445
|
+
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",
|
|
7446
|
+
create: (context) => ({ CallExpression(node) {
|
|
7447
|
+
if (!isImportedSelectAtom(node)) return;
|
|
7448
|
+
let cursor = node.parent ?? null;
|
|
7449
|
+
let nearestFunctionLike = null;
|
|
7450
|
+
while (cursor) {
|
|
7451
|
+
if (isFunctionLikeNode(cursor)) {
|
|
7452
|
+
nearestFunctionLike = cursor;
|
|
7453
|
+
break;
|
|
7454
|
+
}
|
|
7455
|
+
cursor = cursor.parent ?? null;
|
|
7456
|
+
}
|
|
7457
|
+
if (!nearestFunctionLike) return;
|
|
7458
|
+
if (isCallbackOfMemoizingHook(nearestFunctionLike)) return;
|
|
7459
|
+
let outerCursor = nearestFunctionLike;
|
|
7460
|
+
while (outerCursor) {
|
|
7461
|
+
if (isFunctionLikeNode(outerCursor) && containingFunctionIsComponentOrHook(outerCursor)) {
|
|
7462
|
+
context.report({
|
|
7463
|
+
node,
|
|
7464
|
+
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])`"
|
|
7465
|
+
});
|
|
7466
|
+
return;
|
|
7467
|
+
}
|
|
7468
|
+
outerCursor = outerCursor.parent ?? null;
|
|
7469
|
+
}
|
|
7470
|
+
} })
|
|
7471
|
+
});
|
|
7472
|
+
//#endregion
|
|
7473
|
+
//#region src/plugin/rules/jotai/jotai-tq-use-raw-query-atom.ts
|
|
7474
|
+
const QUERY_ATOM_FACTORY_IMPORTED_NAMES = new Set([
|
|
7475
|
+
"atomWithQuery",
|
|
7476
|
+
"atomWithSuspenseQuery",
|
|
7477
|
+
"atomWithInfiniteQuery",
|
|
7478
|
+
"atomWithSuspenseInfiniteQuery"
|
|
7479
|
+
]);
|
|
7480
|
+
const SUBSCRIBING_HOOK_NAMES = new Set(["useAtomValue", "useAtom"]);
|
|
7481
|
+
const QUERY_ATOM_NAMING_CONVENTION = /(SuspenseInfiniteQuery|SuspenseQuery|InfiniteQuery|Query)Atom$/;
|
|
7482
|
+
const jotaiTqUseRawQueryAtom = defineRule({
|
|
7483
|
+
id: "jotai-tq-use-raw-query-atom",
|
|
7484
|
+
severity: "warn",
|
|
7485
|
+
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)",
|
|
7486
|
+
create: (context) => {
|
|
7487
|
+
const queryAtomFactoryLocalNames = /* @__PURE__ */ new Set();
|
|
7488
|
+
const queryAtomBindingNames = /* @__PURE__ */ new Set();
|
|
7489
|
+
return {
|
|
7490
|
+
ImportDeclaration(node) {
|
|
7491
|
+
const source = node.source?.value;
|
|
7492
|
+
for (const specifier of node.specifiers ?? []) {
|
|
7493
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
7494
|
+
if (!isNodeOfType(specifier.local, "Identifier")) continue;
|
|
7495
|
+
const localName = specifier.local.name;
|
|
7496
|
+
if (source === "jotai-tanstack-query") {
|
|
7497
|
+
const importedName = getImportedName$1(specifier);
|
|
7498
|
+
if (importedName && QUERY_ATOM_FACTORY_IMPORTED_NAMES.has(importedName)) queryAtomFactoryLocalNames.add(localName);
|
|
7499
|
+
continue;
|
|
7500
|
+
}
|
|
7501
|
+
if (typeof source !== "string") continue;
|
|
7502
|
+
if (source.startsWith("jotai") || source === "react" || source.startsWith("react/")) continue;
|
|
7503
|
+
if (QUERY_ATOM_NAMING_CONVENTION.test(localName)) queryAtomBindingNames.add(localName);
|
|
7504
|
+
}
|
|
7505
|
+
},
|
|
7506
|
+
VariableDeclarator(node) {
|
|
7507
|
+
if (queryAtomFactoryLocalNames.size === 0) return;
|
|
7508
|
+
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
7509
|
+
const initializer = node.init;
|
|
7510
|
+
if (!isNodeOfType(initializer, "CallExpression")) return;
|
|
7511
|
+
if (!isNodeOfType(initializer.callee, "Identifier")) return;
|
|
7512
|
+
if (!queryAtomFactoryLocalNames.has(initializer.callee.name)) return;
|
|
7513
|
+
queryAtomBindingNames.add(node.id.name);
|
|
7514
|
+
},
|
|
7515
|
+
CallExpression(node) {
|
|
7516
|
+
if (queryAtomBindingNames.size === 0) return;
|
|
7517
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
7518
|
+
if (!SUBSCRIBING_HOOK_NAMES.has(node.callee.name)) return;
|
|
7519
|
+
const args = node.arguments ?? [];
|
|
7520
|
+
if (args.length === 0) return;
|
|
7521
|
+
const firstArgument = args[0];
|
|
7522
|
+
if (!isNodeOfType(firstArgument, "Identifier")) return;
|
|
7523
|
+
if (!queryAtomBindingNames.has(firstArgument.name)) return;
|
|
7524
|
+
context.report({
|
|
7525
|
+
node,
|
|
7526
|
+
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)\``
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7529
|
+
};
|
|
7530
|
+
}
|
|
7531
|
+
});
|
|
7532
|
+
//#endregion
|
|
7533
|
+
//#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
|
|
7534
|
+
const isAsyncFunctionLike$1 = (node) => {
|
|
7535
|
+
if (!node) return false;
|
|
7536
|
+
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
7537
|
+
return node.async === true;
|
|
7538
|
+
};
|
|
7539
|
+
const classifyFirstParameter = (fn) => {
|
|
7540
|
+
const parameters = fn.params ?? [];
|
|
7541
|
+
if (parameters.length === 0) return null;
|
|
7542
|
+
const first = parameters[0];
|
|
7543
|
+
if (isNodeOfType(first, "Identifier")) return {
|
|
7544
|
+
kind: "identifier",
|
|
7545
|
+
name: first.name
|
|
7546
|
+
};
|
|
7547
|
+
if (isNodeOfType(first, "ArrayPattern") || isNodeOfType(first, "ObjectPattern")) return { kind: "destructured" };
|
|
7548
|
+
if (isNodeOfType(first, "AssignmentPattern")) {
|
|
7549
|
+
if (isNodeOfType(first.left, "Identifier")) return {
|
|
7550
|
+
kind: "identifier",
|
|
7551
|
+
name: first.left.name
|
|
7552
|
+
};
|
|
7553
|
+
if (isNodeOfType(first.left, "ArrayPattern") || isNodeOfType(first.left, "ObjectPattern")) return { kind: "destructured" };
|
|
7554
|
+
}
|
|
7555
|
+
return null;
|
|
7556
|
+
};
|
|
7557
|
+
const isReduceCallee = (callee) => {
|
|
7558
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
7559
|
+
if (!callee.computed) {
|
|
7560
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
7561
|
+
if (callee.property.name !== "reduce" && callee.property.name !== "reduceRight") return null;
|
|
7562
|
+
return { methodName: callee.property.name };
|
|
7563
|
+
}
|
|
7564
|
+
if (isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string") {
|
|
7565
|
+
const propertyName = callee.property.value;
|
|
7566
|
+
if (propertyName !== "reduce" && propertyName !== "reduceRight") return null;
|
|
7567
|
+
return { methodName: propertyName };
|
|
7568
|
+
}
|
|
7569
|
+
return null;
|
|
7570
|
+
};
|
|
7571
|
+
const bodyAwaitsAccumulator = (fn, accumulatorName) => {
|
|
7572
|
+
const body = fn.body;
|
|
7573
|
+
if (!body) return false;
|
|
7574
|
+
let awaitsAccumulator = false;
|
|
7575
|
+
walkAst(body, (child) => {
|
|
7576
|
+
if (awaitsAccumulator) return false;
|
|
7577
|
+
if (isNodeOfType(child, "FunctionDeclaration") || isNodeOfType(child, "FunctionExpression") || isNodeOfType(child, "ArrowFunctionExpression")) {
|
|
7578
|
+
if (child !== fn) return false;
|
|
7579
|
+
}
|
|
7580
|
+
if (!isNodeOfType(child, "AwaitExpression")) return;
|
|
7581
|
+
if (!child.argument) return;
|
|
7582
|
+
const awaitArgument = stripParenExpression(child.argument);
|
|
7583
|
+
if (isNodeOfType(awaitArgument, "Identifier") && awaitArgument.name === accumulatorName) {
|
|
7584
|
+
awaitsAccumulator = true;
|
|
7585
|
+
return false;
|
|
7586
|
+
}
|
|
7587
|
+
});
|
|
7588
|
+
return awaitsAccumulator;
|
|
7589
|
+
};
|
|
7590
|
+
const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
7591
|
+
id: "js-async-reduce-without-awaited-acc",
|
|
7592
|
+
severity: "warn",
|
|
7593
|
+
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",
|
|
7594
|
+
create: (context) => ({ CallExpression(node) {
|
|
7595
|
+
const reduceMatch = isReduceCallee(node.callee);
|
|
7596
|
+
if (!reduceMatch) return;
|
|
7597
|
+
const args = node.arguments ?? [];
|
|
7598
|
+
if (args.length === 0) return;
|
|
7599
|
+
const reducerCandidate = stripParenExpression(args[0]);
|
|
7600
|
+
if (!isAsyncFunctionLike$1(reducerCandidate)) return;
|
|
7601
|
+
const reducer = reducerCandidate;
|
|
7602
|
+
if (!containsDirectAwait(reducer.body)) return;
|
|
7603
|
+
const firstParameter = classifyFirstParameter(reducer);
|
|
7604
|
+
if (!firstParameter) return;
|
|
7605
|
+
if (firstParameter.kind === "destructured") {
|
|
7606
|
+
context.report({
|
|
7607
|
+
node: reducer,
|
|
7608
|
+
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([...])\``
|
|
7609
|
+
});
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
if (bodyAwaitsAccumulator(reducer, firstParameter.name)) return;
|
|
7613
|
+
const previousParamName = [
|
|
7614
|
+
"previous",
|
|
7615
|
+
"prev",
|
|
7616
|
+
"priorResult"
|
|
7617
|
+
].find((candidate) => candidate !== firstParameter.name) ?? `${firstParameter.name}Prev`;
|
|
7618
|
+
context.report({
|
|
7619
|
+
node: reducer,
|
|
7620
|
+
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(...)\``
|
|
7621
|
+
});
|
|
7622
|
+
} })
|
|
7623
|
+
});
|
|
7624
|
+
//#endregion
|
|
6723
7625
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
6724
7626
|
const ITERATOR_METHOD_NAMES$1 = new Set([
|
|
6725
7627
|
"forEach",
|
|
@@ -6743,7 +7645,7 @@ const isInsideLoopContext = (node) => {
|
|
|
6743
7645
|
let current = node.parent;
|
|
6744
7646
|
while (current) {
|
|
6745
7647
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
6746
|
-
if (isFunctionLike(current)) {
|
|
7648
|
+
if (isFunctionLike$1(current)) {
|
|
6747
7649
|
if (isIteratorCallback(current)) return true;
|
|
6748
7650
|
return false;
|
|
6749
7651
|
}
|
|
@@ -7097,7 +7999,7 @@ const jsHoistIntl = defineRule({
|
|
|
7097
7999
|
let cursor = node.parent ?? null;
|
|
7098
8000
|
let inFunctionBody = false;
|
|
7099
8001
|
while (cursor) {
|
|
7100
|
-
if (isFunctionLike(cursor)) {
|
|
8002
|
+
if (isFunctionLike$1(cursor)) {
|
|
7101
8003
|
inFunctionBody = true;
|
|
7102
8004
|
const fnParent = cursor.parent;
|
|
7103
8005
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -7527,6 +8429,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
7527
8429
|
id: "js-tosorted-immutable",
|
|
7528
8430
|
tags: ["test-noise"],
|
|
7529
8431
|
severity: "warn",
|
|
8432
|
+
disabledBy: ["react-native"],
|
|
7530
8433
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
|
|
7531
8434
|
create: (context) => ({ CallExpression(node) {
|
|
7532
8435
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
@@ -7815,7 +8718,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
7815
8718
|
const settings = resolveSettings$34(context.settings);
|
|
7816
8719
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
7817
8720
|
const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
|
|
7818
|
-
const filename =
|
|
8721
|
+
const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
|
|
7819
8722
|
const extensionOnly = path.extname(filename).slice(1);
|
|
7820
8723
|
const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
|
|
7821
8724
|
let didReportMismatch = false;
|
|
@@ -8440,33 +9343,8 @@ const findVariableInitializer = (referenceNode, bindingName) => {
|
|
|
8440
9343
|
return best;
|
|
8441
9344
|
};
|
|
8442
9345
|
//#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
9346
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
8469
|
-
const buildMessage$
|
|
9347
|
+
const buildMessage$17 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
|
|
8470
9348
|
const DEFAULT_MAX_DEPTH = 14;
|
|
8471
9349
|
const resolveSettings$30 = (settings) => {
|
|
8472
9350
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -8533,7 +9411,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8533
9411
|
const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
|
|
8534
9412
|
if (total > max) context.report({
|
|
8535
9413
|
node,
|
|
8536
|
-
message: buildMessage$
|
|
9414
|
+
message: buildMessage$17(total, max)
|
|
8537
9415
|
});
|
|
8538
9416
|
};
|
|
8539
9417
|
return {
|
|
@@ -8548,7 +9426,7 @@ const jsxMaxDepth = defineRule({
|
|
|
8548
9426
|
});
|
|
8549
9427
|
//#endregion
|
|
8550
9428
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
8551
|
-
const MESSAGE$
|
|
9429
|
+
const MESSAGE$37 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
|
|
8552
9430
|
const LITERAL_TEXT_TAGS = new Set([
|
|
8553
9431
|
"code",
|
|
8554
9432
|
"pre",
|
|
@@ -8583,7 +9461,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
8583
9461
|
if (isInsideLiteralTextTag(node)) return;
|
|
8584
9462
|
context.report({
|
|
8585
9463
|
node,
|
|
8586
|
-
message: MESSAGE$
|
|
9464
|
+
message: MESSAGE$37
|
|
8587
9465
|
});
|
|
8588
9466
|
} })
|
|
8589
9467
|
});
|
|
@@ -8605,7 +9483,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
8605
9483
|
};
|
|
8606
9484
|
//#endregion
|
|
8607
9485
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
8608
|
-
const MESSAGE$
|
|
9486
|
+
const MESSAGE$36 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
|
|
8609
9487
|
const isConstructedValue = (expression) => {
|
|
8610
9488
|
const stripped = stripParenExpression(expression);
|
|
8611
9489
|
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 +9502,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
8624
9502
|
recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
|
|
8625
9503
|
category: "Performance",
|
|
8626
9504
|
create: (context) => {
|
|
8627
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9505
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
8628
9506
|
return { JSXOpeningElement(node) {
|
|
8629
9507
|
if (isTestlikeFile) return;
|
|
8630
9508
|
if (!isProviderName(node.name)) return;
|
|
@@ -8641,7 +9519,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
8641
9519
|
if (!isConstructedValue(innerExpression)) continue;
|
|
8642
9520
|
context.report({
|
|
8643
9521
|
node: attribute,
|
|
8644
|
-
message: MESSAGE$
|
|
9522
|
+
message: MESSAGE$36
|
|
8645
9523
|
});
|
|
8646
9524
|
}
|
|
8647
9525
|
} };
|
|
@@ -8724,7 +9602,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
8724
9602
|
};
|
|
8725
9603
|
//#endregion
|
|
8726
9604
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
8727
|
-
const MESSAGE$
|
|
9605
|
+
const MESSAGE$35 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
|
|
8728
9606
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
8729
9607
|
"icon",
|
|
8730
9608
|
"Icon",
|
|
@@ -8970,7 +9848,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
8970
9848
|
recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
|
|
8971
9849
|
category: "Performance",
|
|
8972
9850
|
create: (context) => {
|
|
8973
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
9851
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
8974
9852
|
let memoRegistry = null;
|
|
8975
9853
|
return {
|
|
8976
9854
|
Program(node) {
|
|
@@ -8992,7 +9870,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
8992
9870
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
8993
9871
|
context.report({
|
|
8994
9872
|
node,
|
|
8995
|
-
message: MESSAGE$
|
|
9873
|
+
message: MESSAGE$35
|
|
8996
9874
|
});
|
|
8997
9875
|
}
|
|
8998
9876
|
};
|
|
@@ -9280,7 +10158,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
9280
10158
|
];
|
|
9281
10159
|
//#endregion
|
|
9282
10160
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
9283
|
-
const MESSAGE$
|
|
10161
|
+
const MESSAGE$34 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
|
|
9284
10162
|
const isDataArrayPropName = (propName) => {
|
|
9285
10163
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
9286
10164
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -9341,7 +10219,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
9341
10219
|
recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
|
|
9342
10220
|
category: "Performance",
|
|
9343
10221
|
create: (context) => {
|
|
9344
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10222
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9345
10223
|
let memoRegistry = null;
|
|
9346
10224
|
return {
|
|
9347
10225
|
Program(node) {
|
|
@@ -9363,7 +10241,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
9363
10241
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
9364
10242
|
context.report({
|
|
9365
10243
|
node,
|
|
9366
|
-
message: MESSAGE$
|
|
10244
|
+
message: MESSAGE$34
|
|
9367
10245
|
});
|
|
9368
10246
|
}
|
|
9369
10247
|
};
|
|
@@ -9621,7 +10499,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
9621
10499
|
]);
|
|
9622
10500
|
//#endregion
|
|
9623
10501
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
9624
|
-
const MESSAGE$
|
|
10502
|
+
const MESSAGE$33 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
|
|
9625
10503
|
const isAccessorPredicateName = (propName) => {
|
|
9626
10504
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
9627
10505
|
if (propName.length <= prefix.length) continue;
|
|
@@ -9803,7 +10681,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
9803
10681
|
recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
|
|
9804
10682
|
category: "Performance",
|
|
9805
10683
|
create: (context) => {
|
|
9806
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10684
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
9807
10685
|
let memoRegistry = null;
|
|
9808
10686
|
return {
|
|
9809
10687
|
Program(node) {
|
|
@@ -9826,7 +10704,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
9826
10704
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
9827
10705
|
context.report({
|
|
9828
10706
|
node,
|
|
9829
|
-
message: MESSAGE$
|
|
10707
|
+
message: MESSAGE$33
|
|
9830
10708
|
});
|
|
9831
10709
|
}
|
|
9832
10710
|
};
|
|
@@ -10046,7 +10924,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
10046
10924
|
];
|
|
10047
10925
|
//#endregion
|
|
10048
10926
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
10049
|
-
const MESSAGE$
|
|
10927
|
+
const MESSAGE$32 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
|
|
10050
10928
|
const isConfigObjectPropName = (propName) => {
|
|
10051
10929
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
10052
10930
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -10109,7 +10987,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10109
10987
|
recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
|
|
10110
10988
|
category: "Performance",
|
|
10111
10989
|
create: (context) => {
|
|
10112
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
10990
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10113
10991
|
let memoRegistry = null;
|
|
10114
10992
|
return {
|
|
10115
10993
|
Program(node) {
|
|
@@ -10133,7 +11011,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10133
11011
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
10134
11012
|
context.report({
|
|
10135
11013
|
node,
|
|
10136
|
-
message: MESSAGE$
|
|
11014
|
+
message: MESSAGE$32
|
|
10137
11015
|
});
|
|
10138
11016
|
}
|
|
10139
11017
|
};
|
|
@@ -10141,7 +11019,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
10141
11019
|
});
|
|
10142
11020
|
//#endregion
|
|
10143
11021
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
10144
|
-
const MESSAGE$
|
|
11022
|
+
const MESSAGE$31 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
|
|
10145
11023
|
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
11024
|
const resolveSettings$29 = (settings) => {
|
|
10147
11025
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -10181,7 +11059,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
10181
11059
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
10182
11060
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
10183
11061
|
node: attribute,
|
|
10184
|
-
message: MESSAGE$
|
|
11062
|
+
message: MESSAGE$31
|
|
10185
11063
|
});
|
|
10186
11064
|
}
|
|
10187
11065
|
} };
|
|
@@ -10469,7 +11347,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
10469
11347
|
});
|
|
10470
11348
|
//#endregion
|
|
10471
11349
|
//#region src/plugin/rules/react-builtins/jsx-no-undef.ts
|
|
10472
|
-
const buildMessage$
|
|
11350
|
+
const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
|
|
10473
11351
|
const KNOWN_GLOBALS = new Set([
|
|
10474
11352
|
"globalThis",
|
|
10475
11353
|
"window",
|
|
@@ -10504,7 +11382,7 @@ const jsxNoUndef = defineRule({
|
|
|
10504
11382
|
if (findVariableInitializer(node, rootIdentifier)) return;
|
|
10505
11383
|
context.report({
|
|
10506
11384
|
node: node.name,
|
|
10507
|
-
message: buildMessage$
|
|
11385
|
+
message: buildMessage$16(rootIdentifier)
|
|
10508
11386
|
});
|
|
10509
11387
|
} })
|
|
10510
11388
|
});
|
|
@@ -10603,7 +11481,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
10603
11481
|
});
|
|
10604
11482
|
//#endregion
|
|
10605
11483
|
//#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
|
|
10606
|
-
const buildMessage$
|
|
11484
|
+
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
11485
|
const resolveSettings$26 = (settings) => {
|
|
10608
11486
|
const reactDoctor = settings?.["react-doctor"];
|
|
10609
11487
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
|
|
@@ -10719,7 +11597,7 @@ const jsxPascalCase = defineRule({
|
|
|
10719
11597
|
if (!isPascal && !isAllCaps) {
|
|
10720
11598
|
context.report({
|
|
10721
11599
|
node,
|
|
10722
|
-
message: buildMessage$
|
|
11600
|
+
message: buildMessage$15(segment, settings.allowAllCaps)
|
|
10723
11601
|
});
|
|
10724
11602
|
return;
|
|
10725
11603
|
}
|
|
@@ -10771,7 +11649,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
10771
11649
|
});
|
|
10772
11650
|
//#endregion
|
|
10773
11651
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
10774
|
-
const MESSAGE$
|
|
11652
|
+
const MESSAGE$30 = "JSX prop spreading is forbidden — list each prop explicitly.";
|
|
10775
11653
|
const resolveSettings$25 = (settings) => {
|
|
10776
11654
|
const reactDoctor = settings?.["react-doctor"];
|
|
10777
11655
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -10811,7 +11689,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
10811
11689
|
}
|
|
10812
11690
|
context.report({
|
|
10813
11691
|
node: attribute,
|
|
10814
|
-
message: MESSAGE$
|
|
11692
|
+
message: MESSAGE$30
|
|
10815
11693
|
});
|
|
10816
11694
|
}
|
|
10817
11695
|
} };
|
|
@@ -10914,7 +11792,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
10914
11792
|
category: "Accessibility",
|
|
10915
11793
|
create: (context) => {
|
|
10916
11794
|
const settings = resolveSettings$24(context.settings);
|
|
10917
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
11795
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10918
11796
|
return { JSXElement(node) {
|
|
10919
11797
|
if (isTestlikeFile) return;
|
|
10920
11798
|
const opening = node.openingElement;
|
|
@@ -10966,7 +11844,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
10966
11844
|
});
|
|
10967
11845
|
//#endregion
|
|
10968
11846
|
//#region src/plugin/rules/a11y/lang.ts
|
|
10969
|
-
const MESSAGE$
|
|
11847
|
+
const MESSAGE$29 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
|
|
10970
11848
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
10971
11849
|
"aa",
|
|
10972
11850
|
"ab",
|
|
@@ -11177,7 +12055,7 @@ const lang = defineRule({
|
|
|
11177
12055
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
11178
12056
|
context.report({
|
|
11179
12057
|
node: langAttr,
|
|
11180
|
-
message: MESSAGE$
|
|
12058
|
+
message: MESSAGE$29
|
|
11181
12059
|
});
|
|
11182
12060
|
return;
|
|
11183
12061
|
}
|
|
@@ -11186,13 +12064,13 @@ const lang = defineRule({
|
|
|
11186
12064
|
if (value === null) return;
|
|
11187
12065
|
if (!isValidLangTag(value)) context.report({
|
|
11188
12066
|
node: langAttr,
|
|
11189
|
-
message: MESSAGE$
|
|
12067
|
+
message: MESSAGE$29
|
|
11190
12068
|
});
|
|
11191
12069
|
} })
|
|
11192
12070
|
});
|
|
11193
12071
|
//#endregion
|
|
11194
12072
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
11195
|
-
const MESSAGE$
|
|
12073
|
+
const MESSAGE$28 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
|
|
11196
12074
|
const DEFAULT_AUDIO = ["audio"];
|
|
11197
12075
|
const DEFAULT_VIDEO = ["video"];
|
|
11198
12076
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -11232,7 +12110,7 @@ const mediaHasCaption = defineRule({
|
|
|
11232
12110
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
11233
12111
|
context.report({
|
|
11234
12112
|
node: node.name,
|
|
11235
|
-
message: MESSAGE$
|
|
12113
|
+
message: MESSAGE$28
|
|
11236
12114
|
});
|
|
11237
12115
|
return;
|
|
11238
12116
|
}
|
|
@@ -11249,7 +12127,7 @@ const mediaHasCaption = defineRule({
|
|
|
11249
12127
|
return kindValue.value.toLowerCase() === "captions";
|
|
11250
12128
|
})) context.report({
|
|
11251
12129
|
node: node.name,
|
|
11252
|
-
message: MESSAGE$
|
|
12130
|
+
message: MESSAGE$28
|
|
11253
12131
|
});
|
|
11254
12132
|
} };
|
|
11255
12133
|
}
|
|
@@ -11452,7 +12330,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
11452
12330
|
severity: "warn",
|
|
11453
12331
|
recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
|
|
11454
12332
|
create: (context) => ({ Program(programNode) {
|
|
11455
|
-
const filename = normalizeFilename$1(context.
|
|
12333
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11456
12334
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
11457
12335
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
11458
12336
|
if (!programNode.body?.some((statement) => {
|
|
@@ -11517,7 +12395,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
11517
12395
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
11518
12396
|
const callback = getEffectCallback(node);
|
|
11519
12397
|
if (!callback || !containsFetchCall(callback)) return;
|
|
11520
|
-
const filename = normalizeFilename$1(context.
|
|
12398
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11521
12399
|
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
|
|
11522
12400
|
node,
|
|
11523
12401
|
message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
|
|
@@ -11550,7 +12428,7 @@ const nextjsNoClientSideRedirect = defineRule({
|
|
|
11550
12428
|
severity: "warn",
|
|
11551
12429
|
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
12430
|
create: (context) => {
|
|
11553
|
-
const filename = normalizeFilename$1(context.
|
|
12431
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11554
12432
|
const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
|
|
11555
12433
|
return { CallExpression(node) {
|
|
11556
12434
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
@@ -11619,7 +12497,7 @@ const nextjsNoHeadImport = defineRule({
|
|
|
11619
12497
|
recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
|
|
11620
12498
|
create: (context) => ({ ImportDeclaration(node) {
|
|
11621
12499
|
if (node.source?.value !== "next/head") return;
|
|
11622
|
-
const filename = normalizeFilename$1(context.
|
|
12500
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11623
12501
|
if (!APP_DIRECTORY_PATTERN.test(filename)) return;
|
|
11624
12502
|
context.report({
|
|
11625
12503
|
node,
|
|
@@ -11636,7 +12514,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
11636
12514
|
severity: "warn",
|
|
11637
12515
|
recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
|
|
11638
12516
|
create: (context) => {
|
|
11639
|
-
const filename = normalizeFilename$1(context.
|
|
12517
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11640
12518
|
const isOgRoute = OG_ROUTE_PATTERN.test(filename);
|
|
11641
12519
|
return { JSXOpeningElement(node) {
|
|
11642
12520
|
if (isOgRoute) return;
|
|
@@ -11940,7 +12818,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
11940
12818
|
};
|
|
11941
12819
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
11942
12820
|
if (remainingDepth <= 0) return [];
|
|
11943
|
-
if (isFunctionLike(expression)) return expression.body ? [expression.body] : [];
|
|
12821
|
+
if (isFunctionLike$1(expression)) return expression.body ? [expression.body] : [];
|
|
11944
12822
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
11945
12823
|
for (const callArgument of expression.arguments ?? []) {
|
|
11946
12824
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -11990,7 +12868,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
11990
12868
|
resolveBinding = buildProgramBindingLookup(node);
|
|
11991
12869
|
},
|
|
11992
12870
|
ExportNamedDeclaration(node) {
|
|
11993
|
-
const filename = normalizeFilename$1(context.
|
|
12871
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
11994
12872
|
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
11995
12873
|
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
11996
12874
|
if (!isExportedGetHandler(node)) return;
|
|
@@ -12020,14 +12898,6 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
12020
12898
|
}
|
|
12021
12899
|
});
|
|
12022
12900
|
//#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
12901
|
//#region src/plugin/rules/nextjs/nextjs-no-use-search-params-without-suspense.ts
|
|
12032
12902
|
const fileMentionsSuspense = (programNode) => {
|
|
12033
12903
|
let didSee = false;
|
|
@@ -12038,7 +12908,7 @@ const fileMentionsSuspense = (programNode) => {
|
|
|
12038
12908
|
return false;
|
|
12039
12909
|
}
|
|
12040
12910
|
if (isNodeOfType(child, "ImportDeclaration") && child.source?.value === "react") {
|
|
12041
|
-
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName(specifier) === "Suspense")) {
|
|
12911
|
+
if ((child.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ImportSpecifier") && getImportedName$1(specifier) === "Suspense")) {
|
|
12042
12912
|
didSee = true;
|
|
12043
12913
|
return false;
|
|
12044
12914
|
}
|
|
@@ -12071,7 +12941,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
|
|
|
12071
12941
|
});
|
|
12072
12942
|
//#endregion
|
|
12073
12943
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
12074
|
-
const MESSAGE$
|
|
12944
|
+
const MESSAGE$27 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
|
|
12075
12945
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
12076
12946
|
const noAccessKey = defineRule({
|
|
12077
12947
|
id: "no-access-key",
|
|
@@ -12087,7 +12957,7 @@ const noAccessKey = defineRule({
|
|
|
12087
12957
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
12088
12958
|
context.report({
|
|
12089
12959
|
node: accessKey,
|
|
12090
|
-
message: MESSAGE$
|
|
12960
|
+
message: MESSAGE$27
|
|
12091
12961
|
});
|
|
12092
12962
|
return;
|
|
12093
12963
|
}
|
|
@@ -12097,7 +12967,7 @@ const noAccessKey = defineRule({
|
|
|
12097
12967
|
if (isUndefinedIdentifier(expression)) return;
|
|
12098
12968
|
context.report({
|
|
12099
12969
|
node: accessKey,
|
|
12100
|
-
message: MESSAGE$
|
|
12970
|
+
message: MESSAGE$27
|
|
12101
12971
|
});
|
|
12102
12972
|
}
|
|
12103
12973
|
} })
|
|
@@ -12406,7 +13276,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
12406
13276
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
12407
13277
|
if (isNodeOfType(fn, "Identifier")) {
|
|
12408
13278
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
12409
|
-
if (definitionNode && isFunctionLike(definitionNode)) return definitionNode;
|
|
13279
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return definitionNode;
|
|
12410
13280
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12411
13281
|
const initializer = definitionNode.init;
|
|
12412
13282
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -12499,14 +13369,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
12499
13369
|
return node ?? null;
|
|
12500
13370
|
};
|
|
12501
13371
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
12502
|
-
if (isFunctionLike(node)) return true;
|
|
13372
|
+
if (isFunctionLike$1(node)) return true;
|
|
12503
13373
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
12504
13374
|
if (isNodeOfType(node, "Identifier")) {
|
|
12505
13375
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
12506
|
-
if (definitionNode && isFunctionLike(definitionNode)) return true;
|
|
13376
|
+
if (definitionNode && isFunctionLike$1(definitionNode)) return true;
|
|
12507
13377
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
12508
13378
|
const initializer = definitionNode.init;
|
|
12509
|
-
return isFunctionLike(initializer);
|
|
13379
|
+
return isFunctionLike$1(initializer);
|
|
12510
13380
|
}
|
|
12511
13381
|
}
|
|
12512
13382
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -12516,7 +13386,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12516
13386
|
if (visited.has(node)) return false;
|
|
12517
13387
|
visited.add(node);
|
|
12518
13388
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
12519
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike(node)) return false;
|
|
13389
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
|
|
12520
13390
|
const record = node;
|
|
12521
13391
|
for (const [key, value] of Object.entries(record)) {
|
|
12522
13392
|
if (key === "parent") continue;
|
|
@@ -12528,7 +13398,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
12528
13398
|
};
|
|
12529
13399
|
const hasCleanup = (analysis, node) => {
|
|
12530
13400
|
const fn = getEffectFn(analysis, node);
|
|
12531
|
-
if (!isFunctionLike(fn)) return false;
|
|
13401
|
+
if (!isFunctionLike$1(fn)) return false;
|
|
12532
13402
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
12533
13403
|
return hasCleanupReturn(analysis, fn.body);
|
|
12534
13404
|
};
|
|
@@ -12542,9 +13412,9 @@ const findContainingNode = (analysis, node) => {
|
|
|
12542
13412
|
//#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
|
|
12543
13413
|
const noAdjustStateOnPropChange = defineRule({
|
|
12544
13414
|
id: "no-adjust-state-on-prop-change",
|
|
12545
|
-
severity: "
|
|
13415
|
+
severity: "error",
|
|
12546
13416
|
tags: ["test-noise"],
|
|
12547
|
-
recommendation: "Adjust the state inline during render
|
|
13417
|
+
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
12548
13418
|
create: (context) => ({ CallExpression(node) {
|
|
12549
13419
|
if (!isUseEffect(node)) return;
|
|
12550
13420
|
const analysis = getProgramAnalysis(node);
|
|
@@ -12563,14 +13433,14 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
12563
13433
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
|
|
12564
13434
|
context.report({
|
|
12565
13435
|
node: callExpr,
|
|
12566
|
-
message: "
|
|
13436
|
+
message: "State adjusted in a useEffect when a prop changes — forces an extra render with a stale UI between the two commits. Adjust the state during render with a `prev`-prop comparison instead, or refactor to remove the duplicated state."
|
|
12567
13437
|
});
|
|
12568
13438
|
}
|
|
12569
13439
|
} })
|
|
12570
13440
|
});
|
|
12571
13441
|
//#endregion
|
|
12572
13442
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
12573
|
-
const MESSAGE$
|
|
13443
|
+
const MESSAGE$26 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
|
|
12574
13444
|
const noAriaHiddenOnFocusable = defineRule({
|
|
12575
13445
|
id: "no-aria-hidden-on-focusable",
|
|
12576
13446
|
tags: ["react-jsx-only"],
|
|
@@ -12596,7 +13466,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
12596
13466
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
12597
13467
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
12598
13468
|
node: ariaHidden,
|
|
12599
|
-
message: MESSAGE$
|
|
13469
|
+
message: MESSAGE$26
|
|
12600
13470
|
});
|
|
12601
13471
|
} })
|
|
12602
13472
|
});
|
|
@@ -12876,7 +13746,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
12876
13746
|
let current = node;
|
|
12877
13747
|
while (current.parent) {
|
|
12878
13748
|
const parent = current.parent;
|
|
12879
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13749
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12880
13750
|
const callee = parent.callee;
|
|
12881
13751
|
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
13752
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -12895,7 +13765,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
12895
13765
|
let current = node;
|
|
12896
13766
|
while (current.parent) {
|
|
12897
13767
|
const parent = current.parent;
|
|
12898
|
-
if (isFunctionLike(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
13768
|
+
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
12899
13769
|
const callee = parent.callee;
|
|
12900
13770
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
12901
13771
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -12963,7 +13833,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
12963
13833
|
});
|
|
12964
13834
|
//#endregion
|
|
12965
13835
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
12966
|
-
const MESSAGE$
|
|
13836
|
+
const MESSAGE$25 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
|
|
12967
13837
|
const SECOND_INDEX_METHODS = new Set([
|
|
12968
13838
|
"every",
|
|
12969
13839
|
"filter",
|
|
@@ -13165,7 +14035,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13165
14035
|
}
|
|
13166
14036
|
context.report({
|
|
13167
14037
|
node: keyAttribute,
|
|
13168
|
-
message: MESSAGE$
|
|
14038
|
+
message: MESSAGE$25
|
|
13169
14039
|
});
|
|
13170
14040
|
},
|
|
13171
14041
|
CallExpression(node) {
|
|
@@ -13185,7 +14055,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13185
14055
|
if (propName !== "key") continue;
|
|
13186
14056
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
13187
14057
|
node: property,
|
|
13188
|
-
message: MESSAGE$
|
|
14058
|
+
message: MESSAGE$25
|
|
13189
14059
|
});
|
|
13190
14060
|
}
|
|
13191
14061
|
}
|
|
@@ -13193,7 +14063,7 @@ const noArrayIndexKey = defineRule({
|
|
|
13193
14063
|
});
|
|
13194
14064
|
//#endregion
|
|
13195
14065
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
13196
|
-
const MESSAGE$
|
|
14066
|
+
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
14067
|
const resolveSettings$21 = (settings) => {
|
|
13198
14068
|
const reactDoctor = settings?.["react-doctor"];
|
|
13199
14069
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -13230,7 +14100,7 @@ const noAutofocus = defineRule({
|
|
|
13230
14100
|
category: "Accessibility",
|
|
13231
14101
|
create: (context) => {
|
|
13232
14102
|
const settings = resolveSettings$21(context.settings);
|
|
13233
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
14103
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
13234
14104
|
return { JSXOpeningElement(node) {
|
|
13235
14105
|
if (isTestlikeFile) return;
|
|
13236
14106
|
const autoFocusAttribute = node.attributes.find((attribute) => {
|
|
@@ -13248,7 +14118,7 @@ const noAutofocus = defineRule({
|
|
|
13248
14118
|
}
|
|
13249
14119
|
context.report({
|
|
13250
14120
|
node: autoFocusAttribute,
|
|
13251
|
-
message: MESSAGE$
|
|
14121
|
+
message: MESSAGE$24
|
|
13252
14122
|
});
|
|
13253
14123
|
} };
|
|
13254
14124
|
}
|
|
@@ -13604,7 +14474,7 @@ const noBarrelImport = defineRule({
|
|
|
13604
14474
|
if (didReportForFile) return;
|
|
13605
14475
|
const source = node.source?.value;
|
|
13606
14476
|
if (typeof source !== "string" || !source.startsWith(".")) return;
|
|
13607
|
-
const filename = normalizeFilename$1(context.
|
|
14477
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
13608
14478
|
if (!filename) return;
|
|
13609
14479
|
const importRequests = getRuntimeImportRequests(node);
|
|
13610
14480
|
if (importRequests.length === 0) return;
|
|
@@ -13739,7 +14609,7 @@ const noChainStateUpdates = defineRule({
|
|
|
13739
14609
|
});
|
|
13740
14610
|
//#endregion
|
|
13741
14611
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
13742
|
-
const MESSAGE$
|
|
14612
|
+
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
14613
|
const noChildrenProp = defineRule({
|
|
13744
14614
|
id: "no-children-prop",
|
|
13745
14615
|
severity: "warn",
|
|
@@ -13750,7 +14620,7 @@ const noChildrenProp = defineRule({
|
|
|
13750
14620
|
if (node.name.name !== "children") return;
|
|
13751
14621
|
context.report({
|
|
13752
14622
|
node: node.name,
|
|
13753
|
-
message: MESSAGE$
|
|
14623
|
+
message: MESSAGE$23
|
|
13754
14624
|
});
|
|
13755
14625
|
},
|
|
13756
14626
|
CallExpression(node) {
|
|
@@ -13763,90 +14633,15 @@ const noChildrenProp = defineRule({
|
|
|
13763
14633
|
const propertyKey = property.key;
|
|
13764
14634
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
13765
14635
|
node: propertyKey,
|
|
13766
|
-
message: MESSAGE$
|
|
14636
|
+
message: MESSAGE$23
|
|
13767
14637
|
});
|
|
13768
14638
|
}
|
|
13769
14639
|
}
|
|
13770
14640
|
})
|
|
13771
14641
|
});
|
|
13772
14642
|
//#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
14643
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
13849
|
-
const MESSAGE$
|
|
14644
|
+
const MESSAGE$22 = "`React.cloneElement` is uncommon and leads to fragile components.";
|
|
13850
14645
|
const noCloneElement = defineRule({
|
|
13851
14646
|
id: "no-clone-element",
|
|
13852
14647
|
severity: "warn",
|
|
@@ -13858,7 +14653,7 @@ const noCloneElement = defineRule({
|
|
|
13858
14653
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
13859
14654
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
13860
14655
|
node: callee,
|
|
13861
|
-
message: MESSAGE$
|
|
14656
|
+
message: MESSAGE$22
|
|
13862
14657
|
});
|
|
13863
14658
|
return;
|
|
13864
14659
|
}
|
|
@@ -13871,14 +14666,14 @@ const noCloneElement = defineRule({
|
|
|
13871
14666
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
13872
14667
|
context.report({
|
|
13873
14668
|
node: callee,
|
|
13874
|
-
message: MESSAGE$
|
|
14669
|
+
message: MESSAGE$22
|
|
13875
14670
|
});
|
|
13876
14671
|
}
|
|
13877
14672
|
} })
|
|
13878
14673
|
});
|
|
13879
14674
|
//#endregion
|
|
13880
14675
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
13881
|
-
const MESSAGE$
|
|
14676
|
+
const MESSAGE$21 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
|
|
13882
14677
|
const noDanger = defineRule({
|
|
13883
14678
|
id: "no-danger",
|
|
13884
14679
|
severity: "warn",
|
|
@@ -13889,7 +14684,7 @@ const noDanger = defineRule({
|
|
|
13889
14684
|
if (!propAttribute) return;
|
|
13890
14685
|
context.report({
|
|
13891
14686
|
node: propAttribute.name,
|
|
13892
|
-
message: MESSAGE$
|
|
14687
|
+
message: MESSAGE$21
|
|
13893
14688
|
});
|
|
13894
14689
|
},
|
|
13895
14690
|
CallExpression(node) {
|
|
@@ -13901,7 +14696,7 @@ const noDanger = defineRule({
|
|
|
13901
14696
|
const propertyKey = property.key;
|
|
13902
14697
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
13903
14698
|
node: propertyKey,
|
|
13904
|
-
message: MESSAGE$
|
|
14699
|
+
message: MESSAGE$21
|
|
13905
14700
|
});
|
|
13906
14701
|
}
|
|
13907
14702
|
}
|
|
@@ -13909,7 +14704,7 @@ const noDanger = defineRule({
|
|
|
13909
14704
|
});
|
|
13910
14705
|
//#endregion
|
|
13911
14706
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
13912
|
-
const MESSAGE$
|
|
14707
|
+
const MESSAGE$20 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
|
|
13913
14708
|
const isLineBreak = (child) => {
|
|
13914
14709
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
13915
14710
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -13978,7 +14773,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13978
14773
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
13979
14774
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
13980
14775
|
node: opening,
|
|
13981
|
-
message: MESSAGE$
|
|
14776
|
+
message: MESSAGE$20
|
|
13982
14777
|
});
|
|
13983
14778
|
},
|
|
13984
14779
|
CallExpression(node) {
|
|
@@ -13990,7 +14785,7 @@ const noDangerWithChildren = defineRule({
|
|
|
13990
14785
|
if (!propsShape.hasDangerously) return;
|
|
13991
14786
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
13992
14787
|
node,
|
|
13993
|
-
message: MESSAGE$
|
|
14788
|
+
message: MESSAGE$20
|
|
13994
14789
|
});
|
|
13995
14790
|
}
|
|
13996
14791
|
})
|
|
@@ -14365,7 +15160,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
14365
15160
|
};
|
|
14366
15161
|
const getInlineFunctionNode = (node) => {
|
|
14367
15162
|
if (!node) return null;
|
|
14368
|
-
if (isFunctionLike(node)) return node;
|
|
15163
|
+
if (isFunctionLike$1(node)) return node;
|
|
14369
15164
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
14370
15165
|
for (const argument of node.arguments ?? []) {
|
|
14371
15166
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -14376,7 +15171,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
14376
15171
|
const getNearestComponentFunction = (node) => {
|
|
14377
15172
|
let cursor = node.parent ?? null;
|
|
14378
15173
|
while (cursor) {
|
|
14379
|
-
if (isFunctionLike(cursor)) return cursor;
|
|
15174
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
14380
15175
|
cursor = cursor.parent ?? null;
|
|
14381
15176
|
}
|
|
14382
15177
|
return null;
|
|
@@ -14557,7 +15352,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
14557
15352
|
//#endregion
|
|
14558
15353
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
14559
15354
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
14560
|
-
const MESSAGE$
|
|
15355
|
+
const MESSAGE$19 = "Do not use `this.setState` in `componentDidMount`.";
|
|
14561
15356
|
const resolveSettings$20 = (settings) => {
|
|
14562
15357
|
const reactDoctor = settings?.["react-doctor"];
|
|
14563
15358
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14575,7 +15370,7 @@ const noDidMountSetState = defineRule({
|
|
|
14575
15370
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14576
15371
|
context.report({
|
|
14577
15372
|
node: node.callee,
|
|
14578
|
-
message: MESSAGE$
|
|
15373
|
+
message: MESSAGE$19
|
|
14579
15374
|
});
|
|
14580
15375
|
} };
|
|
14581
15376
|
}
|
|
@@ -14583,7 +15378,7 @@ const noDidMountSetState = defineRule({
|
|
|
14583
15378
|
//#endregion
|
|
14584
15379
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
14585
15380
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
14586
|
-
const MESSAGE$
|
|
15381
|
+
const MESSAGE$18 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
|
|
14587
15382
|
const resolveSettings$19 = (settings) => {
|
|
14588
15383
|
const reactDoctor = settings?.["react-doctor"];
|
|
14589
15384
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -14601,7 +15396,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
14601
15396
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
14602
15397
|
context.report({
|
|
14603
15398
|
node: node.callee,
|
|
14604
|
-
message: MESSAGE$
|
|
15399
|
+
message: MESSAGE$18
|
|
14605
15400
|
});
|
|
14606
15401
|
} };
|
|
14607
15402
|
}
|
|
@@ -14624,7 +15419,7 @@ const isStateMemberExpression = (node) => {
|
|
|
14624
15419
|
};
|
|
14625
15420
|
//#endregion
|
|
14626
15421
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
14627
|
-
const MESSAGE$
|
|
15422
|
+
const MESSAGE$17 = "Never mutate `this.state` directly.";
|
|
14628
15423
|
const shouldIgnoreMutation = (node) => {
|
|
14629
15424
|
let isConstructor = false;
|
|
14630
15425
|
let isInsideCallExpression = false;
|
|
@@ -14646,7 +15441,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
14646
15441
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
14647
15442
|
context.report({
|
|
14648
15443
|
node: reportNode,
|
|
14649
|
-
message: MESSAGE$
|
|
15444
|
+
message: MESSAGE$17
|
|
14650
15445
|
});
|
|
14651
15446
|
};
|
|
14652
15447
|
const noDirectMutationState = defineRule({
|
|
@@ -14702,7 +15497,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
14702
15497
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
14703
15498
|
if (!node || typeof node !== "object") return;
|
|
14704
15499
|
let nextShadowedStateNames = shadowedStateNames;
|
|
14705
|
-
if (isFunctionLike(node)) {
|
|
15500
|
+
if (isFunctionLike$1(node)) {
|
|
14706
15501
|
const localBindings = collectFunctionLocalBindings(node);
|
|
14707
15502
|
if (localBindings.size > 0) {
|
|
14708
15503
|
const merged = new Set(shadowedStateNames);
|
|
@@ -14809,7 +15604,7 @@ const noDisabledZoom = defineRule({
|
|
|
14809
15604
|
});
|
|
14810
15605
|
//#endregion
|
|
14811
15606
|
//#region src/plugin/rules/a11y/no-distracting-elements.ts
|
|
14812
|
-
const buildMessage$
|
|
15607
|
+
const buildMessage$14 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
|
|
14813
15608
|
const DEFAULT_DISTRACTING = ["marquee", "blink"];
|
|
14814
15609
|
const resolveSettings$18 = (settings) => {
|
|
14815
15610
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -14829,7 +15624,7 @@ const noDistractingElements = defineRule({
|
|
|
14829
15624
|
const tag = getElementType(node, context.settings);
|
|
14830
15625
|
if (distractingTags.has(tag)) context.report({
|
|
14831
15626
|
node: node.name,
|
|
14832
|
-
message: buildMessage$
|
|
15627
|
+
message: buildMessage$14(tag)
|
|
14833
15628
|
});
|
|
14834
15629
|
} };
|
|
14835
15630
|
}
|
|
@@ -16159,7 +16954,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
16159
16954
|
"ReactDOM",
|
|
16160
16955
|
"ReactDom"
|
|
16161
16956
|
]);
|
|
16162
|
-
const MESSAGE$
|
|
16957
|
+
const MESSAGE$16 = "Unexpected call to `findDOMNode` — removed in React 19.";
|
|
16163
16958
|
const noFindDomNode = defineRule({
|
|
16164
16959
|
id: "no-find-dom-node",
|
|
16165
16960
|
severity: "warn",
|
|
@@ -16169,7 +16964,7 @@ const noFindDomNode = defineRule({
|
|
|
16169
16964
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
16170
16965
|
context.report({
|
|
16171
16966
|
node: callee,
|
|
16172
|
-
message: MESSAGE$
|
|
16967
|
+
message: MESSAGE$16
|
|
16173
16968
|
});
|
|
16174
16969
|
return;
|
|
16175
16970
|
}
|
|
@@ -16180,7 +16975,7 @@ const noFindDomNode = defineRule({
|
|
|
16180
16975
|
if (callee.property.name !== "findDOMNode") return;
|
|
16181
16976
|
context.report({
|
|
16182
16977
|
node: callee.property,
|
|
16183
|
-
message: MESSAGE$
|
|
16978
|
+
message: MESSAGE$16
|
|
16184
16979
|
});
|
|
16185
16980
|
}
|
|
16186
16981
|
} })
|
|
@@ -16197,7 +16992,7 @@ const noFlushSync = defineRule({
|
|
|
16197
16992
|
if (node.source?.value !== "react-dom") return;
|
|
16198
16993
|
for (const specifier of node.specifiers ?? []) {
|
|
16199
16994
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
16200
|
-
if (getImportedName(specifier) === "flushSync") context.report({
|
|
16995
|
+
if (getImportedName$1(specifier) === "flushSync") context.report({
|
|
16201
16996
|
node: specifier,
|
|
16202
16997
|
message: "flushSync from react-dom skips View Transition snapshots and concurrent rendering — prefer startTransition for non-urgent updates"
|
|
16203
16998
|
});
|
|
@@ -16239,6 +17034,64 @@ const noGenericHandlerNames = defineRule({
|
|
|
16239
17034
|
} })
|
|
16240
17035
|
});
|
|
16241
17036
|
//#endregion
|
|
17037
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
17038
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
17039
|
+
"FunctionDeclaration",
|
|
17040
|
+
"FunctionExpression",
|
|
17041
|
+
"ArrowFunctionExpression",
|
|
17042
|
+
"ClassDeclaration",
|
|
17043
|
+
"ClassExpression"
|
|
17044
|
+
]);
|
|
17045
|
+
const isReactImport$1 = (symbol) => {
|
|
17046
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
17047
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
17048
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
17049
|
+
return importDeclaration.source.value === "react";
|
|
17050
|
+
};
|
|
17051
|
+
const getImportedName = (symbol) => {
|
|
17052
|
+
if (symbol.kind !== "import") return null;
|
|
17053
|
+
if (!isReactImport$1(symbol)) return null;
|
|
17054
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
17055
|
+
};
|
|
17056
|
+
const isReactNamespaceImport = (symbol) => {
|
|
17057
|
+
if (symbol.kind !== "import") return false;
|
|
17058
|
+
if (!isReactImport$1(symbol)) return false;
|
|
17059
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
17060
|
+
};
|
|
17061
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
17062
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
17063
|
+
const symbol = scopes.symbolFor(callee);
|
|
17064
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
17065
|
+
};
|
|
17066
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
17067
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
17068
|
+
if (callee.computed) return false;
|
|
17069
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
17070
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
17071
|
+
if (callee.property.name !== "createElement") return false;
|
|
17072
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
17073
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
17074
|
+
};
|
|
17075
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
17076
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
17077
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
17078
|
+
};
|
|
17079
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
17080
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
17081
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
17082
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
17083
|
+
const nodeRecord = node;
|
|
17084
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
17085
|
+
if (key === "parent") continue;
|
|
17086
|
+
const child = nodeRecord[key];
|
|
17087
|
+
if (Array.isArray(child)) {
|
|
17088
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
17089
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
17090
|
+
}
|
|
17091
|
+
return false;
|
|
17092
|
+
};
|
|
17093
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
17094
|
+
//#endregion
|
|
16242
17095
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
16243
17096
|
const noGiantComponent = defineRule({
|
|
16244
17097
|
id: "no-giant-component",
|
|
@@ -16246,10 +17099,13 @@ const noGiantComponent = defineRule({
|
|
|
16246
17099
|
tags: ["test-noise", "react-jsx-only"],
|
|
16247
17100
|
recommendation: "Extract logical sections into focused components: `<UserHeader />`, `<UserActions />`, etc.",
|
|
16248
17101
|
create: (context) => {
|
|
16249
|
-
const
|
|
16250
|
-
if (!bodyNode.loc) return;
|
|
17102
|
+
const getOversizedComponentLineCount = (bodyNode) => {
|
|
17103
|
+
if (!bodyNode.loc) return null;
|
|
16251
17104
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
16252
|
-
|
|
17105
|
+
return lineCount > 300 ? lineCount : null;
|
|
17106
|
+
};
|
|
17107
|
+
const reportOversizedComponent = (nameNode, componentName, lineCount) => {
|
|
17108
|
+
context.report({
|
|
16253
17109
|
node: nameNode,
|
|
16254
17110
|
message: `Component "${componentName}" is ${lineCount} lines — consider breaking it into smaller focused components`
|
|
16255
17111
|
});
|
|
@@ -16257,12 +17113,18 @@ const noGiantComponent = defineRule({
|
|
|
16257
17113
|
return {
|
|
16258
17114
|
FunctionDeclaration(node) {
|
|
16259
17115
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
16260
|
-
|
|
17116
|
+
const lineCount = getOversizedComponentLineCount(node);
|
|
17117
|
+
if (lineCount === null) return;
|
|
17118
|
+
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
17119
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16261
17120
|
},
|
|
16262
17121
|
VariableDeclarator(node) {
|
|
16263
17122
|
if (!isComponentAssignment(node)) return;
|
|
16264
17123
|
if (!isNodeOfType(node.id, "Identifier") || !node.init) return;
|
|
16265
|
-
|
|
17124
|
+
const lineCount = getOversizedComponentLineCount(node.init);
|
|
17125
|
+
if (lineCount === null) return;
|
|
17126
|
+
if (!functionContainsReactRenderOutput(node.init, context.scopes)) return;
|
|
17127
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
16266
17128
|
}
|
|
16267
17129
|
};
|
|
16268
17130
|
}
|
|
@@ -16603,7 +17465,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
16603
17465
|
});
|
|
16604
17466
|
//#endregion
|
|
16605
17467
|
//#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
|
|
16606
|
-
const buildMessage$
|
|
17468
|
+
const buildMessage$13 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
|
|
16607
17469
|
const PRESENTATION_ROLES = ["presentation", "none"];
|
|
16608
17470
|
const DEFAULT_ALLOWED_ROLES$1 = {
|
|
16609
17471
|
tr: ["none", "presentation"],
|
|
@@ -16647,7 +17509,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
|
|
|
16647
17509
|
if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
|
|
16648
17510
|
context.report({
|
|
16649
17511
|
node: roleAttribute,
|
|
16650
|
-
message: buildMessage$
|
|
17512
|
+
message: buildMessage$13(elementType, firstRole)
|
|
16651
17513
|
});
|
|
16652
17514
|
} };
|
|
16653
17515
|
}
|
|
@@ -16848,7 +17710,7 @@ const isInsideClassBody = (node) => {
|
|
|
16848
17710
|
let current = node.parent;
|
|
16849
17711
|
while (current) {
|
|
16850
17712
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
16851
|
-
if (isFunctionLike(current)) return false;
|
|
17713
|
+
if (isFunctionLike$1(current)) return false;
|
|
16852
17714
|
current = current.parent;
|
|
16853
17715
|
}
|
|
16854
17716
|
return false;
|
|
@@ -17091,7 +17953,7 @@ const noMoment = defineRule({
|
|
|
17091
17953
|
});
|
|
17092
17954
|
//#endregion
|
|
17093
17955
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
17094
|
-
const buildMessage$
|
|
17956
|
+
const buildMessage$12 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
|
|
17095
17957
|
const resolveSettings$16 = (settings) => {
|
|
17096
17958
|
const reactDoctor = settings?.["react-doctor"];
|
|
17097
17959
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -17391,7 +18253,7 @@ const noMultiComp = defineRule({
|
|
|
17391
18253
|
category: "Architecture",
|
|
17392
18254
|
create: (context) => {
|
|
17393
18255
|
const settings = resolveSettings$16(context.settings);
|
|
17394
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
18256
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
17395
18257
|
return { Program(node) {
|
|
17396
18258
|
if (isTestlikeFile) return;
|
|
17397
18259
|
const visitContext = {
|
|
@@ -17412,7 +18274,7 @@ const noMultiComp = defineRule({
|
|
|
17412
18274
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
17413
18275
|
for (const component of flagged.slice(1)) context.report({
|
|
17414
18276
|
node: component.reportNode,
|
|
17415
|
-
message: buildMessage$
|
|
18277
|
+
message: buildMessage$12(component.name)
|
|
17416
18278
|
});
|
|
17417
18279
|
} };
|
|
17418
18280
|
}
|
|
@@ -17490,7 +18352,7 @@ const noMutableInDeps = defineRule({
|
|
|
17490
18352
|
});
|
|
17491
18353
|
//#endregion
|
|
17492
18354
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
17493
|
-
const MESSAGE$
|
|
18355
|
+
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
18356
|
const MUTATING_ARRAY_METHODS = new Set([
|
|
17495
18357
|
"copyWithin",
|
|
17496
18358
|
"fill",
|
|
@@ -17687,7 +18549,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
17687
18549
|
reportedNodes.add(mutation.node);
|
|
17688
18550
|
context.report({
|
|
17689
18551
|
node: mutation.node,
|
|
17690
|
-
message: MESSAGE$
|
|
18552
|
+
message: MESSAGE$15
|
|
17691
18553
|
});
|
|
17692
18554
|
}
|
|
17693
18555
|
};
|
|
@@ -17773,7 +18635,7 @@ const noMutatingReducerState = defineRule({
|
|
|
17773
18635
|
});
|
|
17774
18636
|
//#endregion
|
|
17775
18637
|
//#region src/plugin/rules/react-builtins/no-namespace.ts
|
|
17776
|
-
const buildMessage$
|
|
18638
|
+
const buildMessage$11 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
|
|
17777
18639
|
const noNamespace = defineRule({
|
|
17778
18640
|
id: "no-namespace",
|
|
17779
18641
|
severity: "warn",
|
|
@@ -17785,7 +18647,7 @@ const noNamespace = defineRule({
|
|
|
17785
18647
|
const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
|
|
17786
18648
|
context.report({
|
|
17787
18649
|
node: namespaced,
|
|
17788
|
-
message: buildMessage$
|
|
18650
|
+
message: buildMessage$11(fullName)
|
|
17789
18651
|
});
|
|
17790
18652
|
},
|
|
17791
18653
|
CallExpression(node) {
|
|
@@ -17796,7 +18658,7 @@ const noNamespace = defineRule({
|
|
|
17796
18658
|
if (!firstArgument.value.includes(":")) return;
|
|
17797
18659
|
context.report({
|
|
17798
18660
|
node: firstArgument,
|
|
17799
|
-
message: buildMessage$
|
|
18661
|
+
message: buildMessage$11(firstArgument.value)
|
|
17800
18662
|
});
|
|
17801
18663
|
}
|
|
17802
18664
|
})
|
|
@@ -17840,7 +18702,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
17840
18702
|
});
|
|
17841
18703
|
//#endregion
|
|
17842
18704
|
//#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
|
|
17843
|
-
const buildMessage$
|
|
18705
|
+
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
18706
|
const INTERACTIVE_HANDLERS = [
|
|
17845
18707
|
"onClick",
|
|
17846
18708
|
"onMouseDown",
|
|
@@ -17866,13 +18728,13 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
17866
18728
|
}
|
|
17867
18729
|
context.report({
|
|
17868
18730
|
node: node.name,
|
|
17869
|
-
message: buildMessage$
|
|
18731
|
+
message: buildMessage$10(tag)
|
|
17870
18732
|
});
|
|
17871
18733
|
} })
|
|
17872
18734
|
});
|
|
17873
18735
|
//#endregion
|
|
17874
18736
|
//#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
|
|
17875
|
-
const buildMessage$
|
|
18737
|
+
const buildMessage$9 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
|
|
17876
18738
|
const DEFAULT_ALLOWED_ROLES = {
|
|
17877
18739
|
ul: [
|
|
17878
18740
|
"menu",
|
|
@@ -17936,14 +18798,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
17936
18798
|
if (!isInteractiveRole(firstRole)) return;
|
|
17937
18799
|
context.report({
|
|
17938
18800
|
node: roleAttribute,
|
|
17939
|
-
message: buildMessage$
|
|
18801
|
+
message: buildMessage$9(elementType, firstRole)
|
|
17940
18802
|
});
|
|
17941
18803
|
} };
|
|
17942
18804
|
}
|
|
17943
18805
|
});
|
|
17944
18806
|
//#endregion
|
|
17945
18807
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
17946
|
-
const MESSAGE$
|
|
18808
|
+
const MESSAGE$14 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
|
|
17947
18809
|
const resolveSettings$14 = (settings) => {
|
|
17948
18810
|
const reactDoctor = settings?.["react-doctor"];
|
|
17949
18811
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -17970,7 +18832,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17970
18832
|
if (numeric === null) {
|
|
17971
18833
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
17972
18834
|
node: tabIndex,
|
|
17973
|
-
message: MESSAGE$
|
|
18835
|
+
message: MESSAGE$14
|
|
17974
18836
|
});
|
|
17975
18837
|
return;
|
|
17976
18838
|
}
|
|
@@ -17983,7 +18845,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17983
18845
|
if (!roleAttribute) {
|
|
17984
18846
|
context.report({
|
|
17985
18847
|
node: tabIndex,
|
|
17986
|
-
message: MESSAGE$
|
|
18848
|
+
message: MESSAGE$14
|
|
17987
18849
|
});
|
|
17988
18850
|
return;
|
|
17989
18851
|
}
|
|
@@ -17997,7 +18859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
17997
18859
|
}
|
|
17998
18860
|
context.report({
|
|
17999
18861
|
node: tabIndex,
|
|
18000
|
-
message: MESSAGE$
|
|
18862
|
+
message: MESSAGE$14
|
|
18001
18863
|
});
|
|
18002
18864
|
} };
|
|
18003
18865
|
}
|
|
@@ -18558,7 +19420,7 @@ const noPureBlackBackground = defineRule({
|
|
|
18558
19420
|
});
|
|
18559
19421
|
//#endregion
|
|
18560
19422
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
18561
|
-
const MESSAGE$
|
|
19423
|
+
const MESSAGE$13 = "`React.Children` is uncommon and leads to fragile components.";
|
|
18562
19424
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
18563
19425
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
18564
19426
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -18583,13 +19445,13 @@ const noReactChildren = defineRule({
|
|
|
18583
19445
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
18584
19446
|
context.report({
|
|
18585
19447
|
node: calleeOuter,
|
|
18586
|
-
message: MESSAGE$
|
|
19448
|
+
message: MESSAGE$13
|
|
18587
19449
|
});
|
|
18588
19450
|
return;
|
|
18589
19451
|
}
|
|
18590
19452
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
18591
19453
|
node: calleeOuter,
|
|
18592
|
-
message: MESSAGE$
|
|
19454
|
+
message: MESSAGE$13
|
|
18593
19455
|
});
|
|
18594
19456
|
} })
|
|
18595
19457
|
});
|
|
@@ -18605,7 +19467,7 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
18605
19467
|
if (sourceValue !== source) return;
|
|
18606
19468
|
for (const specifier of node.specifiers ?? []) {
|
|
18607
19469
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18608
|
-
const importedName = getImportedName(specifier);
|
|
19470
|
+
const importedName = getImportedName$1(specifier);
|
|
18609
19471
|
if (!importedName) continue;
|
|
18610
19472
|
const message = messages.get(importedName);
|
|
18611
19473
|
if (message) context.report({
|
|
@@ -18657,7 +19519,7 @@ const buildTestUtilsMessage = (importedName) => {
|
|
|
18657
19519
|
const reportTestUtilsImports = (node, context) => {
|
|
18658
19520
|
for (const specifier of node.specifiers ?? []) {
|
|
18659
19521
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
18660
|
-
const importedName = getImportedName(specifier) ?? "default";
|
|
19522
|
+
const importedName = getImportedName$1(specifier) ?? "default";
|
|
18661
19523
|
context.report({
|
|
18662
19524
|
node: specifier,
|
|
18663
19525
|
message: buildTestUtilsMessage(importedName)
|
|
@@ -18785,7 +19647,7 @@ const getTagsForRole = (role) => {
|
|
|
18785
19647
|
};
|
|
18786
19648
|
//#endregion
|
|
18787
19649
|
//#region src/plugin/rules/a11y/no-redundant-roles.ts
|
|
18788
|
-
const buildMessage$
|
|
19650
|
+
const buildMessage$8 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
|
|
18789
19651
|
const resolveSettings$13 = (settings) => {
|
|
18790
19652
|
const reactDoctor = settings?.["react-doctor"];
|
|
18791
19653
|
return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
|
|
@@ -18808,14 +19670,14 @@ const noRedundantRoles = defineRule({
|
|
|
18808
19670
|
const allowedHere = settings.exceptions[tag] ?? [];
|
|
18809
19671
|
if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
|
|
18810
19672
|
node: roleAttr,
|
|
18811
|
-
message: buildMessage$
|
|
19673
|
+
message: buildMessage$8(tag, role)
|
|
18812
19674
|
});
|
|
18813
19675
|
} };
|
|
18814
19676
|
}
|
|
18815
19677
|
});
|
|
18816
19678
|
//#endregion
|
|
18817
19679
|
//#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
|
|
18818
|
-
const buildMessage$
|
|
19680
|
+
const buildMessage$7 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
|
|
18819
19681
|
const isPureComponentSuper = (superClass) => {
|
|
18820
19682
|
if (!superClass) return false;
|
|
18821
19683
|
if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
|
|
@@ -18847,7 +19709,7 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
18847
19709
|
const className = classNode.id?.name ?? "<anonymous class>";
|
|
18848
19710
|
context.report({
|
|
18849
19711
|
node: reportNode,
|
|
18850
|
-
message: buildMessage$
|
|
19712
|
+
message: buildMessage$7(className)
|
|
18851
19713
|
});
|
|
18852
19714
|
};
|
|
18853
19715
|
return {
|
|
@@ -18906,7 +19768,7 @@ const noRenderPropChildren = defineRule({
|
|
|
18906
19768
|
});
|
|
18907
19769
|
//#endregion
|
|
18908
19770
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
18909
|
-
const MESSAGE$
|
|
19771
|
+
const MESSAGE$12 = "Do not use the return value from `ReactDOM.render`.";
|
|
18910
19772
|
const isReactDomRenderCall = (node) => {
|
|
18911
19773
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
18912
19774
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -18929,7 +19791,7 @@ const noRenderReturnValue = defineRule({
|
|
|
18929
19791
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
18930
19792
|
context.report({
|
|
18931
19793
|
node: node.callee,
|
|
18932
|
-
message: MESSAGE$
|
|
19794
|
+
message: MESSAGE$12
|
|
18933
19795
|
});
|
|
18934
19796
|
} })
|
|
18935
19797
|
});
|
|
@@ -19324,7 +20186,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
19324
20186
|
const isInsideServerOnlyScope = (node) => {
|
|
19325
20187
|
let currentNode = node.parent ?? null;
|
|
19326
20188
|
while (currentNode) {
|
|
19327
|
-
if (isFunctionLike(currentNode)) {
|
|
20189
|
+
if (isFunctionLike$1(currentNode)) {
|
|
19328
20190
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
19329
20191
|
}
|
|
19330
20192
|
currentNode = currentNode.parent ?? null;
|
|
@@ -19338,7 +20200,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
19338
20200
|
severity: "warn",
|
|
19339
20201
|
recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
|
|
19340
20202
|
create: (context) => {
|
|
19341
|
-
const filename = normalizeFilename$1(context.
|
|
20203
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
19342
20204
|
const framework = getReactDoctorStringSetting(context.settings, "framework");
|
|
19343
20205
|
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
19344
20206
|
let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
|
|
@@ -19389,7 +20251,7 @@ const getParentComponent = (node) => {
|
|
|
19389
20251
|
};
|
|
19390
20252
|
//#endregion
|
|
19391
20253
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
19392
|
-
const MESSAGE$
|
|
20254
|
+
const MESSAGE$11 = "Do not use `this.setState` in components.";
|
|
19393
20255
|
const noSetState = defineRule({
|
|
19394
20256
|
id: "no-set-state",
|
|
19395
20257
|
severity: "warn",
|
|
@@ -19403,7 +20265,7 @@ const noSetState = defineRule({
|
|
|
19403
20265
|
if (!getParentComponent(node)) return;
|
|
19404
20266
|
context.report({
|
|
19405
20267
|
node: node.callee,
|
|
19406
|
-
message: MESSAGE$
|
|
20268
|
+
message: MESSAGE$11
|
|
19407
20269
|
});
|
|
19408
20270
|
} })
|
|
19409
20271
|
});
|
|
@@ -19562,7 +20424,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
19562
20424
|
};
|
|
19563
20425
|
//#endregion
|
|
19564
20426
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
19565
|
-
const MESSAGE$
|
|
20427
|
+
const MESSAGE$10 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
|
|
19566
20428
|
const DEFAULT_HANDLERS = [
|
|
19567
20429
|
"onClick",
|
|
19568
20430
|
"onMouseDown",
|
|
@@ -19593,7 +20455,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
19593
20455
|
category: "Accessibility",
|
|
19594
20456
|
create: (context) => {
|
|
19595
20457
|
const settings = resolveSettings$12(context.settings);
|
|
19596
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20458
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
19597
20459
|
return { JSXOpeningElement(node) {
|
|
19598
20460
|
if (isTestlikeFile) return;
|
|
19599
20461
|
let hasNonBlockerHandler = false;
|
|
@@ -19621,7 +20483,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
19621
20483
|
if (!roleAttribute || !roleAttribute.value) {
|
|
19622
20484
|
context.report({
|
|
19623
20485
|
node: node.name,
|
|
19624
|
-
message: MESSAGE$
|
|
20486
|
+
message: MESSAGE$10
|
|
19625
20487
|
});
|
|
19626
20488
|
return;
|
|
19627
20489
|
}
|
|
@@ -19631,14 +20493,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
19631
20493
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
19632
20494
|
context.report({
|
|
19633
20495
|
node: node.name,
|
|
19634
|
-
message: MESSAGE$
|
|
20496
|
+
message: MESSAGE$10
|
|
19635
20497
|
});
|
|
19636
20498
|
return;
|
|
19637
20499
|
}
|
|
19638
20500
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
19639
20501
|
context.report({
|
|
19640
20502
|
node: node.name,
|
|
19641
|
-
message: MESSAGE$
|
|
20503
|
+
message: MESSAGE$10
|
|
19642
20504
|
});
|
|
19643
20505
|
} };
|
|
19644
20506
|
}
|
|
@@ -19670,7 +20532,7 @@ const noStringRefs = defineRule({
|
|
|
19670
20532
|
recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
|
|
19671
20533
|
create: (context) => {
|
|
19672
20534
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
19673
|
-
const isTestlikeFile = isTestlikeFilename(context.
|
|
20535
|
+
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
19674
20536
|
return {
|
|
19675
20537
|
JSXAttribute(node) {
|
|
19676
20538
|
if (isTestlikeFile) return;
|
|
@@ -19694,7 +20556,7 @@ const noStringRefs = defineRule({
|
|
|
19694
20556
|
});
|
|
19695
20557
|
//#endregion
|
|
19696
20558
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
19697
|
-
const MESSAGE$
|
|
20559
|
+
const MESSAGE$9 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
|
|
19698
20560
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
19699
20561
|
let ancestor = node.parent;
|
|
19700
20562
|
while (ancestor) {
|
|
@@ -19762,7 +20624,7 @@ const noThisInSfc = defineRule({
|
|
|
19762
20624
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
19763
20625
|
context.report({
|
|
19764
20626
|
node,
|
|
19765
|
-
message: MESSAGE$
|
|
20627
|
+
message: MESSAGE$9
|
|
19766
20628
|
});
|
|
19767
20629
|
} };
|
|
19768
20630
|
}
|
|
@@ -19944,7 +20806,7 @@ const ESCAPED_VERSIONS = {
|
|
|
19944
20806
|
">": "`>` / `>`",
|
|
19945
20807
|
"}": "`}` (or wrap the literal in `{'}'}`)"
|
|
19946
20808
|
};
|
|
19947
|
-
const buildMessage$
|
|
20809
|
+
const buildMessage$6 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
|
|
19948
20810
|
const noUnescapedEntities = defineRule({
|
|
19949
20811
|
id: "no-unescaped-entities",
|
|
19950
20812
|
severity: "warn",
|
|
@@ -19955,7 +20817,7 @@ const noUnescapedEntities = defineRule({
|
|
|
19955
20817
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
19956
20818
|
context.report({
|
|
19957
20819
|
node,
|
|
19958
|
-
message: buildMessage$
|
|
20820
|
+
message: buildMessage$6(character)
|
|
19959
20821
|
});
|
|
19960
20822
|
return;
|
|
19961
20823
|
}
|
|
@@ -20976,7 +21838,7 @@ const SAFER_REPLACEMENT = {
|
|
|
20976
21838
|
componentWillUpdate: "componentDidUpdate",
|
|
20977
21839
|
UNSAFE_componentWillUpdate: "componentDidUpdate"
|
|
20978
21840
|
};
|
|
20979
|
-
const buildMessage$
|
|
21841
|
+
const buildMessage$5 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
|
|
20980
21842
|
const resolveSettings$9 = (settings) => {
|
|
20981
21843
|
const reactDoctor = settings?.["react-doctor"];
|
|
20982
21844
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -21024,7 +21886,7 @@ const noUnsafe = defineRule({
|
|
|
21024
21886
|
if (!getParentComponent(node)) return;
|
|
21025
21887
|
context.report({
|
|
21026
21888
|
node: node.key,
|
|
21027
|
-
message: buildMessage$
|
|
21889
|
+
message: buildMessage$5(name)
|
|
21028
21890
|
});
|
|
21029
21891
|
},
|
|
21030
21892
|
Property(node) {
|
|
@@ -21035,7 +21897,7 @@ const noUnsafe = defineRule({
|
|
|
21035
21897
|
if (isEs5Component(ancestor)) {
|
|
21036
21898
|
context.report({
|
|
21037
21899
|
node: node.key,
|
|
21038
|
-
message: buildMessage$
|
|
21900
|
+
message: buildMessage$5(name)
|
|
21039
21901
|
});
|
|
21040
21902
|
return;
|
|
21041
21903
|
}
|
|
@@ -21047,7 +21909,7 @@ const noUnsafe = defineRule({
|
|
|
21047
21909
|
});
|
|
21048
21910
|
//#endregion
|
|
21049
21911
|
//#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
|
|
21050
|
-
const buildMessage$
|
|
21912
|
+
const buildMessage$4 = (parentName, isInProp, allowAsProps) => {
|
|
21051
21913
|
let message = "Don't define components inside another component";
|
|
21052
21914
|
if (parentName) message += ` (\`${parentName}\`)`;
|
|
21053
21915
|
message += " — extract it to module scope.";
|
|
@@ -21116,7 +21978,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
21116
21978
|
const findEnclosingComponent = (node) => {
|
|
21117
21979
|
let walker = node.parent;
|
|
21118
21980
|
while (walker) {
|
|
21119
|
-
if (isFunctionLike(walker)) {
|
|
21981
|
+
if (isFunctionLike$1(walker)) {
|
|
21120
21982
|
const componentName = inferFunctionLikeName(walker);
|
|
21121
21983
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
21122
21984
|
component: walker,
|
|
@@ -21282,7 +22144,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
21282
22144
|
if (!enclosing) return;
|
|
21283
22145
|
context.report({
|
|
21284
22146
|
node: reportNode,
|
|
21285
|
-
message: buildMessage$
|
|
22147
|
+
message: buildMessage$4(enclosing.name, propInfo !== null, settings.allowAsProps)
|
|
21286
22148
|
});
|
|
21287
22149
|
};
|
|
21288
22150
|
const checkFunctionLike = (node) => {
|
|
@@ -21414,7 +22276,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
21414
22276
|
//#endregion
|
|
21415
22277
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
21416
22278
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
21417
|
-
const MESSAGE$
|
|
22279
|
+
const MESSAGE$8 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
|
|
21418
22280
|
const resolveSettings$7 = (settings) => {
|
|
21419
22281
|
const reactDoctor = settings?.["react-doctor"];
|
|
21420
22282
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -21447,7 +22309,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
21447
22309
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
21448
22310
|
context.report({
|
|
21449
22311
|
node: node.callee,
|
|
21450
|
-
message: MESSAGE$
|
|
22312
|
+
message: MESSAGE$8
|
|
21451
22313
|
});
|
|
21452
22314
|
} };
|
|
21453
22315
|
}
|
|
@@ -21805,7 +22667,7 @@ const isFileNameAllowed = (filename, checkJS) => {
|
|
|
21805
22667
|
};
|
|
21806
22668
|
const onlyExportComponents = defineRule({
|
|
21807
22669
|
id: "only-export-components",
|
|
21808
|
-
severity: "
|
|
22670
|
+
severity: "warn",
|
|
21809
22671
|
recommendation: "Move non-component exports out of files that export components.",
|
|
21810
22672
|
category: "Architecture",
|
|
21811
22673
|
create: (context) => {
|
|
@@ -21816,7 +22678,7 @@ const onlyExportComponents = defineRule({
|
|
|
21816
22678
|
allowConstantExport: settings.allowConstantExport
|
|
21817
22679
|
};
|
|
21818
22680
|
return { Program(node) {
|
|
21819
|
-
if (!isFileNameAllowed(
|
|
22681
|
+
if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
|
|
21820
22682
|
const allNodes = collectAllNodes(node);
|
|
21821
22683
|
const exports = [];
|
|
21822
22684
|
let hasReactExport = false;
|
|
@@ -21985,6 +22847,214 @@ const onlyExportComponents = defineRule({
|
|
|
21985
22847
|
}
|
|
21986
22848
|
});
|
|
21987
22849
|
//#endregion
|
|
22850
|
+
//#region src/plugin/rules/preact/preact-no-children-length.ts
|
|
22851
|
+
const ARRAY_READ_METHOD_NAMES = new Set([
|
|
22852
|
+
"length",
|
|
22853
|
+
"map",
|
|
22854
|
+
"forEach",
|
|
22855
|
+
"filter",
|
|
22856
|
+
"find",
|
|
22857
|
+
"reduce",
|
|
22858
|
+
"some",
|
|
22859
|
+
"every",
|
|
22860
|
+
"flat",
|
|
22861
|
+
"flatMap",
|
|
22862
|
+
"indexOf",
|
|
22863
|
+
"includes",
|
|
22864
|
+
"slice",
|
|
22865
|
+
"concat",
|
|
22866
|
+
"join"
|
|
22867
|
+
]);
|
|
22868
|
+
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`.";
|
|
22869
|
+
const isDestructuredChildrenParam = (identifier) => {
|
|
22870
|
+
let cursor = identifier.parent;
|
|
22871
|
+
while (cursor) {
|
|
22872
|
+
if (isNodeOfType(cursor, "FunctionDeclaration") || isNodeOfType(cursor, "FunctionExpression") || isNodeOfType(cursor, "ArrowFunctionExpression")) {
|
|
22873
|
+
const firstParam = cursor.params[0];
|
|
22874
|
+
if (!firstParam || !isNodeOfType(firstParam, "ObjectPattern")) return false;
|
|
22875
|
+
return firstParam.properties.some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "children");
|
|
22876
|
+
}
|
|
22877
|
+
cursor = cursor.parent ?? null;
|
|
22878
|
+
}
|
|
22879
|
+
return false;
|
|
22880
|
+
};
|
|
22881
|
+
const isChildrenMemberExpression = (node) => {
|
|
22882
|
+
const object = node.object;
|
|
22883
|
+
if (!isNodeOfType(object, "MemberExpression")) return isNodeOfType(object, "Identifier") && object.name === "children" && isDestructuredChildrenParam(object);
|
|
22884
|
+
if (!isNodeOfType(object.property, "Identifier") || object.property.name !== "children") return false;
|
|
22885
|
+
const propsObject = object.object;
|
|
22886
|
+
if (isNodeOfType(propsObject, "Identifier") && propsObject.name === "props") return true;
|
|
22887
|
+
if (isNodeOfType(propsObject, "MemberExpression") && isNodeOfType(propsObject.property, "Identifier") && propsObject.property.name === "props" && isNodeOfType(propsObject.object, "ThisExpression")) return true;
|
|
22888
|
+
return false;
|
|
22889
|
+
};
|
|
22890
|
+
const preactNoChildrenLength = defineRule({
|
|
22891
|
+
id: "preact-no-children-length",
|
|
22892
|
+
requires: ["preact"],
|
|
22893
|
+
severity: "warn",
|
|
22894
|
+
recommendation: "Wrap with `toChildArray(children)` from `preact` before accessing array methods or `.length`.",
|
|
22895
|
+
create: (context) => ({ MemberExpression(node) {
|
|
22896
|
+
if (node.computed) return;
|
|
22897
|
+
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
22898
|
+
if (!ARRAY_READ_METHOD_NAMES.has(node.property.name)) return;
|
|
22899
|
+
if (!isChildrenMemberExpression(node)) return;
|
|
22900
|
+
context.report({
|
|
22901
|
+
node,
|
|
22902
|
+
message: CHILDREN_ARRAY_MESSAGE
|
|
22903
|
+
});
|
|
22904
|
+
} })
|
|
22905
|
+
});
|
|
22906
|
+
//#endregion
|
|
22907
|
+
//#region src/plugin/rules/preact/preact-no-react-hooks-import.ts
|
|
22908
|
+
const REACT_HOOK_NAMES = new Set([
|
|
22909
|
+
"useCallback",
|
|
22910
|
+
"useContext",
|
|
22911
|
+
"useDebugValue",
|
|
22912
|
+
"useDeferredValue",
|
|
22913
|
+
"useEffect",
|
|
22914
|
+
"useId",
|
|
22915
|
+
"useImperativeHandle",
|
|
22916
|
+
"useInsertionEffect",
|
|
22917
|
+
"useLayoutEffect",
|
|
22918
|
+
"useMemo",
|
|
22919
|
+
"useReducer",
|
|
22920
|
+
"useRef",
|
|
22921
|
+
"useState",
|
|
22922
|
+
"useSyncExternalStore",
|
|
22923
|
+
"useTransition"
|
|
22924
|
+
]);
|
|
22925
|
+
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.`;
|
|
22926
|
+
const preactNoReactHooksImport = defineRule({
|
|
22927
|
+
id: "preact-no-react-hooks-import",
|
|
22928
|
+
requires: ["pure-preact"],
|
|
22929
|
+
severity: "warn",
|
|
22930
|
+
recommendation: "Replace `from \"react\"` with `from \"preact/hooks\"` (or `from \"preact/compat\"` if other React API surface is needed).",
|
|
22931
|
+
create: (context) => ({ ImportDeclaration(node) {
|
|
22932
|
+
const source = node.source;
|
|
22933
|
+
if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
|
|
22934
|
+
const reactHookSpecifiers = [];
|
|
22935
|
+
for (const specifier of node.specifiers) {
|
|
22936
|
+
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
22937
|
+
const imported = specifier.imported;
|
|
22938
|
+
if (!isNodeOfType(imported, "Identifier")) continue;
|
|
22939
|
+
if (REACT_HOOK_NAMES.has(imported.name)) reactHookSpecifiers.push(specifier);
|
|
22940
|
+
}
|
|
22941
|
+
if (reactHookSpecifiers.length === 0) return;
|
|
22942
|
+
const importedNames = reactHookSpecifiers.map((specifier) => {
|
|
22943
|
+
const imported = specifier.imported;
|
|
22944
|
+
return isNodeOfType(imported, "Identifier") ? imported.name : "";
|
|
22945
|
+
});
|
|
22946
|
+
context.report({
|
|
22947
|
+
node,
|
|
22948
|
+
message: buildMessage$3(importedNames)
|
|
22949
|
+
});
|
|
22950
|
+
} })
|
|
22951
|
+
});
|
|
22952
|
+
//#endregion
|
|
22953
|
+
//#region src/plugin/rules/preact/preact-no-render-arguments.ts
|
|
22954
|
+
const PREACT_COMPONENT_NAMESPACES = new Set(["Preact"]);
|
|
22955
|
+
const PREACT_COMPONENT_NAMES = new Set(["Component", "PureComponent"]);
|
|
22956
|
+
const isPreactNamespaceComponentRef = (node) => {
|
|
22957
|
+
if (!isNodeOfType(node, "MemberExpression")) return false;
|
|
22958
|
+
if (!isNodeOfType(node.object, "Identifier")) return false;
|
|
22959
|
+
if (!PREACT_COMPONENT_NAMESPACES.has(node.object.name)) return false;
|
|
22960
|
+
if (!isNodeOfType(node.property, "Identifier")) return false;
|
|
22961
|
+
return PREACT_COMPONENT_NAMES.has(node.property.name);
|
|
22962
|
+
};
|
|
22963
|
+
const isPreactOrReactComponentClass = (node) => {
|
|
22964
|
+
if (isEs6Component(node)) return true;
|
|
22965
|
+
if (!isNodeOfType(node, "ClassDeclaration") && !isNodeOfType(node, "ClassExpression")) return false;
|
|
22966
|
+
const superClass = node.superClass;
|
|
22967
|
+
if (!superClass) return false;
|
|
22968
|
+
return isPreactNamespaceComponentRef(superClass);
|
|
22969
|
+
};
|
|
22970
|
+
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`.";
|
|
22971
|
+
const isInstanceMethodNamedRender = (node) => isNodeOfType(node, "MethodDefinition") && node.kind === "method" && node.static !== true && isNodeOfType(node.key, "Identifier") && node.key.name === "render";
|
|
22972
|
+
const isInsideEs6Component$1 = (methodDefinition) => {
|
|
22973
|
+
const classBody = methodDefinition.parent;
|
|
22974
|
+
if (!classBody || !isNodeOfType(classBody, "ClassBody")) return false;
|
|
22975
|
+
const owningClass = classBody.parent;
|
|
22976
|
+
if (!owningClass) return false;
|
|
22977
|
+
return isPreactOrReactComponentClass(owningClass);
|
|
22978
|
+
};
|
|
22979
|
+
const stripThisParameter = (params) => {
|
|
22980
|
+
const first = params[0];
|
|
22981
|
+
if (!first) return params;
|
|
22982
|
+
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
22983
|
+
return params;
|
|
22984
|
+
};
|
|
22985
|
+
const preactNoRenderArguments = defineRule({
|
|
22986
|
+
id: "preact-no-render-arguments",
|
|
22987
|
+
requires: ["preact"],
|
|
22988
|
+
severity: "warn",
|
|
22989
|
+
recommendation: "Read state/props from `this.props` / `this.state` inside `render()` instead of declaring positional parameters.",
|
|
22990
|
+
create: (context) => ({ MethodDefinition(node) {
|
|
22991
|
+
if (!isInstanceMethodNamedRender(node)) return;
|
|
22992
|
+
if (!isInsideEs6Component$1(node)) return;
|
|
22993
|
+
const renderFunction = node.value;
|
|
22994
|
+
if (!renderFunction || !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
22995
|
+
const firstParameter = stripThisParameter(renderFunction.params)[0];
|
|
22996
|
+
if (!firstParameter) return;
|
|
22997
|
+
context.report({
|
|
22998
|
+
node: firstParameter,
|
|
22999
|
+
message: RENDER_ARGUMENTS_MESSAGE
|
|
23000
|
+
});
|
|
23001
|
+
} })
|
|
23002
|
+
});
|
|
23003
|
+
//#endregion
|
|
23004
|
+
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
23005
|
+
const MESSAGE$7 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
|
|
23006
|
+
const preactPreferOndblclick = defineRule({
|
|
23007
|
+
id: "preact-prefer-ondblclick",
|
|
23008
|
+
requires: ["pure-preact"],
|
|
23009
|
+
severity: "warn",
|
|
23010
|
+
recommendation: "Rename the handler from `onDoubleClick` to `onDblClick` to match the DOM event name.",
|
|
23011
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23012
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23013
|
+
const tagName = node.name.name;
|
|
23014
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23015
|
+
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
23016
|
+
if (!onDoubleClickAttribute) return;
|
|
23017
|
+
context.report({
|
|
23018
|
+
node: onDoubleClickAttribute,
|
|
23019
|
+
message: MESSAGE$7
|
|
23020
|
+
});
|
|
23021
|
+
} })
|
|
23022
|
+
});
|
|
23023
|
+
//#endregion
|
|
23024
|
+
//#region src/plugin/rules/preact/preact-prefer-oninput.ts
|
|
23025
|
+
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.";
|
|
23026
|
+
const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
23027
|
+
"checkbox",
|
|
23028
|
+
"radio",
|
|
23029
|
+
"file"
|
|
23030
|
+
]);
|
|
23031
|
+
const isTextLikeInput = (openingElement) => {
|
|
23032
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
23033
|
+
const tagName = openingElement.name.name;
|
|
23034
|
+
if (tagName === "textarea") return true;
|
|
23035
|
+
if (tagName !== "input") return false;
|
|
23036
|
+
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
23037
|
+
if (!typeAttribute) return true;
|
|
23038
|
+
const typeValue = getJsxPropStringValue(typeAttribute);
|
|
23039
|
+
if (typeValue === null) return true;
|
|
23040
|
+
return !COMPAT_EXEMPT_INPUT_TYPES.has(typeValue);
|
|
23041
|
+
};
|
|
23042
|
+
const preactPreferOninput = defineRule({
|
|
23043
|
+
id: "preact-prefer-oninput",
|
|
23044
|
+
requires: ["pure-preact"],
|
|
23045
|
+
severity: "warn",
|
|
23046
|
+
recommendation: "Replace `onChange` with `onInput` on text-like inputs, or use `preact/compat` which remaps `onChange` automatically.",
|
|
23047
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23048
|
+
if (!isTextLikeInput(node)) return;
|
|
23049
|
+
const onChangeAttribute = findJsxAttribute(node.attributes, "onChange");
|
|
23050
|
+
if (!onChangeAttribute) return;
|
|
23051
|
+
context.report({
|
|
23052
|
+
node: onChangeAttribute,
|
|
23053
|
+
message: PREFER_ONINPUT_MESSAGE
|
|
23054
|
+
});
|
|
23055
|
+
} })
|
|
23056
|
+
});
|
|
23057
|
+
//#endregion
|
|
21988
23058
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
21989
23059
|
const preferDynamicImport = defineRule({
|
|
21990
23060
|
id: "prefer-dynamic-import",
|
|
@@ -22106,6 +23176,49 @@ const preferFunctionComponent = defineRule({
|
|
|
22106
23176
|
}
|
|
22107
23177
|
});
|
|
22108
23178
|
//#endregion
|
|
23179
|
+
//#region src/plugin/rules/a11y/prefer-html-dialog.ts
|
|
23180
|
+
const ROLE_DIALOG_VALUES = new Set(["dialog", "alertdialog"]);
|
|
23181
|
+
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.";
|
|
23182
|
+
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.";
|
|
23183
|
+
const isAriaModalTrue = (attribute) => {
|
|
23184
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
23185
|
+
if (stringValue !== null) return stringValue === "true";
|
|
23186
|
+
const value = attribute.value;
|
|
23187
|
+
if (!value) return true;
|
|
23188
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
23189
|
+
const expression = value.expression;
|
|
23190
|
+
if (isNodeOfType(expression, "Literal") && expression.value === true) return true;
|
|
23191
|
+
}
|
|
23192
|
+
return false;
|
|
23193
|
+
};
|
|
23194
|
+
const preferHtmlDialog = defineRule({
|
|
23195
|
+
id: "prefer-html-dialog",
|
|
23196
|
+
severity: "warn",
|
|
23197
|
+
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()`.",
|
|
23198
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23199
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23200
|
+
const tagName = node.name.name;
|
|
23201
|
+
if (tagName === "dialog") return;
|
|
23202
|
+
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
23203
|
+
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
23204
|
+
if (roleAttribute) {
|
|
23205
|
+
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
23206
|
+
if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
|
|
23207
|
+
context.report({
|
|
23208
|
+
node: roleAttribute,
|
|
23209
|
+
message: ROLE_DIALOG_MESSAGE
|
|
23210
|
+
});
|
|
23211
|
+
return;
|
|
23212
|
+
}
|
|
23213
|
+
}
|
|
23214
|
+
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
23215
|
+
if (ariaModalAttribute && isAriaModalTrue(ariaModalAttribute)) context.report({
|
|
23216
|
+
node: ariaModalAttribute,
|
|
23217
|
+
message: ARIA_MODAL_MESSAGE
|
|
23218
|
+
});
|
|
23219
|
+
} })
|
|
23220
|
+
});
|
|
23221
|
+
//#endregion
|
|
22109
23222
|
//#region src/plugin/rules/a11y/prefer-tag-over-role.ts
|
|
22110
23223
|
const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
|
|
22111
23224
|
const preferTagOverRole = defineRule({
|
|
@@ -22611,106 +23724,6 @@ const queryStableQueryClient = defineRule({
|
|
|
22611
23724
|
}
|
|
22612
23725
|
});
|
|
22613
23726
|
//#endregion
|
|
22614
|
-
//#region src/plugin/rules/architecture/react-compiler-destructure-method.ts
|
|
22615
|
-
const HOOK_OBJECTS_WITH_METHODS = new Map([
|
|
22616
|
-
["useRouter", new Set([
|
|
22617
|
-
"push",
|
|
22618
|
-
"replace",
|
|
22619
|
-
"back",
|
|
22620
|
-
"forward",
|
|
22621
|
-
"refresh",
|
|
22622
|
-
"prefetch"
|
|
22623
|
-
])],
|
|
22624
|
-
["useNavigation", new Set([
|
|
22625
|
-
"navigate",
|
|
22626
|
-
"push",
|
|
22627
|
-
"goBack",
|
|
22628
|
-
"popToTop",
|
|
22629
|
-
"reset",
|
|
22630
|
-
"replace",
|
|
22631
|
-
"dispatch"
|
|
22632
|
-
])],
|
|
22633
|
-
["useSearchParams", new Set([
|
|
22634
|
-
"get",
|
|
22635
|
-
"getAll",
|
|
22636
|
-
"has",
|
|
22637
|
-
"set"
|
|
22638
|
-
])]
|
|
22639
|
-
]);
|
|
22640
|
-
const HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING = new Map([["useNavigation", new Set(["@react-navigation/native", "@react-navigation/core"])]]);
|
|
22641
|
-
const isUnsafeMethodDestructureHookImport = (node, hookSource) => {
|
|
22642
|
-
const moduleSources = HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING.get(hookSource);
|
|
22643
|
-
if (!moduleSources) return false;
|
|
22644
|
-
for (const moduleSource of moduleSources) if (isImportedFromModule(node, hookSource, moduleSource)) return true;
|
|
22645
|
-
return false;
|
|
22646
|
-
};
|
|
22647
|
-
const buildHookBindingMap = (componentBody) => {
|
|
22648
|
-
const result = /* @__PURE__ */ new Map();
|
|
22649
|
-
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return result;
|
|
22650
|
-
for (const statement of componentBody.body ?? []) {
|
|
22651
|
-
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
22652
|
-
for (const declarator of statement.declarations ?? []) {
|
|
22653
|
-
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
22654
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
22655
|
-
const callee = declarator.init.callee;
|
|
22656
|
-
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
22657
|
-
result.set(declarator.id.name, callee.name);
|
|
22658
|
-
}
|
|
22659
|
-
}
|
|
22660
|
-
return result;
|
|
22661
|
-
};
|
|
22662
|
-
const reactCompilerDestructureMethod = defineRule({
|
|
22663
|
-
id: "react-compiler-destructure-method",
|
|
22664
|
-
tags: ["test-noise"],
|
|
22665
|
-
severity: "warn",
|
|
22666
|
-
recommendation: "Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoize",
|
|
22667
|
-
create: (context) => {
|
|
22668
|
-
const hookBindingMapStack = [];
|
|
22669
|
-
const isComponent = (node) => {
|
|
22670
|
-
if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id?.name && isUppercaseName(node.id.name));
|
|
22671
|
-
if (isNodeOfType(node, "VariableDeclarator")) return isComponentAssignment(node);
|
|
22672
|
-
return false;
|
|
22673
|
-
};
|
|
22674
|
-
const enter = (node) => {
|
|
22675
|
-
if (!isComponent(node)) return;
|
|
22676
|
-
let body;
|
|
22677
|
-
if (isNodeOfType(node, "FunctionDeclaration")) body = node.body;
|
|
22678
|
-
else if (isNodeOfType(node, "VariableDeclarator")) {
|
|
22679
|
-
const initializer = node.init;
|
|
22680
|
-
body = isInlineFunctionExpression(initializer) ? initializer.body : null;
|
|
22681
|
-
}
|
|
22682
|
-
hookBindingMapStack.push(buildHookBindingMap(body));
|
|
22683
|
-
};
|
|
22684
|
-
const exit = (node) => {
|
|
22685
|
-
if (isComponent(node)) hookBindingMapStack.pop();
|
|
22686
|
-
};
|
|
22687
|
-
return {
|
|
22688
|
-
FunctionDeclaration: enter,
|
|
22689
|
-
"FunctionDeclaration:exit": exit,
|
|
22690
|
-
VariableDeclarator: enter,
|
|
22691
|
-
"VariableDeclarator:exit": exit,
|
|
22692
|
-
MemberExpression(node) {
|
|
22693
|
-
if (hookBindingMapStack.length === 0) return;
|
|
22694
|
-
if (node.computed) return;
|
|
22695
|
-
if (!isNodeOfType(node.object, "Identifier")) return;
|
|
22696
|
-
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
22697
|
-
const bindingName = node.object.name;
|
|
22698
|
-
const methodName = node.property.name;
|
|
22699
|
-
const hookSource = hookBindingMapStack[hookBindingMapStack.length - 1].get(bindingName);
|
|
22700
|
-
if (!hookSource) return;
|
|
22701
|
-
const allowedMethods = HOOK_OBJECTS_WITH_METHODS.get(hookSource);
|
|
22702
|
-
if (!allowedMethods || !allowedMethods.has(methodName)) return;
|
|
22703
|
-
if (isUnsafeMethodDestructureHookImport(node, hookSource)) return;
|
|
22704
|
-
if (!isNodeOfType(node.parent, "CallExpression") || node.parent.callee !== node) return;
|
|
22705
|
-
context.report({
|
|
22706
|
-
node,
|
|
22707
|
-
message: `Destructure for clarity: \`const { ${methodName} } = ${hookSource}()\` then call \`${methodName}(...)\` directly — easier for React Compiler to memoize and clearer about which methods this component depends on`
|
|
22708
|
-
});
|
|
22709
|
-
}
|
|
22710
|
-
};
|
|
22711
|
-
}
|
|
22712
|
-
});
|
|
22713
|
-
//#endregion
|
|
22714
23727
|
//#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
|
|
22715
23728
|
const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
22716
23729
|
["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
|
|
@@ -23086,7 +24099,7 @@ const renderingSvgPrecision = defineRule({
|
|
|
23086
24099
|
category: "Performance",
|
|
23087
24100
|
recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
|
|
23088
24101
|
create: (context) => {
|
|
23089
|
-
const filename = context.
|
|
24102
|
+
const filename = context.filename;
|
|
23090
24103
|
const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
|
|
23091
24104
|
return { JSXAttribute(node) {
|
|
23092
24105
|
if (isAutoGenerated) return;
|
|
@@ -23118,7 +24131,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
23118
24131
|
let found = false;
|
|
23119
24132
|
walkAst(functionBody, (child) => {
|
|
23120
24133
|
if (found) return;
|
|
23121
|
-
if (child !== functionBody && isFunctionLike(child)) return false;
|
|
24134
|
+
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
23122
24135
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
23123
24136
|
});
|
|
23124
24137
|
return found;
|
|
@@ -23137,7 +24150,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
23137
24150
|
let found = false;
|
|
23138
24151
|
walkAst(componentBody, (child) => {
|
|
23139
24152
|
if (found) return;
|
|
23140
|
-
if (!isFunctionLike(child)) return;
|
|
24153
|
+
if (!isFunctionLike$1(child)) return;
|
|
23141
24154
|
const functionBody = child.body;
|
|
23142
24155
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
23143
24156
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -24209,6 +25222,55 @@ const rnListDataMapped = defineRule({
|
|
|
24209
25222
|
} })
|
|
24210
25223
|
});
|
|
24211
25224
|
//#endregion
|
|
25225
|
+
//#region src/plugin/rules/react-native/rn-list-missing-estimated-item-size.ts
|
|
25226
|
+
const RECYCLABLE_LIST_PACKAGES = {
|
|
25227
|
+
FlashList: ["@shopify/flash-list"],
|
|
25228
|
+
LegendList: ["@legendapp/list"]
|
|
25229
|
+
};
|
|
25230
|
+
const SIZING_HINT_ATTRIBUTE_NAMES = new Set(["estimatedItemSize", "estimatedListSize"]);
|
|
25231
|
+
const isEmptyArrayLiteral = (node) => {
|
|
25232
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return false;
|
|
25233
|
+
const expression = node.value.expression;
|
|
25234
|
+
return isNodeOfType(expression, "ArrayExpression") && (expression.elements?.length ?? 0) === 0;
|
|
25235
|
+
};
|
|
25236
|
+
const rnListMissingEstimatedItemSize = defineRule({
|
|
25237
|
+
id: "rn-list-missing-estimated-item-size",
|
|
25238
|
+
tags: ["test-noise"],
|
|
25239
|
+
requires: ["react-native"],
|
|
25240
|
+
severity: "warn",
|
|
25241
|
+
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",
|
|
25242
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
25243
|
+
const localElementName = resolveJsxElementName(node);
|
|
25244
|
+
if (!localElementName) return;
|
|
25245
|
+
let canonicalRecyclerName = null;
|
|
25246
|
+
for (const [canonicalName, packageSources] of Object.entries(RECYCLABLE_LIST_PACKAGES)) if (packageSources.some((packageSource) => getImportedNameFromModule(node, localElementName, packageSource) === canonicalName)) {
|
|
25247
|
+
canonicalRecyclerName = canonicalName;
|
|
25248
|
+
break;
|
|
25249
|
+
}
|
|
25250
|
+
if (!canonicalRecyclerName) return;
|
|
25251
|
+
let hasSizingHint = false;
|
|
25252
|
+
let dataIsEmptyLiteral = false;
|
|
25253
|
+
let hasDataProp = false;
|
|
25254
|
+
for (const attribute of node.attributes ?? []) {
|
|
25255
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
25256
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
25257
|
+
const attributeName = attribute.name.name;
|
|
25258
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
25259
|
+
if (attributeName === "data") {
|
|
25260
|
+
hasDataProp = true;
|
|
25261
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
25262
|
+
}
|
|
25263
|
+
}
|
|
25264
|
+
if (hasSizingHint) return;
|
|
25265
|
+
if (dataIsEmptyLiteral) return;
|
|
25266
|
+
if (!hasDataProp) return;
|
|
25267
|
+
context.report({
|
|
25268
|
+
node,
|
|
25269
|
+
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`
|
|
25270
|
+
});
|
|
25271
|
+
} })
|
|
25272
|
+
});
|
|
25273
|
+
//#endregion
|
|
24212
25274
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
24213
25275
|
const RECYCLABLE_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
24214
25276
|
const rnListRecyclableWithoutTypes = defineRule({
|
|
@@ -24369,7 +25431,8 @@ const classifyPackagePlatform = (filename) => {
|
|
|
24369
25431
|
//#endregion
|
|
24370
25432
|
//#region src/plugin/utils/is-expo-managed-file.ts
|
|
24371
25433
|
const isExpoManagedFileActive = (context) => {
|
|
24372
|
-
const
|
|
25434
|
+
const rawFilename = context.filename;
|
|
25435
|
+
const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
|
|
24373
25436
|
if (filename) {
|
|
24374
25437
|
const packagePlatform = classifyPackagePlatform(filename);
|
|
24375
25438
|
if (packagePlatform === "expo") return true;
|
|
@@ -24396,7 +25459,7 @@ const rnNoDeprecatedModules = defineRule({
|
|
|
24396
25459
|
if (node.source?.value !== "react-native") return;
|
|
24397
25460
|
for (const specifier of node.specifiers ?? []) {
|
|
24398
25461
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
24399
|
-
const importedName = getImportedName(specifier);
|
|
25462
|
+
const importedName = getImportedName$1(specifier);
|
|
24400
25463
|
if (!importedName) continue;
|
|
24401
25464
|
const baseReplacement = DEPRECATED_RN_MODULE_REPLACEMENTS.get(importedName);
|
|
24402
25465
|
if (!baseReplacement) continue;
|
|
@@ -24850,6 +25913,80 @@ const rnNoRawText = defineRule({
|
|
|
24850
25913
|
}
|
|
24851
25914
|
});
|
|
24852
25915
|
//#endregion
|
|
25916
|
+
//#region src/plugin/rules/react-native/rn-no-renderitem-key.ts
|
|
25917
|
+
const collectTopLevelReturnExpressions = (functionNode) => {
|
|
25918
|
+
if (isNodeOfType(functionNode, "ArrowFunctionExpression") && functionNode.body) {
|
|
25919
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
|
|
25920
|
+
}
|
|
25921
|
+
const block = functionNode.body;
|
|
25922
|
+
if (!block || !isNodeOfType(block, "BlockStatement")) return [];
|
|
25923
|
+
const returnExpressions = [];
|
|
25924
|
+
const visit = (node) => {
|
|
25925
|
+
if (FUNCTION_LIKE_TYPES$1.has(node.type)) return;
|
|
25926
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnExpressions.push(node.argument);
|
|
25927
|
+
const nodeRecord = node;
|
|
25928
|
+
for (const fieldName of Object.keys(nodeRecord)) {
|
|
25929
|
+
if (fieldName === "parent") continue;
|
|
25930
|
+
const child = nodeRecord[fieldName];
|
|
25931
|
+
if (Array.isArray(child)) {
|
|
25932
|
+
for (const childItem of child) if (isAstNode(childItem)) visit(childItem);
|
|
25933
|
+
} else if (isAstNode(child)) visit(child);
|
|
25934
|
+
}
|
|
25935
|
+
};
|
|
25936
|
+
visit(block);
|
|
25937
|
+
return returnExpressions;
|
|
25938
|
+
};
|
|
25939
|
+
const collectReturnedJsxElements = (expression) => {
|
|
25940
|
+
const elements = [];
|
|
25941
|
+
const visit = (current) => {
|
|
25942
|
+
const unwrapped = stripParenExpression(current);
|
|
25943
|
+
if (isNodeOfType(unwrapped, "JSXElement")) {
|
|
25944
|
+
elements.push(unwrapped);
|
|
25945
|
+
return;
|
|
25946
|
+
}
|
|
25947
|
+
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
25948
|
+
visit(unwrapped.consequent);
|
|
25949
|
+
visit(unwrapped.alternate);
|
|
25950
|
+
return;
|
|
25951
|
+
}
|
|
25952
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) {
|
|
25953
|
+
visit(unwrapped.right);
|
|
25954
|
+
if (unwrapped.operator === "||" || unwrapped.operator === "??") visit(unwrapped.left);
|
|
25955
|
+
}
|
|
25956
|
+
};
|
|
25957
|
+
visit(expression);
|
|
25958
|
+
return elements;
|
|
25959
|
+
};
|
|
25960
|
+
const rnNoRenderitemKey = defineRule({
|
|
25961
|
+
id: "rn-no-renderitem-key",
|
|
25962
|
+
tags: ["test-noise"],
|
|
25963
|
+
requires: ["react-native"],
|
|
25964
|
+
severity: "warn",
|
|
25965
|
+
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`",
|
|
25966
|
+
create: (context) => ({ JSXAttribute(attributeNode) {
|
|
25967
|
+
if (!isNodeOfType(attributeNode.name, "JSXIdentifier") || !RENDER_ITEM_PROP_NAMES.has(attributeNode.name.name)) return;
|
|
25968
|
+
const openingElement = attributeNode.parent;
|
|
25969
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return;
|
|
25970
|
+
const listComponentName = resolveJsxElementName(openingElement);
|
|
25971
|
+
if (!listComponentName || !REACT_NATIVE_LIST_COMPONENTS.has(listComponentName)) return;
|
|
25972
|
+
if (!attributeNode.value || !isNodeOfType(attributeNode.value, "JSXExpressionContainer")) return;
|
|
25973
|
+
const renderFunction = attributeNode.value.expression;
|
|
25974
|
+
if (!isNodeOfType(renderFunction, "ArrowFunctionExpression") && !isNodeOfType(renderFunction, "FunctionExpression")) return;
|
|
25975
|
+
const returnExpressions = collectTopLevelReturnExpressions(renderFunction);
|
|
25976
|
+
const renderPropName = attributeNode.name.name;
|
|
25977
|
+
for (const returnExpression of returnExpressions) {
|
|
25978
|
+
const returnedJsxElements = collectReturnedJsxElements(returnExpression);
|
|
25979
|
+
for (const jsxElement of returnedJsxElements) {
|
|
25980
|
+
if (!hasJsxKeyAttribute(jsxElement.openingElement)) continue;
|
|
25981
|
+
context.report({
|
|
25982
|
+
node: jsxElement.openingElement,
|
|
25983
|
+
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`
|
|
25984
|
+
});
|
|
25985
|
+
}
|
|
25986
|
+
}
|
|
25987
|
+
} })
|
|
25988
|
+
});
|
|
25989
|
+
//#endregion
|
|
24853
25990
|
//#region src/plugin/rules/react-native/rn-no-scroll-state.ts
|
|
24854
25991
|
const SET_STATE_PATTERN = /^set[A-Z]/;
|
|
24855
25992
|
const findSetStateInBody = (body) => {
|
|
@@ -25004,7 +26141,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
25004
26141
|
if (node.source?.value !== "react-native") return;
|
|
25005
26142
|
for (const specifier of node.specifiers ?? []) {
|
|
25006
26143
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25007
|
-
const importedName = getImportedName(specifier);
|
|
26144
|
+
const importedName = getImportedName$1(specifier);
|
|
25008
26145
|
if (importedName !== "Image" && importedName !== "ImageBackground") continue;
|
|
25009
26146
|
context.report({
|
|
25010
26147
|
node: specifier,
|
|
@@ -25034,7 +26171,7 @@ const rnPreferPressable = defineRule({
|
|
|
25034
26171
|
if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
|
|
25035
26172
|
for (const specifier of node.specifiers ?? []) {
|
|
25036
26173
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25037
|
-
const importedName = getImportedName(specifier);
|
|
26174
|
+
const importedName = getImportedName$1(specifier);
|
|
25038
26175
|
if (!importedName || !TOUCHABLE_COMPONENTS.has(importedName)) continue;
|
|
25039
26176
|
context.report({
|
|
25040
26177
|
node: specifier,
|
|
@@ -25044,6 +26181,81 @@ const rnPreferPressable = defineRule({
|
|
|
25044
26181
|
} })
|
|
25045
26182
|
});
|
|
25046
26183
|
//#endregion
|
|
26184
|
+
//#region src/plugin/rules/react-native/rn-prefer-pressable-over-gesture-detector.ts
|
|
26185
|
+
const COMPOSING_CHAIN_METHOD_NAMES = new Set([
|
|
26186
|
+
"simultaneousWithExternalGesture",
|
|
26187
|
+
"requireExternalGestureToFail",
|
|
26188
|
+
"blocksExternalGesture"
|
|
26189
|
+
]);
|
|
26190
|
+
const analyzeGestureChain = (expression) => {
|
|
26191
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
26192
|
+
const chainMethodNames = [];
|
|
26193
|
+
let numberOfTapsArgument = null;
|
|
26194
|
+
let cursor = expression;
|
|
26195
|
+
while (cursor && isNodeOfType(cursor, "CallExpression")) {
|
|
26196
|
+
const callExpression = cursor;
|
|
26197
|
+
const callee = callExpression.callee;
|
|
26198
|
+
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
26199
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
26200
|
+
const methodName = callee.property.name;
|
|
26201
|
+
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Gesture") return {
|
|
26202
|
+
factoryName: methodName,
|
|
26203
|
+
chainMethodNames,
|
|
26204
|
+
numberOfTapsArgument
|
|
26205
|
+
};
|
|
26206
|
+
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
26207
|
+
chainMethodNames.push(methodName);
|
|
26208
|
+
cursor = callee.object;
|
|
26209
|
+
}
|
|
26210
|
+
return null;
|
|
26211
|
+
};
|
|
26212
|
+
const isTapChainEligibleForPressable = (chain) => {
|
|
26213
|
+
if (chain.factoryName !== "Tap") return false;
|
|
26214
|
+
for (const methodName of chain.chainMethodNames) if (COMPOSING_CHAIN_METHOD_NAMES.has(methodName)) return false;
|
|
26215
|
+
const tapsArg = chain.numberOfTapsArgument;
|
|
26216
|
+
if (tapsArg !== null) {
|
|
26217
|
+
if (!isNodeOfType(tapsArg, "Literal")) return false;
|
|
26218
|
+
if (typeof tapsArg.value !== "number") return false;
|
|
26219
|
+
if (tapsArg.value !== 1) return false;
|
|
26220
|
+
}
|
|
26221
|
+
return true;
|
|
26222
|
+
};
|
|
26223
|
+
const rnPreferPressableOverGestureDetector = defineRule({
|
|
26224
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
26225
|
+
tags: ["test-noise"],
|
|
26226
|
+
requires: ["react-native"],
|
|
26227
|
+
severity: "warn",
|
|
26228
|
+
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",
|
|
26229
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26230
|
+
if (resolveJsxElementName(node) !== "GestureDetector") return;
|
|
26231
|
+
if (!isImportedFromModule(node, "GestureDetector", "react-native-gesture-handler")) return;
|
|
26232
|
+
let gestureExpression = null;
|
|
26233
|
+
for (const attribute of node.attributes ?? []) {
|
|
26234
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26235
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
26236
|
+
if (attribute.name.name !== "gesture") continue;
|
|
26237
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) continue;
|
|
26238
|
+
gestureExpression = attribute.value.expression;
|
|
26239
|
+
break;
|
|
26240
|
+
}
|
|
26241
|
+
if (!gestureExpression) return;
|
|
26242
|
+
const resolvedExpression = stripParenExpression(gestureExpression);
|
|
26243
|
+
let chainExpression = resolvedExpression;
|
|
26244
|
+
if (isNodeOfType(resolvedExpression, "Identifier")) {
|
|
26245
|
+
const binding = findVariableInitializer(node, resolvedExpression.name);
|
|
26246
|
+
if (!binding || !binding.initializer) return;
|
|
26247
|
+
chainExpression = stripParenExpression(binding.initializer);
|
|
26248
|
+
}
|
|
26249
|
+
const chain = analyzeGestureChain(chainExpression);
|
|
26250
|
+
if (!chain) return;
|
|
26251
|
+
if (!isTapChainEligibleForPressable(chain)) return;
|
|
26252
|
+
context.report({
|
|
26253
|
+
node,
|
|
26254
|
+
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)"
|
|
26255
|
+
});
|
|
26256
|
+
} })
|
|
26257
|
+
});
|
|
26258
|
+
//#endregion
|
|
25047
26259
|
//#region src/plugin/rules/react-native/rn-prefer-reanimated.ts
|
|
25048
26260
|
const JS_THREAD_ANIMATION_IMPORTS = new Set(["Animated", "LayoutAnimation"]);
|
|
25049
26261
|
const rnPreferReanimated = defineRule({
|
|
@@ -25056,7 +26268,7 @@ const rnPreferReanimated = defineRule({
|
|
|
25056
26268
|
if (node.source?.value !== "react-native") return;
|
|
25057
26269
|
for (const specifier of node.specifiers ?? []) {
|
|
25058
26270
|
if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
|
|
25059
|
-
const importedName = getImportedName(specifier);
|
|
26271
|
+
const importedName = getImportedName$1(specifier);
|
|
25060
26272
|
if (!importedName || !JS_THREAD_ANIMATION_IMPORTS.has(importedName)) continue;
|
|
25061
26273
|
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
26274
|
context.report({
|
|
@@ -25187,6 +26399,101 @@ const rnScrollviewDynamicPadding = defineRule({
|
|
|
25187
26399
|
} })
|
|
25188
26400
|
});
|
|
25189
26401
|
//#endregion
|
|
26402
|
+
//#region src/plugin/rules/react-native/rn-scrollview-flex-in-content-container.ts
|
|
26403
|
+
const VIRTUALIZED_LIST_NAMES = new Set(["FlashList", "LegendList"]);
|
|
26404
|
+
const getStaticMemberKeyName = (expression) => {
|
|
26405
|
+
if (!expression.computed) {
|
|
26406
|
+
if (isNodeOfType(expression.property, "Identifier")) return expression.property.name;
|
|
26407
|
+
return null;
|
|
26408
|
+
}
|
|
26409
|
+
if (isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "string") return expression.property.value;
|
|
26410
|
+
return null;
|
|
26411
|
+
};
|
|
26412
|
+
const isStyleSheetCreateCallExpression = (expression) => {
|
|
26413
|
+
if (!expression) return false;
|
|
26414
|
+
const callExpression = stripParenExpression(expression);
|
|
26415
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26416
|
+
const callee = callExpression.callee;
|
|
26417
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "StyleSheet" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "create";
|
|
26418
|
+
};
|
|
26419
|
+
const resolveContentContainerStyleObject = (attribute) => {
|
|
26420
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) return null;
|
|
26421
|
+
if (attribute.name.name !== "contentContainerStyle") return null;
|
|
26422
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
26423
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
26424
|
+
if (isNodeOfType(expression, "ObjectExpression")) return expression;
|
|
26425
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
26426
|
+
const styleObjectKeyName = getStaticMemberKeyName(expression);
|
|
26427
|
+
if (!styleObjectKeyName) return null;
|
|
26428
|
+
const styleObjectIdentifierName = getRootIdentifierName(expression);
|
|
26429
|
+
if (!styleObjectIdentifierName) return null;
|
|
26430
|
+
const binding = findVariableInitializer(expression, styleObjectIdentifierName);
|
|
26431
|
+
if (!binding || !binding.initializer) return null;
|
|
26432
|
+
if (!isStyleSheetCreateCallExpression(binding.initializer)) return null;
|
|
26433
|
+
const argument = stripParenExpression(binding.initializer).arguments?.[0];
|
|
26434
|
+
if (!isNodeOfType(argument, "ObjectExpression")) return null;
|
|
26435
|
+
for (const property of argument.properties ?? []) {
|
|
26436
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26437
|
+
if (property.computed) continue;
|
|
26438
|
+
let matchesKey = false;
|
|
26439
|
+
if (isNodeOfType(property.key, "Identifier")) matchesKey = property.key.name === styleObjectKeyName;
|
|
26440
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") matchesKey = property.key.value === styleObjectKeyName;
|
|
26441
|
+
if (!matchesKey) continue;
|
|
26442
|
+
const propertyValue = stripParenExpression(property.value);
|
|
26443
|
+
if (isNodeOfType(propertyValue, "ObjectExpression")) return propertyValue;
|
|
26444
|
+
return null;
|
|
26445
|
+
}
|
|
26446
|
+
}
|
|
26447
|
+
return null;
|
|
26448
|
+
};
|
|
26449
|
+
const collectStyleKeyNames = (objectExpression) => {
|
|
26450
|
+
const names = /* @__PURE__ */ new Set();
|
|
26451
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26452
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26453
|
+
if (property.computed) continue;
|
|
26454
|
+
if (isNodeOfType(property.key, "Identifier")) names.add(property.key.name);
|
|
26455
|
+
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") names.add(property.key.value);
|
|
26456
|
+
}
|
|
26457
|
+
return names;
|
|
26458
|
+
};
|
|
26459
|
+
const findFlexShorthandProperty = (objectExpression) => {
|
|
26460
|
+
for (const property of objectExpression.properties ?? []) {
|
|
26461
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
26462
|
+
if (property.computed) continue;
|
|
26463
|
+
if (!isNodeOfType(property.key, "Identifier") || property.key.name !== "flex") continue;
|
|
26464
|
+
const value = property.value;
|
|
26465
|
+
if (!isNodeOfType(value, "Literal")) return null;
|
|
26466
|
+
if (typeof value.value !== "number" || value.value <= 0) return null;
|
|
26467
|
+
return property;
|
|
26468
|
+
}
|
|
26469
|
+
return null;
|
|
26470
|
+
};
|
|
26471
|
+
const rnScrollviewFlexInContentContainer = defineRule({
|
|
26472
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
26473
|
+
tags: ["test-noise"],
|
|
26474
|
+
requires: ["react-native"],
|
|
26475
|
+
severity: "warn",
|
|
26476
|
+
recommendation: "Use `flexGrow: 1` on `contentContainerStyle` — RN's `flex: 1` shorthand sets `flexBasis: 0` and collapses the container on small devices",
|
|
26477
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
26478
|
+
const elementName = resolveJsxElementName(node);
|
|
26479
|
+
if (!elementName) return;
|
|
26480
|
+
if (!SCROLLVIEW_NAMES.has(elementName) && !VIRTUALIZED_LIST_NAMES.has(elementName)) return;
|
|
26481
|
+
for (const attribute of node.attributes ?? []) {
|
|
26482
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
26483
|
+
const objectExpression = resolveContentContainerStyleObject(attribute);
|
|
26484
|
+
if (!objectExpression) continue;
|
|
26485
|
+
const keyNames = collectStyleKeyNames(objectExpression);
|
|
26486
|
+
if (keyNames.has("flexGrow") || keyNames.has("flexBasis")) continue;
|
|
26487
|
+
const flexProperty = findFlexShorthandProperty(objectExpression);
|
|
26488
|
+
if (!flexProperty) continue;
|
|
26489
|
+
context.report({
|
|
26490
|
+
node: flexProperty,
|
|
26491
|
+
message: `<${elementName}> contentContainerStyle uses \`flex: <number>\` — RN's flex shorthand sets flexBasis: 0 and collapses the container on small devices. Use \`flexGrow: 1\` instead`
|
|
26492
|
+
});
|
|
26493
|
+
}
|
|
26494
|
+
} })
|
|
26495
|
+
});
|
|
26496
|
+
//#endregion
|
|
25190
26497
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
25191
26498
|
const LEGACY_SHADOW_KEYS = new Set([
|
|
25192
26499
|
"shadowColor",
|
|
@@ -28746,7 +30053,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
28746
30053
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
28747
30054
|
let current = node.parent;
|
|
28748
30055
|
while (current) {
|
|
28749
|
-
if (isFunctionLike(current)) {
|
|
30056
|
+
if (isFunctionLike$1(current)) {
|
|
28750
30057
|
const resolvedName = inferFunctionName(current);
|
|
28751
30058
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
28752
30059
|
}
|
|
@@ -28767,7 +30074,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
28767
30074
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
28768
30075
|
let current = node.parent;
|
|
28769
30076
|
while (current) {
|
|
28770
|
-
if (isFunctionLike(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
30077
|
+
if (isFunctionLike$1(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
28771
30078
|
current = current.parent ?? null;
|
|
28772
30079
|
}
|
|
28773
30080
|
return false;
|
|
@@ -29101,7 +30408,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
29101
30408
|
let foundAuthCall = false;
|
|
29102
30409
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
29103
30410
|
if (foundAuthCall) return;
|
|
29104
|
-
if (isFunctionLike(child)) return false;
|
|
30411
|
+
if (isFunctionLike$1(child)) return false;
|
|
29105
30412
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
29106
30413
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
29107
30414
|
});
|
|
@@ -29293,7 +30600,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
29293
30600
|
let isServerSideFile = false;
|
|
29294
30601
|
return {
|
|
29295
30602
|
Program(node) {
|
|
29296
|
-
const filename = normalizeFilename$1(context.
|
|
30603
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29297
30604
|
if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
|
|
29298
30605
|
isServerSideFile = false;
|
|
29299
30606
|
return;
|
|
@@ -29406,7 +30713,7 @@ const serverHoistStaticIo = defineRule({
|
|
|
29406
30713
|
inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
|
|
29407
30714
|
},
|
|
29408
30715
|
ExportDefaultDeclaration(node) {
|
|
29409
|
-
const filename = normalizeFilename$1(context.
|
|
30716
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29410
30717
|
if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
|
|
29411
30718
|
const declaration = node.declaration;
|
|
29412
30719
|
if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
|
|
@@ -29957,7 +31264,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29957
31264
|
};
|
|
29958
31265
|
return {
|
|
29959
31266
|
Program(node) {
|
|
29960
|
-
const filename = context.
|
|
31267
|
+
const filename = context.filename ?? "";
|
|
29961
31268
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29962
31269
|
const statements = node.body ?? [];
|
|
29963
31270
|
for (const statement of statements) collectImportBindings(statement);
|
|
@@ -29967,17 +31274,17 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29967
31274
|
}
|
|
29968
31275
|
},
|
|
29969
31276
|
ImportDeclaration(node) {
|
|
29970
|
-
const filename = context.
|
|
31277
|
+
const filename = context.filename ?? "";
|
|
29971
31278
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29972
31279
|
collectImportBindings(node);
|
|
29973
31280
|
},
|
|
29974
31281
|
VariableDeclarator(node) {
|
|
29975
|
-
const filename = context.
|
|
31282
|
+
const filename = context.filename ?? "";
|
|
29976
31283
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29977
31284
|
collectVariableAlias(node);
|
|
29978
31285
|
},
|
|
29979
31286
|
JSXOpeningElement(node) {
|
|
29980
|
-
const filename = normalizeFilename$1(context.
|
|
31287
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29981
31288
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29982
31289
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
29983
31290
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
@@ -29992,7 +31299,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
29992
31299
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
29993
31300
|
},
|
|
29994
31301
|
"Program:exit"(programNode) {
|
|
29995
|
-
const filename = normalizeFilename$1(context.
|
|
31302
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
29996
31303
|
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
29997
31304
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
29998
31305
|
node: programNode,
|
|
@@ -30011,7 +31318,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
30011
31318
|
severity: "warn",
|
|
30012
31319
|
recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
|
|
30013
31320
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
30014
|
-
const filename = normalizeFilename$1(context.
|
|
31321
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30015
31322
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30016
31323
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
30017
31324
|
const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
|
|
@@ -30085,7 +31392,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
30085
31392
|
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
31393
|
return {
|
|
30087
31394
|
CallExpression(node) {
|
|
30088
|
-
const filename = normalizeFilename$1(context.
|
|
31395
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30089
31396
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30090
31397
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
30091
31398
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
@@ -30095,17 +31402,17 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
30095
31402
|
});
|
|
30096
31403
|
},
|
|
30097
31404
|
"CallExpression:exit"(node) {
|
|
30098
|
-
const filename = normalizeFilename$1(context.
|
|
31405
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30099
31406
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30100
31407
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
30101
31408
|
},
|
|
30102
31409
|
JSXAttribute(node) {
|
|
30103
|
-
const filename = normalizeFilename$1(context.
|
|
31410
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30104
31411
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30105
31412
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
30106
31413
|
},
|
|
30107
31414
|
"JSXAttribute:exit"(node) {
|
|
30108
|
-
const filename = normalizeFilename$1(context.
|
|
31415
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30109
31416
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30110
31417
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
30111
31418
|
}
|
|
@@ -30186,7 +31493,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
30186
31493
|
severity: "warn",
|
|
30187
31494
|
recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
|
|
30188
31495
|
create: (context) => ({ CallExpression(node) {
|
|
30189
|
-
const filename = normalizeFilename$1(context.
|
|
31496
|
+
const filename = normalizeFilename$1(context.filename ?? "");
|
|
30190
31497
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
30191
31498
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
30192
31499
|
const callback = node.arguments?.[0];
|
|
@@ -30353,7 +31660,7 @@ const useLazyMotion = defineRule({
|
|
|
30353
31660
|
if (node.specifiers?.some((specifier) => {
|
|
30354
31661
|
if (!isNodeOfType(specifier, "ImportSpecifier")) return false;
|
|
30355
31662
|
if (specifier.importKind === "type") return false;
|
|
30356
|
-
return getImportedName(specifier) === "motion";
|
|
31663
|
+
return getImportedName$1(specifier) === "motion";
|
|
30357
31664
|
})) context.report({
|
|
30358
31665
|
node,
|
|
30359
31666
|
message: "Import \"m\" with LazyMotion instead of \"motion\" — saves ~30kb in bundle size"
|
|
@@ -30434,6 +31741,17 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
30434
31741
|
//#endregion
|
|
30435
31742
|
//#region src/plugin/rule-registry.ts
|
|
30436
31743
|
const reactDoctorRules = [
|
|
31744
|
+
{
|
|
31745
|
+
key: "react-doctor/activity-wraps-effect-heavy-subtree",
|
|
31746
|
+
id: "activity-wraps-effect-heavy-subtree",
|
|
31747
|
+
source: "react-doctor",
|
|
31748
|
+
originallyExternal: false,
|
|
31749
|
+
rule: {
|
|
31750
|
+
...activityWrapsEffectHeavySubtree,
|
|
31751
|
+
framework: "global",
|
|
31752
|
+
category: "State & Effects"
|
|
31753
|
+
}
|
|
31754
|
+
},
|
|
30437
31755
|
{
|
|
30438
31756
|
key: "react-doctor/advanced-event-handler-refs",
|
|
30439
31757
|
id: "advanced-event-handler-refs",
|
|
@@ -30819,6 +32137,17 @@ const reactDoctorRules = [
|
|
|
30819
32137
|
category: "Architecture"
|
|
30820
32138
|
}
|
|
30821
32139
|
},
|
|
32140
|
+
{
|
|
32141
|
+
key: "react-doctor/hooks-no-nan-in-deps",
|
|
32142
|
+
id: "hooks-no-nan-in-deps",
|
|
32143
|
+
source: "react-doctor",
|
|
32144
|
+
originallyExternal: false,
|
|
32145
|
+
rule: {
|
|
32146
|
+
...hooksNoNanInDeps,
|
|
32147
|
+
framework: "global",
|
|
32148
|
+
category: "State & Effects"
|
|
32149
|
+
}
|
|
32150
|
+
},
|
|
30822
32151
|
{
|
|
30823
32152
|
key: "react-doctor/html-has-lang",
|
|
30824
32153
|
id: "html-has-lang",
|
|
@@ -30830,6 +32159,39 @@ const reactDoctorRules = [
|
|
|
30830
32159
|
category: "Accessibility"
|
|
30831
32160
|
}
|
|
30832
32161
|
},
|
|
32162
|
+
{
|
|
32163
|
+
key: "react-doctor/html-no-invalid-paragraph-child",
|
|
32164
|
+
id: "html-no-invalid-paragraph-child",
|
|
32165
|
+
source: "react-doctor",
|
|
32166
|
+
originallyExternal: false,
|
|
32167
|
+
rule: {
|
|
32168
|
+
...htmlNoInvalidParagraphChild,
|
|
32169
|
+
framework: "global",
|
|
32170
|
+
category: "Correctness"
|
|
32171
|
+
}
|
|
32172
|
+
},
|
|
32173
|
+
{
|
|
32174
|
+
key: "react-doctor/html-no-invalid-table-nesting",
|
|
32175
|
+
id: "html-no-invalid-table-nesting",
|
|
32176
|
+
source: "react-doctor",
|
|
32177
|
+
originallyExternal: false,
|
|
32178
|
+
rule: {
|
|
32179
|
+
...htmlNoInvalidTableNesting,
|
|
32180
|
+
framework: "global",
|
|
32181
|
+
category: "Correctness"
|
|
32182
|
+
}
|
|
32183
|
+
},
|
|
32184
|
+
{
|
|
32185
|
+
key: "react-doctor/html-no-nested-interactive",
|
|
32186
|
+
id: "html-no-nested-interactive",
|
|
32187
|
+
source: "react-doctor",
|
|
32188
|
+
originallyExternal: false,
|
|
32189
|
+
rule: {
|
|
32190
|
+
...htmlNoNestedInteractive,
|
|
32191
|
+
framework: "global",
|
|
32192
|
+
category: "Correctness"
|
|
32193
|
+
}
|
|
32194
|
+
},
|
|
30833
32195
|
{
|
|
30834
32196
|
key: "react-doctor/iframe-has-title",
|
|
30835
32197
|
id: "iframe-has-title",
|
|
@@ -30874,6 +32236,50 @@ const reactDoctorRules = [
|
|
|
30874
32236
|
category: "Accessibility"
|
|
30875
32237
|
}
|
|
30876
32238
|
},
|
|
32239
|
+
{
|
|
32240
|
+
key: "react-doctor/jotai-derived-atom-returns-fresh-object",
|
|
32241
|
+
id: "jotai-derived-atom-returns-fresh-object",
|
|
32242
|
+
source: "react-doctor",
|
|
32243
|
+
originallyExternal: false,
|
|
32244
|
+
rule: {
|
|
32245
|
+
...jotaiDerivedAtomReturnsFreshObject,
|
|
32246
|
+
framework: "global",
|
|
32247
|
+
category: "State & Effects"
|
|
32248
|
+
}
|
|
32249
|
+
},
|
|
32250
|
+
{
|
|
32251
|
+
key: "react-doctor/jotai-select-atom-in-render-body",
|
|
32252
|
+
id: "jotai-select-atom-in-render-body",
|
|
32253
|
+
source: "react-doctor",
|
|
32254
|
+
originallyExternal: false,
|
|
32255
|
+
rule: {
|
|
32256
|
+
...jotaiSelectAtomInRenderBody,
|
|
32257
|
+
framework: "global",
|
|
32258
|
+
category: "State & Effects"
|
|
32259
|
+
}
|
|
32260
|
+
},
|
|
32261
|
+
{
|
|
32262
|
+
key: "react-doctor/jotai-tq-use-raw-query-atom",
|
|
32263
|
+
id: "jotai-tq-use-raw-query-atom",
|
|
32264
|
+
source: "react-doctor",
|
|
32265
|
+
originallyExternal: false,
|
|
32266
|
+
rule: {
|
|
32267
|
+
...jotaiTqUseRawQueryAtom,
|
|
32268
|
+
framework: "global",
|
|
32269
|
+
category: "State & Effects"
|
|
32270
|
+
}
|
|
32271
|
+
},
|
|
32272
|
+
{
|
|
32273
|
+
key: "react-doctor/js-async-reduce-without-awaited-acc",
|
|
32274
|
+
id: "js-async-reduce-without-awaited-acc",
|
|
32275
|
+
source: "react-doctor",
|
|
32276
|
+
originallyExternal: false,
|
|
32277
|
+
rule: {
|
|
32278
|
+
...jsAsyncReduceWithoutAwaitedAcc,
|
|
32279
|
+
framework: "global",
|
|
32280
|
+
category: "Performance"
|
|
32281
|
+
}
|
|
32282
|
+
},
|
|
30877
32283
|
{
|
|
30878
32284
|
key: "react-doctor/js-batch-dom-css",
|
|
30879
32285
|
id: "js-batch-dom-css",
|
|
@@ -32601,6 +34007,61 @@ const reactDoctorRules = [
|
|
|
32601
34007
|
category: "Architecture"
|
|
32602
34008
|
}
|
|
32603
34009
|
},
|
|
34010
|
+
{
|
|
34011
|
+
key: "react-doctor/preact-no-children-length",
|
|
34012
|
+
id: "preact-no-children-length",
|
|
34013
|
+
source: "react-doctor",
|
|
34014
|
+
originallyExternal: false,
|
|
34015
|
+
rule: {
|
|
34016
|
+
...preactNoChildrenLength,
|
|
34017
|
+
framework: "preact",
|
|
34018
|
+
category: "Preact"
|
|
34019
|
+
}
|
|
34020
|
+
},
|
|
34021
|
+
{
|
|
34022
|
+
key: "react-doctor/preact-no-react-hooks-import",
|
|
34023
|
+
id: "preact-no-react-hooks-import",
|
|
34024
|
+
source: "react-doctor",
|
|
34025
|
+
originallyExternal: false,
|
|
34026
|
+
rule: {
|
|
34027
|
+
...preactNoReactHooksImport,
|
|
34028
|
+
framework: "preact",
|
|
34029
|
+
category: "Preact"
|
|
34030
|
+
}
|
|
34031
|
+
},
|
|
34032
|
+
{
|
|
34033
|
+
key: "react-doctor/preact-no-render-arguments",
|
|
34034
|
+
id: "preact-no-render-arguments",
|
|
34035
|
+
source: "react-doctor",
|
|
34036
|
+
originallyExternal: false,
|
|
34037
|
+
rule: {
|
|
34038
|
+
...preactNoRenderArguments,
|
|
34039
|
+
framework: "preact",
|
|
34040
|
+
category: "Preact"
|
|
34041
|
+
}
|
|
34042
|
+
},
|
|
34043
|
+
{
|
|
34044
|
+
key: "react-doctor/preact-prefer-ondblclick",
|
|
34045
|
+
id: "preact-prefer-ondblclick",
|
|
34046
|
+
source: "react-doctor",
|
|
34047
|
+
originallyExternal: false,
|
|
34048
|
+
rule: {
|
|
34049
|
+
...preactPreferOndblclick,
|
|
34050
|
+
framework: "preact",
|
|
34051
|
+
category: "Preact"
|
|
34052
|
+
}
|
|
34053
|
+
},
|
|
34054
|
+
{
|
|
34055
|
+
key: "react-doctor/preact-prefer-oninput",
|
|
34056
|
+
id: "preact-prefer-oninput",
|
|
34057
|
+
source: "react-doctor",
|
|
34058
|
+
originallyExternal: false,
|
|
34059
|
+
rule: {
|
|
34060
|
+
...preactPreferOninput,
|
|
34061
|
+
framework: "preact",
|
|
34062
|
+
category: "Preact"
|
|
34063
|
+
}
|
|
34064
|
+
},
|
|
32604
34065
|
{
|
|
32605
34066
|
key: "react-doctor/prefer-dynamic-import",
|
|
32606
34067
|
id: "prefer-dynamic-import",
|
|
@@ -32634,6 +34095,17 @@ const reactDoctorRules = [
|
|
|
32634
34095
|
category: "Architecture"
|
|
32635
34096
|
}
|
|
32636
34097
|
},
|
|
34098
|
+
{
|
|
34099
|
+
key: "react-doctor/prefer-html-dialog",
|
|
34100
|
+
id: "prefer-html-dialog",
|
|
34101
|
+
source: "react-doctor",
|
|
34102
|
+
originallyExternal: false,
|
|
34103
|
+
rule: {
|
|
34104
|
+
...preferHtmlDialog,
|
|
34105
|
+
framework: "global",
|
|
34106
|
+
category: "Accessibility"
|
|
34107
|
+
}
|
|
34108
|
+
},
|
|
32637
34109
|
{
|
|
32638
34110
|
key: "react-doctor/prefer-tag-over-role",
|
|
32639
34111
|
id: "prefer-tag-over-role",
|
|
@@ -32744,17 +34216,6 @@ const reactDoctorRules = [
|
|
|
32744
34216
|
category: "TanStack Query"
|
|
32745
34217
|
}
|
|
32746
34218
|
},
|
|
32747
|
-
{
|
|
32748
|
-
key: "react-doctor/react-compiler-destructure-method",
|
|
32749
|
-
id: "react-compiler-destructure-method",
|
|
32750
|
-
source: "react-doctor",
|
|
32751
|
-
originallyExternal: false,
|
|
32752
|
-
rule: {
|
|
32753
|
-
...reactCompilerDestructureMethod,
|
|
32754
|
-
framework: "global",
|
|
32755
|
-
category: "Architecture"
|
|
32756
|
-
}
|
|
32757
|
-
},
|
|
32758
34219
|
{
|
|
32759
34220
|
key: "react-doctor/react-compiler-no-manual-memoization",
|
|
32760
34221
|
id: "react-compiler-no-manual-memoization",
|
|
@@ -33035,6 +34496,18 @@ const reactDoctorRules = [
|
|
|
33035
34496
|
tags: [...new Set(["react-native", ...rnListDataMapped.tags ?? []])]
|
|
33036
34497
|
}
|
|
33037
34498
|
},
|
|
34499
|
+
{
|
|
34500
|
+
key: "react-doctor/rn-list-missing-estimated-item-size",
|
|
34501
|
+
id: "rn-list-missing-estimated-item-size",
|
|
34502
|
+
source: "react-doctor",
|
|
34503
|
+
originallyExternal: false,
|
|
34504
|
+
rule: {
|
|
34505
|
+
...rnListMissingEstimatedItemSize,
|
|
34506
|
+
framework: "react-native",
|
|
34507
|
+
category: "React Native",
|
|
34508
|
+
tags: [...new Set(["react-native", ...rnListMissingEstimatedItemSize.tags ?? []])]
|
|
34509
|
+
}
|
|
34510
|
+
},
|
|
33038
34511
|
{
|
|
33039
34512
|
key: "react-doctor/rn-list-recyclable-without-types",
|
|
33040
34513
|
id: "rn-list-recyclable-without-types",
|
|
@@ -33155,6 +34628,18 @@ const reactDoctorRules = [
|
|
|
33155
34628
|
tags: [...new Set(["react-native", ...rnNoRawText.tags ?? []])]
|
|
33156
34629
|
}
|
|
33157
34630
|
},
|
|
34631
|
+
{
|
|
34632
|
+
key: "react-doctor/rn-no-renderitem-key",
|
|
34633
|
+
id: "rn-no-renderitem-key",
|
|
34634
|
+
source: "react-doctor",
|
|
34635
|
+
originallyExternal: false,
|
|
34636
|
+
rule: {
|
|
34637
|
+
...rnNoRenderitemKey,
|
|
34638
|
+
framework: "react-native",
|
|
34639
|
+
category: "React Native",
|
|
34640
|
+
tags: [...new Set(["react-native", ...rnNoRenderitemKey.tags ?? []])]
|
|
34641
|
+
}
|
|
34642
|
+
},
|
|
33158
34643
|
{
|
|
33159
34644
|
key: "react-doctor/rn-no-scroll-state",
|
|
33160
34645
|
id: "rn-no-scroll-state",
|
|
@@ -33227,6 +34712,18 @@ const reactDoctorRules = [
|
|
|
33227
34712
|
tags: [...new Set(["react-native", ...rnPreferPressable.tags ?? []])]
|
|
33228
34713
|
}
|
|
33229
34714
|
},
|
|
34715
|
+
{
|
|
34716
|
+
key: "react-doctor/rn-prefer-pressable-over-gesture-detector",
|
|
34717
|
+
id: "rn-prefer-pressable-over-gesture-detector",
|
|
34718
|
+
source: "react-doctor",
|
|
34719
|
+
originallyExternal: false,
|
|
34720
|
+
rule: {
|
|
34721
|
+
...rnPreferPressableOverGestureDetector,
|
|
34722
|
+
framework: "react-native",
|
|
34723
|
+
category: "React Native",
|
|
34724
|
+
tags: [...new Set(["react-native", ...rnPreferPressableOverGestureDetector.tags ?? []])]
|
|
34725
|
+
}
|
|
34726
|
+
},
|
|
33230
34727
|
{
|
|
33231
34728
|
key: "react-doctor/rn-prefer-reanimated",
|
|
33232
34729
|
id: "rn-prefer-reanimated",
|
|
@@ -33263,6 +34760,18 @@ const reactDoctorRules = [
|
|
|
33263
34760
|
tags: [...new Set(["react-native", ...rnScrollviewDynamicPadding.tags ?? []])]
|
|
33264
34761
|
}
|
|
33265
34762
|
},
|
|
34763
|
+
{
|
|
34764
|
+
key: "react-doctor/rn-scrollview-flex-in-content-container",
|
|
34765
|
+
id: "rn-scrollview-flex-in-content-container",
|
|
34766
|
+
source: "react-doctor",
|
|
34767
|
+
originallyExternal: false,
|
|
34768
|
+
rule: {
|
|
34769
|
+
...rnScrollviewFlexInContentContainer,
|
|
34770
|
+
framework: "react-native",
|
|
34771
|
+
category: "React Native",
|
|
34772
|
+
tags: [...new Set(["react-native", ...rnScrollviewFlexInContentContainer.tags ?? []])]
|
|
34773
|
+
}
|
|
34774
|
+
},
|
|
33266
34775
|
{
|
|
33267
34776
|
key: "react-doctor/rn-style-prefer-boxshadow",
|
|
33268
34777
|
id: "rn-style-prefer-boxshadow",
|
|
@@ -33642,7 +35151,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
|
|
|
33642
35151
|
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
33643
35152
|
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
33644
35153
|
const isReactNativeFileActive = (context) => {
|
|
33645
|
-
const rawFilename = context.
|
|
35154
|
+
const rawFilename = context.filename;
|
|
33646
35155
|
if (!rawFilename) return true;
|
|
33647
35156
|
const filename = normalizeFilename$1(rawFilename);
|
|
33648
35157
|
if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
|
|
@@ -33696,7 +35205,7 @@ const appendNode = (builder, block, node) => {
|
|
|
33696
35205
|
};
|
|
33697
35206
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
33698
35207
|
builder.nodeBlock.set(node, block);
|
|
33699
|
-
if (isFunctionLike(node)) return;
|
|
35208
|
+
if (isFunctionLike$1(node)) return;
|
|
33700
35209
|
const record = node;
|
|
33701
35210
|
for (const key of Object.keys(record)) {
|
|
33702
35211
|
if (key === "parent") continue;
|
|
@@ -34034,7 +35543,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34034
35543
|
body: program.body
|
|
34035
35544
|
});
|
|
34036
35545
|
const visit = (node) => {
|
|
34037
|
-
if (isFunctionLike(node)) {
|
|
35546
|
+
if (isFunctionLike$1(node)) {
|
|
34038
35547
|
const body = node.body;
|
|
34039
35548
|
if (body) buildFor(node, body);
|
|
34040
35549
|
}
|
|
@@ -34051,7 +35560,7 @@ const analyzeControlFlow = (program) => {
|
|
|
34051
35560
|
const enclosingFunction = (node) => {
|
|
34052
35561
|
let current = node;
|
|
34053
35562
|
while (current) {
|
|
34054
|
-
if (isFunctionLike(current)) return current;
|
|
35563
|
+
if (isFunctionLike$1(current)) return current;
|
|
34055
35564
|
if (isNodeOfType(current, "Program")) return current;
|
|
34056
35565
|
current = current.parent ?? null;
|
|
34057
35566
|
}
|
|
@@ -34130,7 +35639,9 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
34130
35639
|
};
|
|
34131
35640
|
const enrichedContext = {
|
|
34132
35641
|
report: baseContext.report,
|
|
34133
|
-
|
|
35642
|
+
get filename() {
|
|
35643
|
+
return baseContext.filename ?? baseContext.getFilename?.();
|
|
35644
|
+
},
|
|
34134
35645
|
settings: baseContext.settings,
|
|
34135
35646
|
get scopes() {
|
|
34136
35647
|
return getScopes();
|
|
@@ -34273,6 +35784,7 @@ const NEXTJS_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramewor
|
|
|
34273
35784
|
const REACT_NATIVE_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("react-native")));
|
|
34274
35785
|
const TANSTACK_START_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-start")));
|
|
34275
35786
|
const TANSTACK_QUERY_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-query")));
|
|
35787
|
+
const PREACT_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("preact")));
|
|
34276
35788
|
const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES));
|
|
34277
35789
|
const ALL_REACT_DOCTOR_RULE_KEYS = new Set(REACT_DOCTOR_RULES.map((rule) => rule.key));
|
|
34278
35790
|
const FRAMEWORK_SPECIFIC_RULE_KEYS = collectFrameworkSpecificRuleKeys();
|
|
@@ -34281,6 +35793,6 @@ const REACT_COMPILER_RULES = toRuleMap(collectExternalRulesBySource("react-compi
|
|
|
34281
35793
|
//#region src/index.ts
|
|
34282
35794
|
var src_default = plugin;
|
|
34283
35795
|
//#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 };
|
|
35796
|
+
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
35797
|
|
|
34286
35798
|
//# sourceMappingURL=index.js.map
|