oxlint-plugin-react-doctor 0.2.9 → 0.2.11

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