oxlint-plugin-react-doctor 0.2.9 → 0.2.10

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