oxlint-plugin-react-doctor 0.2.9 → 0.2.11-dev.f036b0f

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