oxlint-plugin-react-doctor 0.2.10 → 0.2.11-dev.d0f5206

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 +401 -20
  2. package/dist/index.js +2244 -594
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ 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
+ import { parseSync } from "oxc-parser";
5
6
  //#region src/plugin/utils/is-testlike-filename.ts
6
7
  const NON_PRODUCTION_PATH_SEGMENTS = [
7
8
  "/test/",
@@ -223,7 +224,7 @@ const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
223
224
  //#endregion
224
225
  //#region src/plugin/utils/define-rule.ts
225
226
  const wrapCreateForTestNoise = (create) => ((context) => {
226
- if (isTestlikeFilename(context.getFilename?.())) return {};
227
+ if (isTestlikeFilename(context.filename)) return {};
227
228
  return create(context);
228
229
  });
229
230
  const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
@@ -444,6 +445,13 @@ const REACT_HOC_NAMES = new Set([
444
445
  "React.memo",
445
446
  "React.forwardRef"
446
447
  ]);
448
+ const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
449
+ const COMPONENT_HOC_WRAPPER_NAMES = new Set([
450
+ "memo",
451
+ "forwardRef",
452
+ "observer",
453
+ "lazy"
454
+ ]);
447
455
  const SUBSCRIPTION_METHOD_NAMES = new Set([
448
456
  "subscribe",
449
457
  "addEventListener",
@@ -813,6 +821,12 @@ const getElementType = (openingElement, settings) => {
813
821
  return baseName;
814
822
  };
815
823
  //#endregion
824
+ //#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
825
+ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
826
+ if (!rawFilename) return false;
827
+ return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
828
+ };
829
+ //#endregion
816
830
  //#region src/plugin/utils/is-hidden-from-screen-reader.ts
817
831
  const isHiddenFromScreenReader = (openingElement, settings) => {
818
832
  if (getElementType(openingElement, settings).toLowerCase() === "input") {
@@ -983,6 +997,7 @@ const altText = defineRule({
983
997
  recommendation: "Provide `alt` (or aria-label / aria-labelledby) for non-decorative images.",
984
998
  category: "Accessibility",
985
999
  create: (context) => {
1000
+ if (isNextjsMetadataImageRouteFilename(context.filename)) return {};
986
1001
  const settings = resolveSettings$53(context.settings);
987
1002
  const checkImg = !settings.elements || settings.elements.includes("img");
988
1003
  const checkObject = !settings.elements || settings.elements.includes("object");
@@ -1019,7 +1034,7 @@ const altText = defineRule({
1019
1034
  });
1020
1035
  //#endregion
1021
1036
  //#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
1022
- const buildMessage$28 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
1037
+ const buildMessage$30 = (text) => `\`${text}\` is ambiguous link text — describe the destination instead (e.g. "View pricing details").`;
1023
1038
  const DEFAULT_AMBIGUOUS = [
1024
1039
  "click here",
1025
1040
  "here",
@@ -1076,14 +1091,14 @@ const anchorAmbiguousText = defineRule({
1076
1091
  const normalized = normalizeText(accessibleText);
1077
1092
  if (ambiguousSet.has(normalized)) context.report({
1078
1093
  node: node.openingElement.name,
1079
- message: buildMessage$28(normalized)
1094
+ message: buildMessage$30(normalized)
1080
1095
  });
1081
1096
  } };
1082
1097
  }
1083
1098
  });
1084
1099
  //#endregion
1085
1100
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1086
- const MESSAGE$47 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
1101
+ const MESSAGE$49 = "Anchor must have accessible content — provide visible text, `aria-label`, or `aria-labelledby`.";
1087
1102
  const anchorHasContent = defineRule({
1088
1103
  id: "anchor-has-content",
1089
1104
  tags: ["react-jsx-only"],
@@ -1098,7 +1113,7 @@ const anchorHasContent = defineRule({
1098
1113
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1099
1114
  context.report({
1100
1115
  node: opening.name,
1101
- message: MESSAGE$47
1116
+ message: MESSAGE$49
1102
1117
  });
1103
1118
  } })
1104
1119
  });
@@ -1491,7 +1506,7 @@ const parseJsxValue = (value) => {
1491
1506
  };
1492
1507
  //#endregion
1493
1508
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
1494
- const MESSAGE$46 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
1509
+ const MESSAGE$48 = "An element with `aria-activedescendant` must be tabbable — add `tabIndex={0}` so it can receive focus.";
1495
1510
  const ariaActivedescendantHasTabindex = defineRule({
1496
1511
  id: "aria-activedescendant-has-tabindex",
1497
1512
  tags: ["react-jsx-only"],
@@ -1508,14 +1523,14 @@ const ariaActivedescendantHasTabindex = defineRule({
1508
1523
  if (tabIndexValue === null || tabIndexValue >= -1) return;
1509
1524
  context.report({
1510
1525
  node: node.name,
1511
- message: MESSAGE$46
1526
+ message: MESSAGE$48
1512
1527
  });
1513
1528
  return;
1514
1529
  }
1515
1530
  if (isInteractiveElement(tag, node)) return;
1516
1531
  context.report({
1517
1532
  node: node.name,
1518
- message: MESSAGE$46
1533
+ message: MESSAGE$48
1519
1534
  });
1520
1535
  } })
1521
1536
  });
@@ -1655,7 +1670,7 @@ const ARIA_PROPERTIES = new Map([
1655
1670
  const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
1656
1671
  //#endregion
1657
1672
  //#region src/plugin/rules/a11y/aria-props.ts
1658
- const buildMessage$27 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
1673
+ const buildMessage$29 = (name) => `\`${name}\` is not a valid ARIA property — check WAI-ARIA spec.`;
1659
1674
  const ariaProps = defineRule({
1660
1675
  id: "aria-props",
1661
1676
  tags: ["react-jsx-only"],
@@ -1668,7 +1683,7 @@ const ariaProps = defineRule({
1668
1683
  if (!name || !name.startsWith("aria-")) return;
1669
1684
  if (!isValidAriaProperty(name)) context.report({
1670
1685
  node: node.name,
1671
- message: buildMessage$27(name)
1686
+ message: buildMessage$29(name)
1672
1687
  });
1673
1688
  } })
1674
1689
  });
@@ -1819,7 +1834,7 @@ const buildExpectedDescription = (propType) => {
1819
1834
  case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
1820
1835
  }
1821
1836
  };
1822
- const buildMessage$26 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1837
+ const buildMessage$28 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1823
1838
  const allowNoneValue = (propType) => {
1824
1839
  switch (propType.kind) {
1825
1840
  case "boolean":
@@ -1952,13 +1967,13 @@ const ariaProptypes = defineRule({
1952
1967
  if (!node.value) {
1953
1968
  if (!allowNoneValue(propType)) context.report({
1954
1969
  node,
1955
- message: buildMessage$26(propName, propType)
1970
+ message: buildMessage$28(propName, propType)
1956
1971
  });
1957
1972
  return;
1958
1973
  }
1959
1974
  if (!isValidValueForType(propType, node.value)) context.report({
1960
1975
  node,
1961
- message: buildMessage$26(propName, propType)
1976
+ message: buildMessage$28(propName, propType)
1962
1977
  });
1963
1978
  } })
1964
1979
  });
@@ -2270,7 +2285,7 @@ const ariaRole = defineRule({
2270
2285
  });
2271
2286
  //#endregion
2272
2287
  //#region src/plugin/rules/a11y/aria-unsupported-elements.ts
2273
- const buildMessage$25 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
2288
+ const buildMessage$27 = (tag, attribute) => `\`<${tag}>\` does not support \`${attribute}\` — reserved HTML elements don't accept ARIA attributes.`;
2274
2289
  const ariaUnsupportedElements = defineRule({
2275
2290
  id: "aria-unsupported-elements",
2276
2291
  tags: ["react-jsx-only"],
@@ -2287,7 +2302,7 @@ const ariaUnsupportedElements = defineRule({
2287
2302
  if (!attrName) continue;
2288
2303
  if (attrName.startsWith("aria-") || attrName === "role") context.report({
2289
2304
  node: attribute,
2290
- message: buildMessage$25(tag, attrName)
2305
+ message: buildMessage$27(tag, attrName)
2291
2306
  });
2292
2307
  }
2293
2308
  } })
@@ -2320,7 +2335,7 @@ const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
2320
2335
  "BigInt",
2321
2336
  "Reflect"
2322
2337
  ]);
2323
- const MUTATING_ARRAY_METHODS$1 = new Set([
2338
+ const MUTATING_ARRAY_METHODS = new Set([
2324
2339
  "push",
2325
2340
  "pop",
2326
2341
  "shift",
@@ -2331,6 +2346,12 @@ const MUTATING_ARRAY_METHODS$1 = new Set([
2331
2346
  "fill",
2332
2347
  "copyWithin"
2333
2348
  ]);
2349
+ const MUTATING_COLLECTION_METHODS = new Set([
2350
+ "add",
2351
+ "clear",
2352
+ "delete",
2353
+ "set"
2354
+ ]);
2334
2355
  const CHAINABLE_ITERATION_METHODS = new Set([
2335
2356
  "map",
2336
2357
  "filter",
@@ -2542,7 +2563,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
2542
2563
  * (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
2543
2564
  * type-guard form covers both shapes without callers paying a cast.
2544
2565
  */
2545
- const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
2566
+ const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
2546
2567
  //#endregion
2547
2568
  //#region src/plugin/utils/is-inline-function-expression.ts
2548
2569
  /**
@@ -2565,7 +2586,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
2565
2586
  let firstAwait = null;
2566
2587
  walkAst(block, (child) => {
2567
2588
  if (firstAwait) return false;
2568
- if (child !== block && isFunctionLike$1(child)) return false;
2589
+ if (child !== block && isFunctionLike$2(child)) return false;
2569
2590
  if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
2570
2591
  });
2571
2592
  return firstAwait;
@@ -2712,6 +2733,17 @@ const asyncAwaitInLoop = defineRule({
2712
2733
  }
2713
2734
  });
2714
2735
  //#endregion
2736
+ //#region src/plugin/constants/ts-type-position-keys.ts
2737
+ const TYPE_POSITION_CHILD_KEYS = new Set([
2738
+ "implements",
2739
+ "returnType",
2740
+ "superTypeArguments",
2741
+ "superTypeParameters",
2742
+ "typeAnnotation",
2743
+ "typeArguments",
2744
+ "typeParameters"
2745
+ ]);
2746
+ //#endregion
2715
2747
  //#region src/plugin/utils/collect-pattern-names.ts
2716
2748
  const collectPatternNames = (pattern, into) => {
2717
2749
  if (!pattern) return;
@@ -2741,14 +2773,6 @@ const collectPatternNames = (pattern, into) => {
2741
2773
  };
2742
2774
  //#endregion
2743
2775
  //#region src/plugin/utils/collect-reference-identifier-names.ts
2744
- const TYPE_POSITION_KEYS = new Set([
2745
- "typeAnnotation",
2746
- "typeParameters",
2747
- "typeArguments",
2748
- "returnType",
2749
- "superTypeArguments",
2750
- "superTypeParameters"
2751
- ]);
2752
2776
  const collectScopedReferencesInPattern = (pattern, into, shadowed) => {
2753
2777
  if (!pattern) return;
2754
2778
  if (isNodeOfType(pattern, "Identifier")) return;
@@ -2809,7 +2833,7 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
2809
2833
  if (typeof node.type === "string" && node.type.startsWith("TS")) return;
2810
2834
  for (const [key, child] of Object.entries(node)) {
2811
2835
  if (key === "parent") continue;
2812
- if (TYPE_POSITION_KEYS.has(key)) continue;
2836
+ if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
2813
2837
  if (Array.isArray(child)) {
2814
2838
  for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
2815
2839
  } else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
@@ -3017,13 +3041,13 @@ const asyncDeferAwait = defineRule({
3017
3041
  const inspectAllStatementBlocks = (functionBody) => {
3018
3042
  if (!functionBody) return;
3019
3043
  walkAst(functionBody, (descendant) => {
3020
- if (isFunctionLike$1(descendant)) return false;
3044
+ if (isFunctionLike$2(descendant)) return false;
3021
3045
  if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
3022
3046
  else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
3023
3047
  });
3024
3048
  };
3025
3049
  const enterFunction = (node) => {
3026
- if (!isFunctionLike$1(node)) return;
3050
+ if (!isFunctionLike$2(node)) return;
3027
3051
  if (!node.async) return;
3028
3052
  if (!isNodeOfType(node.body, "BlockStatement")) return;
3029
3053
  inspectAllStatementBlocks(node.body);
@@ -3127,7 +3151,7 @@ const asyncParallel = defineRule({
3127
3151
  severity: "warn",
3128
3152
  recommendation: "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
3129
3153
  create: (context) => {
3130
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
3154
+ const filename = normalizeFilename$1(context.filename ?? "");
3131
3155
  const isBrowserTestFile = BROWSER_TEST_FILE_PATTERN.test(filename);
3132
3156
  let hasTestLibraryImport = false;
3133
3157
  const shouldSkipFile = () => isBrowserTestFile || hasTestLibraryImport;
@@ -3154,7 +3178,7 @@ const asyncParallel = defineRule({
3154
3178
  });
3155
3179
  //#endregion
3156
3180
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
3157
- const buildMessage$24 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
3181
+ const buildMessage$26 = (value) => `\`autoComplete\` value \`${value}\` is not a known HTML autofill token.`;
3158
3182
  const AUTOFILL_TOKENS = new Set([
3159
3183
  "off",
3160
3184
  "on",
@@ -3242,7 +3266,7 @@ const autocompleteValid = defineRule({
3242
3266
  if (!AUTOFILL_TOKENS.has(token)) {
3243
3267
  context.report({
3244
3268
  node: attribute,
3245
- message: buildMessage$24(value)
3269
+ message: buildMessage$26(value)
3246
3270
  });
3247
3271
  return;
3248
3272
  }
@@ -3330,7 +3354,7 @@ const buttonHasType = defineRule({
3330
3354
  recommendation: "Set `type=\"button\"` (or `\"submit\"` / `\"reset\"`) explicitly on every `<button>`.",
3331
3355
  create: (context) => {
3332
3356
  const settings = resolveSettings$48(context.settings);
3333
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
3357
+ const isTestlikeFile = isTestlikeFilename(context.filename);
3334
3358
  return {
3335
3359
  JSXOpeningElement(node) {
3336
3360
  if (isTestlikeFile) return;
@@ -3517,7 +3541,7 @@ const isPureEventBlockerHandler = (attribute) => {
3517
3541
  //#endregion
3518
3542
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
3519
3543
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
3520
- const MESSAGE$45 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
3544
+ const MESSAGE$47 = "Visible non-interactive elements with click handlers must have a corresponding keyboard listener (`onKeyUp`, `onKeyDown`, or `onKeyPress`).";
3521
3545
  const KEY_HANDLERS = [
3522
3546
  "onKeyUp",
3523
3547
  "onKeyDown",
@@ -3530,7 +3554,7 @@ const clickEventsHaveKeyEvents = defineRule({
3530
3554
  recommendation: "Pair `onClick` with `onKeyUp` / `onKeyDown` / `onKeyPress` for keyboard users.",
3531
3555
  category: "Accessibility",
3532
3556
  create: (context) => {
3533
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
3557
+ const isTestlikeFile = isTestlikeFilename(context.filename);
3534
3558
  return { JSXOpeningElement(node) {
3535
3559
  if (isTestlikeFile) return;
3536
3560
  const tag = getElementType(node, context.settings);
@@ -3548,7 +3572,7 @@ const clickEventsHaveKeyEvents = defineRule({
3548
3572
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
3549
3573
  context.report({
3550
3574
  node: node.name,
3551
- message: MESSAGE$45
3575
+ message: MESSAGE$47
3552
3576
  });
3553
3577
  } };
3554
3578
  }
@@ -3659,7 +3683,7 @@ const stripParenExpression = (node) => {
3659
3683
  };
3660
3684
  //#endregion
3661
3685
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
3662
- const MESSAGE$44 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
3686
+ const MESSAGE$46 = "A control must be associated with a text label — add visible text, `aria-label`, or `aria-labelledby`.";
3663
3687
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
3664
3688
  const DEFAULT_LABELLING_PROPS = [
3665
3689
  "alt",
@@ -3791,7 +3815,7 @@ const controlHasAssociatedLabel = defineRule({
3791
3815
  category: "Accessibility",
3792
3816
  create: (context) => {
3793
3817
  const settings = resolveSettings$46(context.settings);
3794
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
3818
+ const isTestlikeFile = isTestlikeFilename(context.filename);
3795
3819
  return { JSXElement(node) {
3796
3820
  if (isTestlikeFile) return;
3797
3821
  const opening = node.openingElement;
@@ -3819,7 +3843,7 @@ const controlHasAssociatedLabel = defineRule({
3819
3843
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
3820
3844
  context.report({
3821
3845
  node: opening,
3822
- message: MESSAGE$44
3846
+ message: MESSAGE$46
3823
3847
  });
3824
3848
  } };
3825
3849
  }
@@ -4143,14 +4167,21 @@ const isEs6Component = (node) => {
4143
4167
  };
4144
4168
  //#endregion
4145
4169
  //#region src/plugin/rules/react-builtins/display-name.ts
4146
- const MESSAGE$43 = "Component is missing a `displayName` — assign one for easier debugging.";
4170
+ const MESSAGE$45 = "Component is missing a `displayName` — assign one for easier debugging.";
4171
+ const DEFAULT_ADDITIONAL_HOCS = [
4172
+ "observer",
4173
+ "lazy",
4174
+ "withTracking"
4175
+ ];
4147
4176
  const resolveSettings$45 = (settings) => {
4148
4177
  const reactDoctor = settings?.["react-doctor"];
4149
4178
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
4179
+ const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
4150
4180
  return {
4151
4181
  ignoreTranspilerName: ruleSettings.ignoreTranspilerName ?? false,
4152
4182
  checkContextObjects: ruleSettings.checkContextObjects ?? false,
4153
- reactVersion: ruleSettings.reactVersion ?? ""
4183
+ reactVersion: ruleSettings.reactVersion ?? "",
4184
+ additionalHoCs
4154
4185
  };
4155
4186
  };
4156
4187
  const isReactVersionAtLeast$1 = (version, major, minor) => {
@@ -4223,29 +4254,28 @@ const isCreateContextCall = (node) => {
4223
4254
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
4224
4255
  return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
4225
4256
  };
4226
- const isObserverCall = (node) => {
4227
- if (!isNodeOfType(node, "CallExpression")) return false;
4228
- const callee = node.callee;
4229
- if (isNodeOfType(callee, "Identifier")) return callee.name === "observer";
4230
- return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "observer";
4231
- };
4232
- const getCallName = (node) => {
4233
- if (!isNodeOfType(node, "CallExpression")) return null;
4234
- const callee = node.callee;
4235
- if (isNodeOfType(callee, "Identifier")) return callee.name;
4236
- if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee);
4237
- return null;
4238
- };
4239
4257
  const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
4240
4258
  const firstCallArgument = (node) => {
4241
4259
  if (!isNodeOfType(node, "CallExpression")) return null;
4242
4260
  const first = node.arguments[0];
4243
4261
  return first ? first : null;
4244
4262
  };
4245
- const isDisplayNameHoC = (node) => {
4246
- const callName = getCallName(node);
4247
- return callName === "memo" || callName === "forwardRef";
4263
+ const resolveHoCCalleeName = (node, additionalHoCs) => {
4264
+ if (!isNodeOfType(node, "CallExpression")) return null;
4265
+ const callee = node.callee;
4266
+ if (isNodeOfType(callee, "Identifier")) {
4267
+ if (callee.name === "memo" || callee.name === "forwardRef") return callee.name;
4268
+ if (additionalHoCs.has(callee.name)) return callee.name;
4269
+ return null;
4270
+ }
4271
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
4272
+ const propertyName = callee.property.name;
4273
+ if (propertyName === "memo" || propertyName === "forwardRef") return propertyName;
4274
+ if (additionalHoCs.has(propertyName)) return propertyName;
4275
+ }
4276
+ return null;
4248
4277
  };
4278
+ const isDisplayNameHoC = (node, additionalHoCs) => resolveHoCCalleeName(node, additionalHoCs) !== null;
4249
4279
  const supportsComposedForwardRefDisplayName = (version) => {
4250
4280
  if (!version) return false;
4251
4281
  if (isReactVersionAtLeast$1(version, 15, 7)) return true;
@@ -4253,17 +4283,17 @@ const supportsComposedForwardRefDisplayName = (version) => {
4253
4283
  return Boolean(match && Number(match[1]) >= 11);
4254
4284
  };
4255
4285
  const shouldReportHoCDisplayName = (node, settings) => {
4256
- if (!isDisplayNameHoC(node)) return false;
4286
+ if (!isDisplayNameHoC(node, settings.additionalHoCs)) return false;
4257
4287
  if (!containsJsx$1(node)) return false;
4258
4288
  const assignedName = getAssignedName(node);
4259
4289
  const programRoot = findProgramRoot(node);
4260
4290
  if (assignedName && programRoot && hasDisplayNameAssignment(assignedName, programRoot)) return false;
4261
- const callName = getCallName(node);
4291
+ const callName = resolveHoCCalleeName(node, settings.additionalHoCs);
4262
4292
  const firstArgument = firstCallArgument(node);
4263
4293
  if (!firstArgument) return false;
4264
- if (callName === "forwardRef" && isNodeOfType(node.parent, "CallExpression") && getCallName(node.parent) === "memo" && firstCallArgument(node.parent) === node && supportsComposedForwardRefDisplayName(settings.reactVersion)) return false;
4294
+ if (callName === "forwardRef" && isNodeOfType(node.parent, "CallExpression") && resolveHoCCalleeName(node.parent, settings.additionalHoCs) === "memo" && firstCallArgument(node.parent) === node && supportsComposedForwardRefDisplayName(settings.reactVersion)) return false;
4265
4295
  if (callName === "memo" && isNodeOfType(firstArgument, "CallExpression")) {
4266
- if (getCallName(firstArgument) !== "forwardRef") return false;
4296
+ if (resolveHoCCalleeName(firstArgument, settings.additionalHoCs) !== "forwardRef") return false;
4267
4297
  return !supportsComposedForwardRefDisplayName(settings.reactVersion);
4268
4298
  }
4269
4299
  if (isNamedFunctionLike(firstArgument)) return false;
@@ -4339,7 +4369,7 @@ const displayName = defineRule({
4339
4369
  const reportAt = (node) => {
4340
4370
  context.report({
4341
4371
  node,
4342
- message: MESSAGE$43
4372
+ message: MESSAGE$45
4343
4373
  });
4344
4374
  };
4345
4375
  return {
@@ -4417,10 +4447,6 @@ const displayName = defineRule({
4417
4447
  reportAt(node);
4418
4448
  return;
4419
4449
  }
4420
- if (isObserverCall(node) && containsJsx$1(node)) {
4421
- reportAt(node);
4422
- return;
4423
- }
4424
4450
  if (shouldReportHoCDisplayName(node, settings)) {
4425
4451
  reportAt(node);
4426
4452
  return;
@@ -4462,7 +4488,7 @@ const displayName = defineRule({
4462
4488
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
4463
4489
  const walkInsideStatementBlocks = (node, visitor) => {
4464
4490
  if (!node || typeof node !== "object") return;
4465
- if (isFunctionLike$1(node)) return;
4491
+ if (isFunctionLike$2(node)) return;
4466
4492
  visitor(node);
4467
4493
  const nodeRecord = node;
4468
4494
  for (const key of Object.keys(nodeRecord)) {
@@ -4550,7 +4576,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4550
4576
  let didFindRelease = false;
4551
4577
  walkAst(node, (child) => {
4552
4578
  if (didFindRelease) return false;
4553
- if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
4579
+ if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
4554
4580
  if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
4555
4581
  didFindRelease = true;
4556
4582
  return false;
@@ -4559,7 +4585,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4559
4585
  return didFindRelease;
4560
4586
  };
4561
4587
  const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
4562
- if (!isFunctionLike$1(node)) return false;
4588
+ if (!isFunctionLike$2(node)) return false;
4563
4589
  return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
4564
4590
  };
4565
4591
  const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
@@ -4801,7 +4827,7 @@ const recordReference = (state, identifier, flag) => {
4801
4827
  };
4802
4828
  const isFunctionBodyBlock = (block) => {
4803
4829
  if (!block.parent) return false;
4804
- return isFunctionLike$1(block.parent);
4830
+ return isFunctionLike$2(block.parent);
4805
4831
  };
4806
4832
  const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
4807
4833
  const handleVariableDeclaration = (declaration, state) => {
@@ -4928,8 +4954,35 @@ const inferReferenceFlag = (identifier) => {
4928
4954
  const setNodeScope = (node, state) => {
4929
4955
  state.nodeScope.set(node, state.currentScope);
4930
4956
  };
4957
+ const walkParameterReferences = (pattern, state) => {
4958
+ if (isNodeOfType(pattern, "AssignmentPattern")) {
4959
+ walkParameterReferences(pattern.left, state);
4960
+ const defaultValue = pattern.right ?? null;
4961
+ if (defaultValue) walk(defaultValue, state);
4962
+ return;
4963
+ }
4964
+ if (isNodeOfType(pattern, "ObjectPattern")) {
4965
+ for (const property of pattern.properties) {
4966
+ const propertyNode = property;
4967
+ if (isNodeOfType(propertyNode, "RestElement")) {
4968
+ walkParameterReferences(propertyNode.argument, state);
4969
+ continue;
4970
+ }
4971
+ if (!isNodeOfType(propertyNode, "Property")) continue;
4972
+ const propertyDetail = propertyNode;
4973
+ if (propertyDetail.computed) walk(propertyDetail.key, state);
4974
+ walkParameterReferences(propertyDetail.value, state);
4975
+ }
4976
+ return;
4977
+ }
4978
+ if (isNodeOfType(pattern, "ArrayPattern")) {
4979
+ for (const element of pattern.elements) if (element) walkParameterReferences(element, state);
4980
+ return;
4981
+ }
4982
+ if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
4983
+ };
4931
4984
  const walk = (node, state) => {
4932
- if (isFunctionLike$1(node)) {
4985
+ if (isFunctionLike$2(node)) {
4933
4986
  if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
4934
4987
  setNodeScope(node, state);
4935
4988
  const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
@@ -4943,7 +4996,9 @@ const walk = (node, state) => {
4943
4996
  });
4944
4997
  tagAsBinding(state, node.id);
4945
4998
  }
4946
- handleFunctionParameters(node.params ?? [], fnScope, state);
4999
+ const functionParams = node.params ?? [];
5000
+ handleFunctionParameters(functionParams, fnScope, state);
5001
+ for (const param of functionParams) walkParameterReferences(param, state);
4947
5002
  const body = node.body;
4948
5003
  if (body) walk(body, state);
4949
5004
  popScope(state);
@@ -4985,6 +5040,7 @@ const walk = (node, state) => {
4985
5040
  const nodeRecord = node;
4986
5041
  for (const key of Object.keys(nodeRecord)) {
4987
5042
  if (key === "parent") continue;
5043
+ if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
4988
5044
  const child = nodeRecord[key];
4989
5045
  if (Array.isArray(child)) {
4990
5046
  for (const item of child) if (isAstNode(item)) walk(item, state);
@@ -5047,6 +5103,7 @@ const walk = (node, state) => {
5047
5103
  const nodeRecord = node;
5048
5104
  for (const key of Object.keys(nodeRecord)) {
5049
5105
  if (key === "parent") continue;
5106
+ if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
5050
5107
  const child = nodeRecord[key];
5051
5108
  if (Array.isArray(child)) {
5052
5109
  for (const item of child) if (isAstNode(item)) walk(item, state);
@@ -5166,20 +5223,12 @@ const isAstDescendant = (inner, outer) => {
5166
5223
  };
5167
5224
  //#endregion
5168
5225
  //#region src/plugin/semantic/closure-captures.ts
5169
- const TYPE_ONLY_CHILD_KEYS = new Set([
5170
- "implements",
5171
- "returnType",
5172
- "superTypeArguments",
5173
- "typeAnnotation",
5174
- "typeArguments",
5175
- "typeParameters"
5176
- ]);
5177
5226
  const closureCaptures = (functionNode, scopes) => {
5178
5227
  const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
5179
5228
  const out = [];
5180
5229
  const seen = /* @__PURE__ */ new Set();
5181
5230
  const visit = (node) => {
5182
- if (node !== functionNode && isFunctionLike$1(node)) {
5231
+ if (node !== functionNode && isFunctionLike$2(node)) {
5183
5232
  const innerCaptures = closureCaptures(node, scopes);
5184
5233
  for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
5185
5234
  if (!seen.has(reference.id)) {
@@ -5201,7 +5250,7 @@ const closureCaptures = (functionNode, scopes) => {
5201
5250
  const record = node;
5202
5251
  for (const key of Object.keys(record)) {
5203
5252
  if (key === "parent") continue;
5204
- if (TYPE_ONLY_CHILD_KEYS.has(key)) continue;
5253
+ if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
5205
5254
  const child = record[key];
5206
5255
  if (Array.isArray(child)) {
5207
5256
  for (const item of child) if (isAstNode(item)) visit(item);
@@ -5251,7 +5300,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
5251
5300
  "ParenthesizedExpression",
5252
5301
  "ChainExpression"
5253
5302
  ]);
5254
- const unwrapExpression = (node) => {
5303
+ const unwrapExpression$1 = (node) => {
5255
5304
  let current = node;
5256
5305
  while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
5257
5306
  const inner = current.expression;
@@ -5358,7 +5407,7 @@ const symbolHasStableHookOrigin = (symbol) => {
5358
5407
  if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
5359
5408
  const initializerRaw = declarator.init;
5360
5409
  if (!initializerRaw) return false;
5361
- const initializer = unwrapExpression(initializerRaw);
5410
+ const initializer = unwrapExpression$1(initializerRaw);
5362
5411
  if (symbol.kind === "const") {
5363
5412
  if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
5364
5413
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
@@ -5378,13 +5427,13 @@ const symbolHasStableHookOrigin = (symbol) => {
5378
5427
  return false;
5379
5428
  };
5380
5429
  const symbolHasUseEffectEventOrigin = (symbol) => {
5381
- const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
5430
+ const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
5382
5431
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
5383
5432
  return getHookName(initializer.callee) === "useEffectEvent";
5384
5433
  };
5385
5434
  const getFunctionValueNode = (symbol) => {
5386
5435
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
5387
- const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
5436
+ const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
5388
5437
  if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
5389
5438
  return null;
5390
5439
  };
@@ -5519,23 +5568,23 @@ const computeDepKey = (reference) => {
5519
5568
  return fullName;
5520
5569
  };
5521
5570
  const computeDeclaredDepKey = (entry) => {
5522
- const stripped = unwrapExpression(entry);
5571
+ const stripped = unwrapExpression$1(entry);
5523
5572
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
5524
5573
  if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
5525
5574
  return null;
5526
5575
  };
5527
5576
  const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
5528
5577
  if (!depsArgument) return false;
5529
- const strippedDepsArgument = unwrapExpression(depsArgument);
5578
+ const strippedDepsArgument = unwrapExpression$1(depsArgument);
5530
5579
  if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
5531
5580
  return strippedDepsArgument.elements.some((element) => {
5532
5581
  if (!element) return false;
5533
- const strippedElement = unwrapExpression(element);
5582
+ const strippedElement = unwrapExpression$1(element);
5534
5583
  return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
5535
5584
  });
5536
5585
  };
5537
5586
  const stringifyMemberChain = (node) => {
5538
- const stripped = unwrapExpression(node);
5587
+ const stripped = unwrapExpression$1(node);
5539
5588
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
5540
5589
  if (isNodeOfType(stripped, "ThisExpression")) return "this";
5541
5590
  if (isNodeOfType(stripped, "MemberExpression")) {
@@ -5564,33 +5613,6 @@ const collectCaptureDepKeys = (callback, scopes) => {
5564
5613
  if (!depKey) continue;
5565
5614
  keys.add(depKey);
5566
5615
  }
5567
- const functionParams = callback.params ?? [];
5568
- for (const param of functionParams) {
5569
- if (!isNodeOfType(param, "AssignmentPattern")) continue;
5570
- const visitDefaultValue = (node) => {
5571
- if (isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression")) {
5572
- const depKey = stringifyMemberChain(node);
5573
- if (depKey) keys.add(depKey);
5574
- }
5575
- const reference = scopes.referenceFor(node);
5576
- if (reference?.resolvedSymbol) {
5577
- const symbol = reference.resolvedSymbol;
5578
- if (!isOutsideAllFunctions(symbol)) {
5579
- const depKey = computeDepKey(reference);
5580
- if (depKey) keys.add(depKey);
5581
- }
5582
- }
5583
- const record = node;
5584
- for (const key of Object.keys(record)) {
5585
- if (key === "parent") continue;
5586
- const child = record[key];
5587
- if (Array.isArray(child)) {
5588
- for (const item of child) if (isAstNode(item)) visitDefaultValue(item);
5589
- } else if (isAstNode(child)) visitDefaultValue(child);
5590
- }
5591
- };
5592
- visitDefaultValue(param.right);
5593
- }
5594
5616
  return {
5595
5617
  keys,
5596
5618
  stableCapturedNames
@@ -5604,13 +5626,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
5604
5626
  return false;
5605
5627
  };
5606
5628
  const getMemberRootIdentifier = (node) => {
5607
- const stripped = unwrapExpression(node);
5629
+ const stripped = unwrapExpression$1(node);
5608
5630
  if (isNodeOfType(stripped, "Identifier")) return stripped;
5609
5631
  if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
5610
5632
  return null;
5611
5633
  };
5612
5634
  const hasComputedMemberExpression = (node) => {
5613
- const stripped = unwrapExpression(node);
5635
+ const stripped = unwrapExpression$1(node);
5614
5636
  if (!isNodeOfType(stripped, "MemberExpression")) return false;
5615
5637
  if (stripped.computed) return true;
5616
5638
  return hasComputedMemberExpression(stripped.object);
@@ -5631,7 +5653,7 @@ const isRegExpLiteral = (node) => {
5631
5653
  };
5632
5654
  const isUnstableInitializer = (node) => {
5633
5655
  if (!node) return false;
5634
- const stripped = unwrapExpression(node);
5656
+ const stripped = unwrapExpression$1(node);
5635
5657
  if (isRegExpLiteral(stripped)) return true;
5636
5658
  if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
5637
5659
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
@@ -5787,7 +5809,7 @@ const hasMemberCallForRoot = (node, rootName) => {
5787
5809
  const visit = (current) => {
5788
5810
  if (didFindMemberCall) return;
5789
5811
  if (isNodeOfType(current, "CallExpression")) {
5790
- if (getMemberRootIdentifier(unwrapExpression(current.callee))?.name === rootName) {
5812
+ if (getMemberRootIdentifier(unwrapExpression$1(current.callee))?.name === rootName) {
5791
5813
  didFindMemberCall = true;
5792
5814
  return;
5793
5815
  }
@@ -5849,7 +5871,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5849
5871
  return;
5850
5872
  }
5851
5873
  const depsArgumentRaw = node.arguments[depsArgumentIndex];
5852
- const callbackExpression = unwrapExpression(callbackArgument);
5874
+ const callbackExpression = unwrapExpression$1(callbackArgument);
5853
5875
  let callbackToAnalyze = null;
5854
5876
  const forcedCaptureKeys = /* @__PURE__ */ new Set();
5855
5877
  if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
@@ -5857,7 +5879,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5857
5879
  const callbackSymbol = context.scopes.symbolFor(callbackExpression);
5858
5880
  const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
5859
5881
  if (functionValueNode) callbackToAnalyze = functionValueNode;
5860
- else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
5882
+ else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$1(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
5861
5883
  else if (depsArgumentRaw) {
5862
5884
  if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
5863
5885
  context.report({
@@ -5914,7 +5936,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5914
5936
  });
5915
5937
  return;
5916
5938
  }
5917
- const depsArgument = unwrapExpression(depsArgumentRaw);
5939
+ const depsArgument = unwrapExpression$1(depsArgumentRaw);
5918
5940
  if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
5919
5941
  if (isAutoDependenciesHook(hookName)) return;
5920
5942
  if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
@@ -5955,11 +5977,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
5955
5977
  for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
5956
5978
  const hasLiteralDepElement = depsArgument.elements.some((element) => {
5957
5979
  if (!element) return false;
5958
- return isLiteralOrEmptyTemplate(unwrapExpression(element));
5980
+ return isLiteralOrEmptyTemplate(unwrapExpression$1(element));
5959
5981
  });
5960
5982
  const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
5961
5983
  if (!element) return false;
5962
- return isNonStringLiteral(unwrapExpression(element));
5984
+ return isNonStringLiteral(unwrapExpression$1(element));
5963
5985
  });
5964
5986
  if (hasNonStringLiteralDep) context.report({
5965
5987
  node: depsArgument,
@@ -5980,7 +6002,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5980
6002
  });
5981
6003
  continue;
5982
6004
  }
5983
- const stripped = unwrapExpression(elementNode);
6005
+ const stripped = unwrapExpression$1(elementNode);
5984
6006
  if (isLiteralOrEmptyTemplate(stripped)) continue;
5985
6007
  if (isNodeOfType(stripped, "Identifier")) {
5986
6008
  const depSymbol = context.scopes.symbolFor(stripped);
@@ -6200,7 +6222,7 @@ const flattenJsxName$1 = (name) => {
6200
6222
  return "";
6201
6223
  };
6202
6224
  const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
6203
- const buildMessage$23 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
6225
+ const buildMessage$25 = (propName, message) => message ?? `Prop \`${propName}\` is forbidden on this component.`;
6204
6226
  const forbidComponentProps = defineRule({
6205
6227
  id: "forbid-component-props",
6206
6228
  severity: "warn",
@@ -6226,7 +6248,7 @@ const forbidComponentProps = defineRule({
6226
6248
  if (!isForbiddenForTag(entry, tag)) continue;
6227
6249
  context.report({
6228
6250
  node: attribute,
6229
- message: buildMessage$23(propName, entry.message)
6251
+ message: buildMessage$25(propName, entry.message)
6230
6252
  });
6231
6253
  break;
6232
6254
  }
@@ -6236,7 +6258,7 @@ const forbidComponentProps = defineRule({
6236
6258
  });
6237
6259
  //#endregion
6238
6260
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6239
- const buildMessage$22 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
6261
+ const buildMessage$24 = (propName, customMessage) => customMessage ?? `Prop \`${propName}\` is forbidden on DOM nodes.`;
6240
6262
  const resolveSettings$43 = (settings) => {
6241
6263
  const reactDoctor = settings?.["react-doctor"];
6242
6264
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
@@ -6274,7 +6296,7 @@ const forbidDomProps = defineRule({
6274
6296
  if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
6275
6297
  context.report({
6276
6298
  node: attribute.name,
6277
- message: buildMessage$22(propName, descriptor.message)
6299
+ message: buildMessage$24(propName, descriptor.message)
6278
6300
  });
6279
6301
  }
6280
6302
  } };
@@ -6344,7 +6366,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6344
6366
  };
6345
6367
  //#endregion
6346
6368
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6347
- const buildMessage$21 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
6369
+ const buildMessage$23 = (element, customHelp) => customHelp ? `<${element}> is forbidden — ${customHelp}` : `<${element}> is forbidden.`;
6348
6370
  const resolveSettings$42 = (settings) => {
6349
6371
  const reactDoctor = settings?.["react-doctor"];
6350
6372
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
@@ -6368,7 +6390,7 @@ const forbidElements = defineRule({
6368
6390
  if (!fullName || !forbidMap.has(fullName)) return;
6369
6391
  context.report({
6370
6392
  node: node.name,
6371
- message: buildMessage$21(fullName, forbidMap.get(fullName))
6393
+ message: buildMessage$23(fullName, forbidMap.get(fullName))
6372
6394
  });
6373
6395
  },
6374
6396
  CallExpression(node) {
@@ -6388,7 +6410,7 @@ const forbidElements = defineRule({
6388
6410
  if (!elementName || !forbidMap.has(elementName)) return;
6389
6411
  context.report({
6390
6412
  node: firstArgument,
6391
- message: buildMessage$21(elementName, forbidMap.get(elementName))
6413
+ message: buildMessage$23(elementName, forbidMap.get(elementName))
6392
6414
  });
6393
6415
  }
6394
6416
  };
@@ -6396,7 +6418,7 @@ const forbidElements = defineRule({
6396
6418
  });
6397
6419
  //#endregion
6398
6420
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
6399
- const MESSAGE$42 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
6421
+ const MESSAGE$44 = "Components wrapped with `forwardRef` must accept a `ref` parameter — drop `forwardRef` if you don't need a ref.";
6400
6422
  const forwardRefUsesRef = defineRule({
6401
6423
  id: "forward-ref-uses-ref",
6402
6424
  severity: "warn",
@@ -6415,13 +6437,13 @@ const forwardRefUsesRef = defineRule({
6415
6437
  if (isNodeOfType(onlyParam, "RestElement")) return;
6416
6438
  context.report({
6417
6439
  node: inner,
6418
- message: MESSAGE$42
6440
+ message: MESSAGE$44
6419
6441
  });
6420
6442
  } })
6421
6443
  });
6422
6444
  //#endregion
6423
6445
  //#region src/plugin/rules/a11y/heading-has-content.ts
6424
- const MESSAGE$41 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
6446
+ const MESSAGE$43 = "Heading elements must contain accessible text content (or `aria-label` / `aria-labelledby`).";
6425
6447
  const DEFAULT_HEADING_TAGS = [
6426
6448
  "h1",
6427
6449
  "h2",
@@ -6453,7 +6475,7 @@ const headingHasContent = defineRule({
6453
6475
  if (isHiddenFromScreenReader(node, context.settings)) return;
6454
6476
  context.report({
6455
6477
  node,
6456
- message: MESSAGE$41
6478
+ message: MESSAGE$43
6457
6479
  });
6458
6480
  } };
6459
6481
  }
@@ -6589,7 +6611,7 @@ const hooksNoNanInDeps = defineRule({
6589
6611
  });
6590
6612
  //#endregion
6591
6613
  //#region src/plugin/rules/a11y/html-has-lang.ts
6592
- const MESSAGE$40 = "`<html>` element must have a non-empty `lang` attribute.";
6614
+ const MESSAGE$42 = "`<html>` element must have a non-empty `lang` attribute.";
6593
6615
  const resolveSettings$39 = (settings) => {
6594
6616
  const reactDoctor = settings?.["react-doctor"];
6595
6617
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -6636,7 +6658,7 @@ const htmlHasLang = defineRule({
6636
6658
  if (!lang) {
6637
6659
  context.report({
6638
6660
  node: node.name,
6639
- message: MESSAGE$40
6661
+ message: MESSAGE$42
6640
6662
  });
6641
6663
  return;
6642
6664
  }
@@ -6644,13 +6666,13 @@ const htmlHasLang = defineRule({
6644
6666
  if (verdict === "missing" || verdict === "empty") {
6645
6667
  context.report({
6646
6668
  node: lang,
6647
- message: MESSAGE$40
6669
+ message: MESSAGE$42
6648
6670
  });
6649
6671
  return;
6650
6672
  }
6651
6673
  if (hasSpread && !lang) context.report({
6652
6674
  node: node.name,
6653
- message: MESSAGE$40
6675
+ message: MESSAGE$42
6654
6676
  });
6655
6677
  } };
6656
6678
  }
@@ -6690,7 +6712,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
6690
6712
  "table",
6691
6713
  "ul"
6692
6714
  ]);
6693
- const buildMessage$20 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
6715
+ const buildMessage$22 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
6694
6716
  const isParagraphElement = (candidate) => {
6695
6717
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6696
6718
  const opening = candidate.openingElement;
@@ -6718,7 +6740,7 @@ const htmlNoInvalidParagraphChild = defineRule({
6718
6740
  if (!findEnclosingParagraph(node)) return;
6719
6741
  context.report({
6720
6742
  node: node.name,
6721
- message: buildMessage$20(childTagName)
6743
+ message: buildMessage$22(childTagName)
6722
6744
  });
6723
6745
  } })
6724
6746
  });
@@ -6738,7 +6760,7 @@ const ROW_GROUPS = new Set([
6738
6760
  "tbody",
6739
6761
  "tfoot"
6740
6762
  ]);
6741
- const buildMessage$19 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
6763
+ const buildMessage$21 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
6742
6764
  const buildNestedTableMessage = () => "Improper table nesting — `<table>` cannot be a direct descendant of another table element. Tables can only nest inside a `<td>` or `<th>` cell of an outer table.";
6743
6765
  const getHostTagName = (jsxElement) => {
6744
6766
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
@@ -6806,28 +6828,28 @@ const htmlNoInvalidTableNesting = defineRule({
6806
6828
  if (ROW_GROUPS.has(tagName)) {
6807
6829
  if (actualParent !== "table") context.report({
6808
6830
  node: node.openingElement.name,
6809
- message: buildMessage$19(tagName, "`<table>`", actualParent)
6831
+ message: buildMessage$21(tagName, "`<table>`", actualParent)
6810
6832
  });
6811
6833
  return;
6812
6834
  }
6813
6835
  if (tagName === "tr") {
6814
6836
  if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
6815
6837
  node: node.openingElement.name,
6816
- message: buildMessage$19(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6838
+ message: buildMessage$21(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6817
6839
  });
6818
6840
  return;
6819
6841
  }
6820
6842
  if (tagName === "td" || tagName === "th") {
6821
6843
  if (actualParent !== "tr") context.report({
6822
6844
  node: node.openingElement.name,
6823
- message: buildMessage$19(tagName, "`<tr>`", actualParent)
6845
+ message: buildMessage$21(tagName, "`<tr>`", actualParent)
6824
6846
  });
6825
6847
  }
6826
6848
  } })
6827
6849
  });
6828
6850
  //#endregion
6829
6851
  //#region src/plugin/rules/correctness/html-no-nested-interactive.ts
6830
- const buildMessage$18 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
6852
+ const buildMessage$20 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
6831
6853
  const isJsxElementWithTagName = (candidate, tagName) => {
6832
6854
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6833
6855
  const opening = candidate.openingElement;
@@ -6855,13 +6877,13 @@ const htmlNoNestedInteractive = defineRule({
6855
6877
  if (!findEnclosingSameTag(node, tagName)) return;
6856
6878
  context.report({
6857
6879
  node: node.name,
6858
- message: buildMessage$18(tagName)
6880
+ message: buildMessage$20(tagName)
6859
6881
  });
6860
6882
  } })
6861
6883
  });
6862
6884
  //#endregion
6863
6885
  //#region src/plugin/rules/a11y/iframe-has-title.ts
6864
- const MESSAGE$39 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
6886
+ const MESSAGE$41 = "`<iframe>` element must have a non-empty `title` attribute for assistive technology.";
6865
6887
  const evaluateTitleValue = (value) => {
6866
6888
  if (!value) return "missing";
6867
6889
  if (isNodeOfType(value, "Literal")) {
@@ -6900,14 +6922,14 @@ const iframeHasTitle = defineRule({
6900
6922
  if (!titleAttr) {
6901
6923
  if (hasSpread || tag === "iframe") context.report({
6902
6924
  node: node.name,
6903
- message: MESSAGE$39
6925
+ message: MESSAGE$41
6904
6926
  });
6905
6927
  return;
6906
6928
  }
6907
6929
  const verdict = evaluateTitleValue(titleAttr.value);
6908
6930
  if (verdict === "missing" || verdict === "empty") context.report({
6909
6931
  node: titleAttr,
6910
- message: MESSAGE$39
6932
+ message: MESSAGE$41
6911
6933
  });
6912
6934
  } })
6913
6935
  });
@@ -7010,7 +7032,7 @@ const iframeMissingSandbox = defineRule({
7010
7032
  });
7011
7033
  //#endregion
7012
7034
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
7013
- const MESSAGE$38 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
7035
+ const MESSAGE$40 = "`alt` text contains redundant words like \"image\" / \"photo\" / \"picture\" — describe the content instead.";
7014
7036
  const DEFAULT_COMPONENTS = ["img"];
7015
7037
  const DEFAULT_REDUNDANT_WORDS = [
7016
7038
  "image",
@@ -7072,7 +7094,7 @@ const imgRedundantAlt = defineRule({
7072
7094
  if (!altAttribute) return;
7073
7095
  if (altValueRedundant(altAttribute, settings.words)) context.report({
7074
7096
  node: altAttribute,
7075
- message: MESSAGE$38
7097
+ message: MESSAGE$40
7076
7098
  });
7077
7099
  } };
7078
7100
  }
@@ -7267,7 +7289,7 @@ const isAtomFromJotai = (callExpression) => {
7267
7289
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
7268
7290
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
7269
7291
  };
7270
- const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7292
+ const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7271
7293
  const getFirstParameterName = (fn) => {
7272
7294
  const parameters = fn.params ?? [];
7273
7295
  if (parameters.length !== 1) return null;
@@ -7385,7 +7407,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7385
7407
  const args = node.arguments ?? [];
7386
7408
  if (args.length === 0) return;
7387
7409
  const reader = args[0];
7388
- if (!isFunctionLike(reader)) return;
7410
+ if (!isFunctionLike$1(reader)) return;
7389
7411
  const getParameterName = getFirstParameterName(reader);
7390
7412
  if (!getParameterName) return;
7391
7413
  const freshReturn = getFreshReturnForFunction(reader);
@@ -7401,7 +7423,6 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7401
7423
  //#endregion
7402
7424
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
7403
7425
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
7404
- const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
7405
7426
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
7406
7427
  const HOOK_NAME_PATTERN = /^use[A-Z]/;
7407
7428
  const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
@@ -7639,7 +7660,7 @@ const isInsideLoopContext = (node) => {
7639
7660
  let current = node.parent;
7640
7661
  while (current) {
7641
7662
  if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
7642
- if (isFunctionLike$1(current)) {
7663
+ if (isFunctionLike$2(current)) {
7643
7664
  if (isIteratorCallback(current)) return true;
7644
7665
  return false;
7645
7666
  }
@@ -7993,7 +8014,7 @@ const jsHoistIntl = defineRule({
7993
8014
  let cursor = node.parent ?? null;
7994
8015
  let inFunctionBody = false;
7995
8016
  while (cursor) {
7996
- if (isFunctionLike$1(cursor)) {
8017
+ if (isFunctionLike$2(cursor)) {
7997
8018
  inFunctionBody = true;
7998
8019
  const fnParent = cursor.parent;
7999
8020
  if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
@@ -8423,6 +8444,7 @@ const jsTosortedImmutable = defineRule({
8423
8444
  id: "js-tosorted-immutable",
8424
8445
  tags: ["test-noise"],
8425
8446
  severity: "warn",
8447
+ disabledBy: ["react-native"],
8426
8448
  recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` for immutable sorting without the spread allocation",
8427
8449
  create: (context) => ({ CallExpression(node) {
8428
8450
  if (!isMemberProperty(node.callee, "sort")) return;
@@ -8711,7 +8733,7 @@ const jsxFilenameExtension = defineRule({
8711
8733
  const settings = resolveSettings$34(context.settings);
8712
8734
  const allowedExtensions = normalizeExtensions(settings.extensions);
8713
8735
  const allowedList = [...allowedExtensions].map((extension) => `.${extension}`).join(", ");
8714
- const filename = context.getFilename ? normalizeFilename$1(context.getFilename()) : "fixture.tsx";
8736
+ const filename = normalizeFilename$1(context.filename ?? "fixture.tsx");
8715
8737
  const extensionOnly = path.extname(filename).slice(1);
8716
8738
  const fileHasAllowedExtension = allowedExtensions.has(extensionOnly);
8717
8739
  let didReportMismatch = false;
@@ -9337,7 +9359,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
9337
9359
  };
9338
9360
  //#endregion
9339
9361
  //#region src/plugin/rules/react-builtins/jsx-max-depth.ts
9340
- const buildMessage$17 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
9362
+ const buildMessage$19 = (depth, max) => `JSX nesting depth ${depth} exceeds maximum ${max}.`;
9341
9363
  const DEFAULT_MAX_DEPTH = 14;
9342
9364
  const resolveSettings$30 = (settings) => {
9343
9365
  const reactDoctor = settings?.["react-doctor"];
@@ -9404,7 +9426,7 @@ const jsxMaxDepth = defineRule({
9404
9426
  const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
9405
9427
  if (total > max) context.report({
9406
9428
  node,
9407
- message: buildMessage$17(total, max)
9429
+ message: buildMessage$19(total, max)
9408
9430
  });
9409
9431
  };
9410
9432
  return {
@@ -9419,7 +9441,7 @@ const jsxMaxDepth = defineRule({
9419
9441
  });
9420
9442
  //#endregion
9421
9443
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
9422
- const MESSAGE$37 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
9444
+ const MESSAGE$39 = "Comment-like text in JSX must live inside `{/* … */}` — bare `//` or `/*` becomes literal text.";
9423
9445
  const LITERAL_TEXT_TAGS = new Set([
9424
9446
  "code",
9425
9447
  "pre",
@@ -9454,11 +9476,20 @@ const jsxNoCommentTextnodes = defineRule({
9454
9476
  if (isInsideLiteralTextTag(node)) return;
9455
9477
  context.report({
9456
9478
  node,
9457
- message: MESSAGE$37
9479
+ message: MESSAGE$39
9458
9480
  });
9459
9481
  } })
9460
9482
  });
9461
9483
  //#endregion
9484
+ //#region src/plugin/utils/is-canonical-react-namespace-name.ts
9485
+ const isCanonicalReactNamespaceName = (namespaceName) => {
9486
+ if (namespaceName === "React") return true;
9487
+ if (namespaceName === "react") return true;
9488
+ if (namespaceName.startsWith("_react")) return true;
9489
+ if (namespaceName.startsWith("_React")) return true;
9490
+ return false;
9491
+ };
9492
+ //#endregion
9462
9493
  //#region src/plugin/utils/is-inside-function-scope.ts
9463
9494
  const FUNCTION_NODE_TYPES$1 = new Set([
9464
9495
  "FunctionDeclaration",
@@ -9476,7 +9507,12 @@ const isInsideFunctionScope = (node) => {
9476
9507
  };
9477
9508
  //#endregion
9478
9509
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
9479
- const MESSAGE$36 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
9510
+ const MESSAGE$38 = "Context `value` prop is constructed inline — wrap with `useMemo`/`useCallback` or hoist a constant to avoid re-renders.";
9511
+ const CONTEXT_MODULES$1 = [
9512
+ "react",
9513
+ "use-context-selector",
9514
+ "react-tracked"
9515
+ ];
9480
9516
  const isConstructedValue = (expression) => {
9481
9517
  const stripped = stripParenExpression(expression);
9482
9518
  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;
@@ -9484,10 +9520,57 @@ const isConstructedValue = (expression) => {
9484
9520
  if (isNodeOfType(stripped, "LogicalExpression")) return isConstructedValue(stripped.left) || isConstructedValue(stripped.right);
9485
9521
  return false;
9486
9522
  };
9487
- const isProviderName = (node) => {
9523
+ const isProviderMemberName = (node) => {
9488
9524
  if (!isNodeOfType(node, "JSXMemberExpression")) return false;
9489
9525
  return node.property.name === "Provider";
9490
9526
  };
9527
+ const isCreateContextCallExpression = (expression) => {
9528
+ const stripped = stripParenExpression(expression);
9529
+ if (!isNodeOfType(stripped, "CallExpression")) return false;
9530
+ const callee = stripped.callee;
9531
+ if (isNodeOfType(callee, "Identifier")) {
9532
+ for (const moduleName of CONTEXT_MODULES$1) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
9533
+ return false;
9534
+ }
9535
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
9536
+ const namespaceIdentifier = callee.object;
9537
+ const propertyIdentifier = callee.property;
9538
+ if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
9539
+ if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
9540
+ if (propertyIdentifier.name !== "createContext") return false;
9541
+ if (isCanonicalReactNamespaceName(namespaceIdentifier.name)) return true;
9542
+ for (const moduleName of CONTEXT_MODULES$1) {
9543
+ if (getImportedNameFromModule(namespaceIdentifier, namespaceIdentifier.name, moduleName) === null) continue;
9544
+ return true;
9545
+ }
9546
+ }
9547
+ return false;
9548
+ };
9549
+ const collectContextBindings = (programRoot) => {
9550
+ const bindings = /* @__PURE__ */ new Set();
9551
+ if (!isNodeOfType(programRoot, "Program")) return bindings;
9552
+ for (const topLevel of programRoot.body ?? []) {
9553
+ let declaration = topLevel;
9554
+ if (isNodeOfType(topLevel, "ExportNamedDeclaration") && topLevel.declaration) declaration = topLevel.declaration;
9555
+ if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) continue;
9556
+ for (const declarator of declaration.declarations ?? []) {
9557
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
9558
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
9559
+ if (!declarator.init) continue;
9560
+ if (!isAstNode(declarator.init)) continue;
9561
+ if (!isCreateContextCallExpression(declarator.init)) continue;
9562
+ bindings.add(declarator.id.name);
9563
+ }
9564
+ }
9565
+ return bindings;
9566
+ };
9567
+ const isCreateContextBindingJsxName = (node, contextBindings) => {
9568
+ if (!isNodeOfType(node, "JSXIdentifier")) return false;
9569
+ if (!contextBindings.has(node.name)) return false;
9570
+ const binding = findVariableInitializer(node, node.name);
9571
+ if (!binding) return false;
9572
+ return binding.scopeOwner.type === "Program";
9573
+ };
9491
9574
  const jsxNoConstructedContextValues = defineRule({
9492
9575
  id: "jsx-no-constructed-context-values",
9493
9576
  tags: ["react-jsx-only"],
@@ -9495,27 +9578,36 @@ const jsxNoConstructedContextValues = defineRule({
9495
9578
  recommendation: "Memoize the context value (`useMemo`) or hoist it outside the render.",
9496
9579
  category: "Performance",
9497
9580
  create: (context) => {
9498
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
9499
- return { JSXOpeningElement(node) {
9500
- if (isTestlikeFile) return;
9501
- if (!isProviderName(node.name)) return;
9502
- if (!isInsideFunctionScope(node)) return;
9503
- for (const attribute of node.attributes) {
9504
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9505
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
9506
- if (attribute.name.name !== "value") continue;
9507
- const attributeValue = attribute.value;
9508
- if (!attributeValue) continue;
9509
- if (!isNodeOfType(attributeValue, "JSXExpressionContainer")) continue;
9510
- const innerExpression = attributeValue.expression;
9511
- if (!innerExpression || innerExpression.type === "JSXEmptyExpression") continue;
9512
- if (!isConstructedValue(innerExpression)) continue;
9513
- context.report({
9514
- node: attribute,
9515
- message: MESSAGE$36
9516
- });
9581
+ const isTestlikeFile = isTestlikeFilename(context.filename);
9582
+ let contextBindings = /* @__PURE__ */ new Set();
9583
+ return {
9584
+ Program(node) {
9585
+ contextBindings = collectContextBindings(node);
9586
+ },
9587
+ JSXOpeningElement(node) {
9588
+ if (isTestlikeFile) return;
9589
+ const nameNode = node.name;
9590
+ const isLegacyProvider = isProviderMemberName(nameNode);
9591
+ const isReact19Shorthand = isCreateContextBindingJsxName(nameNode, contextBindings);
9592
+ if (!isLegacyProvider && !isReact19Shorthand) return;
9593
+ if (!isInsideFunctionScope(node)) return;
9594
+ for (const attribute of node.attributes) {
9595
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9596
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
9597
+ if (attribute.name.name !== "value") continue;
9598
+ const attributeValue = attribute.value;
9599
+ if (!attributeValue) continue;
9600
+ if (!isNodeOfType(attributeValue, "JSXExpressionContainer")) continue;
9601
+ const innerExpression = attributeValue.expression;
9602
+ if (!innerExpression || innerExpression.type === "JSXEmptyExpression") continue;
9603
+ if (!isConstructedValue(innerExpression)) continue;
9604
+ context.report({
9605
+ node: attribute,
9606
+ message: MESSAGE$38
9607
+ });
9608
+ }
9517
9609
  }
9518
- } };
9610
+ };
9519
9611
  }
9520
9612
  });
9521
9613
  //#endregion
@@ -9541,7 +9633,8 @@ const jsxNoDuplicateProps = defineRule({
9541
9633
  //#endregion
9542
9634
  //#region src/plugin/utils/build-same-file-memo-registry.ts
9543
9635
  const HOC_NAMES_FOR_MEMOISATION = new Set([
9544
- ...REACT_HOC_NAMES,
9636
+ "memo",
9637
+ "React.memo",
9545
9638
  "observer",
9546
9639
  "observable",
9547
9640
  "lazy",
@@ -9595,7 +9688,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
9595
9688
  };
9596
9689
  //#endregion
9597
9690
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
9598
- const MESSAGE$35 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
9691
+ const MESSAGE$37 = "JSX prop receives JSX created on every render — extract it or memoize to avoid re-renders.";
9599
9692
  const KNOWN_SLOT_PROP_NAMES = new Set([
9600
9693
  "icon",
9601
9694
  "Icon",
@@ -9841,7 +9934,7 @@ const jsxNoJsxAsProp = defineRule({
9841
9934
  recommendation: "Hoist the inner JSX outside the render or memoize via `useMemo`.",
9842
9935
  category: "Performance",
9843
9936
  create: (context) => {
9844
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
9937
+ const isTestlikeFile = isTestlikeFilename(context.filename);
9845
9938
  let memoRegistry = null;
9846
9939
  return {
9847
9940
  Program(node) {
@@ -9863,7 +9956,7 @@ const jsxNoJsxAsProp = defineRule({
9863
9956
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
9864
9957
  context.report({
9865
9958
  node,
9866
- message: MESSAGE$35
9959
+ message: MESSAGE$37
9867
9960
  });
9868
9961
  }
9869
9962
  };
@@ -10151,7 +10244,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
10151
10244
  ];
10152
10245
  //#endregion
10153
10246
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
10154
- const MESSAGE$34 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
10247
+ const MESSAGE$36 = "JSX prop receives a new Array on every render — extract it or memoize to avoid re-renders.";
10155
10248
  const isDataArrayPropName = (propName) => {
10156
10249
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
10157
10250
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -10212,7 +10305,7 @@ const jsxNoNewArrayAsProp = defineRule({
10212
10305
  recommendation: "Memoize the array (`useMemo`) or hoist it outside the component.",
10213
10306
  category: "Performance",
10214
10307
  create: (context) => {
10215
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
10308
+ const isTestlikeFile = isTestlikeFilename(context.filename);
10216
10309
  let memoRegistry = null;
10217
10310
  return {
10218
10311
  Program(node) {
@@ -10234,7 +10327,7 @@ const jsxNoNewArrayAsProp = defineRule({
10234
10327
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
10235
10328
  context.report({
10236
10329
  node,
10237
- message: MESSAGE$34
10330
+ message: MESSAGE$36
10238
10331
  });
10239
10332
  }
10240
10333
  };
@@ -10492,7 +10585,7 @@ const SAFE_RECEIVER_NAMES = new Set([
10492
10585
  ]);
10493
10586
  //#endregion
10494
10587
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
10495
- const MESSAGE$33 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
10588
+ const MESSAGE$35 = "JSX prop receives a new Function on every render — extract it or memoize (`useCallback`) to avoid re-renders.";
10496
10589
  const isAccessorPredicateName = (propName) => {
10497
10590
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
10498
10591
  if (propName.length <= prefix.length) continue;
@@ -10674,7 +10767,7 @@ const jsxNoNewFunctionAsProp = defineRule({
10674
10767
  recommendation: "Memoize the callback (`useCallback`) or hoist it outside the component.",
10675
10768
  category: "Performance",
10676
10769
  create: (context) => {
10677
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
10770
+ const isTestlikeFile = isTestlikeFilename(context.filename);
10678
10771
  let memoRegistry = null;
10679
10772
  return {
10680
10773
  Program(node) {
@@ -10697,7 +10790,7 @@ const jsxNoNewFunctionAsProp = defineRule({
10697
10790
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
10698
10791
  context.report({
10699
10792
  node,
10700
- message: MESSAGE$33
10793
+ message: MESSAGE$35
10701
10794
  });
10702
10795
  }
10703
10796
  };
@@ -10917,7 +11010,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
10917
11010
  ];
10918
11011
  //#endregion
10919
11012
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
10920
- const MESSAGE$32 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
11013
+ const MESSAGE$34 = "JSX prop receives a new Object on every render — extract it or memoize to avoid re-renders.";
10921
11014
  const isConfigObjectPropName = (propName) => {
10922
11015
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
10923
11016
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -10980,7 +11073,7 @@ const jsxNoNewObjectAsProp = defineRule({
10980
11073
  recommendation: "Memoize the object (`useMemo`) or hoist it outside the component.",
10981
11074
  category: "Performance",
10982
11075
  create: (context) => {
10983
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
11076
+ const isTestlikeFile = isTestlikeFilename(context.filename);
10984
11077
  let memoRegistry = null;
10985
11078
  return {
10986
11079
  Program(node) {
@@ -11004,7 +11097,7 @@ const jsxNoNewObjectAsProp = defineRule({
11004
11097
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
11005
11098
  context.report({
11006
11099
  node,
11007
- message: MESSAGE$32
11100
+ message: MESSAGE$34
11008
11101
  });
11009
11102
  }
11010
11103
  };
@@ -11012,7 +11105,7 @@ const jsxNoNewObjectAsProp = defineRule({
11012
11105
  });
11013
11106
  //#endregion
11014
11107
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
11015
- const MESSAGE$31 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
11108
+ const MESSAGE$33 = "React 19 disallows `javascript:` URLs as a security precaution — use an event handler instead.";
11016
11109
  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;
11017
11110
  const resolveSettings$29 = (settings) => {
11018
11111
  const reactDoctor = settings?.["react-doctor"];
@@ -11052,7 +11145,7 @@ const jsxNoScriptUrl = defineRule({
11052
11145
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
11053
11146
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
11054
11147
  node: attribute,
11055
- message: MESSAGE$31
11148
+ message: MESSAGE$33
11056
11149
  });
11057
11150
  }
11058
11151
  } };
@@ -11340,7 +11433,7 @@ const jsxNoTargetBlank = defineRule({
11340
11433
  });
11341
11434
  //#endregion
11342
11435
  //#region src/plugin/rules/react-builtins/jsx-no-undef.ts
11343
- const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
11436
+ const buildMessage$18 = (name) => `\`${name}\` is not defined in this scope.`;
11344
11437
  const KNOWN_GLOBALS = new Set([
11345
11438
  "globalThis",
11346
11439
  "window",
@@ -11375,7 +11468,7 @@ const jsxNoUndef = defineRule({
11375
11468
  if (findVariableInitializer(node, rootIdentifier)) return;
11376
11469
  context.report({
11377
11470
  node: node.name,
11378
- message: buildMessage$16(rootIdentifier)
11471
+ message: buildMessage$18(rootIdentifier)
11379
11472
  });
11380
11473
  } })
11381
11474
  });
@@ -11474,7 +11567,7 @@ const jsxNoUselessFragment = defineRule({
11474
11567
  });
11475
11568
  //#endregion
11476
11569
  //#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
11477
- const buildMessage$15 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
11570
+ const buildMessage$17 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
11478
11571
  const resolveSettings$26 = (settings) => {
11479
11572
  const reactDoctor = settings?.["react-doctor"];
11480
11573
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
@@ -11590,7 +11683,7 @@ const jsxPascalCase = defineRule({
11590
11683
  if (!isPascal && !isAllCaps) {
11591
11684
  context.report({
11592
11685
  node,
11593
- message: buildMessage$15(segment, settings.allowAllCaps)
11686
+ message: buildMessage$17(segment, settings.allowAllCaps)
11594
11687
  });
11595
11688
  return;
11596
11689
  }
@@ -11642,7 +11735,7 @@ const jsxPropsNoSpreadMulti = defineRule({
11642
11735
  });
11643
11736
  //#endregion
11644
11737
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
11645
- const MESSAGE$30 = "JSX prop spreading is forbidden — list each prop explicitly.";
11738
+ const MESSAGE$32 = "JSX prop spreading is forbidden — list each prop explicitly.";
11646
11739
  const resolveSettings$25 = (settings) => {
11647
11740
  const reactDoctor = settings?.["react-doctor"];
11648
11741
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -11682,7 +11775,7 @@ const jsxPropsNoSpreading = defineRule({
11682
11775
  }
11683
11776
  context.report({
11684
11777
  node: attribute,
11685
- message: MESSAGE$30
11778
+ message: MESSAGE$32
11686
11779
  });
11687
11780
  }
11688
11781
  } };
@@ -11785,7 +11878,7 @@ const labelHasAssociatedControl = defineRule({
11785
11878
  category: "Accessibility",
11786
11879
  create: (context) => {
11787
11880
  const settings = resolveSettings$24(context.settings);
11788
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
11881
+ const isTestlikeFile = isTestlikeFilename(context.filename);
11789
11882
  return { JSXElement(node) {
11790
11883
  if (isTestlikeFile) return;
11791
11884
  const opening = node.openingElement;
@@ -11837,7 +11930,7 @@ const labelHasAssociatedControl = defineRule({
11837
11930
  });
11838
11931
  //#endregion
11839
11932
  //#region src/plugin/rules/a11y/lang.ts
11840
- const MESSAGE$29 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
11933
+ const MESSAGE$31 = "`<html lang>` value must be a valid IANA / BCP-47 language tag (e.g. `en`, `en-US`).";
11841
11934
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
11842
11935
  "aa",
11843
11936
  "ab",
@@ -12048,7 +12141,7 @@ const lang = defineRule({
12048
12141
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
12049
12142
  context.report({
12050
12143
  node: langAttr,
12051
- message: MESSAGE$29
12144
+ message: MESSAGE$31
12052
12145
  });
12053
12146
  return;
12054
12147
  }
@@ -12057,13 +12150,13 @@ const lang = defineRule({
12057
12150
  if (value === null) return;
12058
12151
  if (!isValidLangTag(value)) context.report({
12059
12152
  node: langAttr,
12060
- message: MESSAGE$29
12153
+ message: MESSAGE$31
12061
12154
  });
12062
12155
  } })
12063
12156
  });
12064
12157
  //#endregion
12065
12158
  //#region src/plugin/rules/a11y/media-has-caption.ts
12066
- const MESSAGE$28 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
12159
+ const MESSAGE$30 = "`<audio>` / `<video>` must have a `<track kind=\"captions\">` child for users who can't hear audio.";
12067
12160
  const DEFAULT_AUDIO = ["audio"];
12068
12161
  const DEFAULT_VIDEO = ["video"];
12069
12162
  const DEFAULT_TRACK = ["track"];
@@ -12103,7 +12196,7 @@ const mediaHasCaption = defineRule({
12103
12196
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
12104
12197
  context.report({
12105
12198
  node: node.name,
12106
- message: MESSAGE$28
12199
+ message: MESSAGE$30
12107
12200
  });
12108
12201
  return;
12109
12202
  }
@@ -12120,7 +12213,7 @@ const mediaHasCaption = defineRule({
12120
12213
  return kindValue.value.toLowerCase() === "captions";
12121
12214
  })) context.report({
12122
12215
  node: node.name,
12123
- message: MESSAGE$28
12216
+ message: MESSAGE$30
12124
12217
  });
12125
12218
  } };
12126
12219
  }
@@ -12323,7 +12416,7 @@ const nextjsMissingMetadata = defineRule({
12323
12416
  severity: "warn",
12324
12417
  recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
12325
12418
  create: (context) => ({ Program(programNode) {
12326
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12419
+ const filename = normalizeFilename$1(context.filename ?? "");
12327
12420
  if (!PAGE_FILE_PATTERN.test(filename)) return;
12328
12421
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
12329
12422
  if (!programNode.body?.some((statement) => {
@@ -12388,7 +12481,7 @@ const nextjsNoClientFetchForServerData = defineRule({
12388
12481
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
12389
12482
  const callback = getEffectCallback(node);
12390
12483
  if (!callback || !containsFetchCall(callback)) return;
12391
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12484
+ const filename = normalizeFilename$1(context.filename ?? "");
12392
12485
  if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || PAGES_DIRECTORY_PATTERN.test(filename)) context.report({
12393
12486
  node,
12394
12487
  message: "useEffect + fetch in a page/layout — fetch data server-side with a server component instead"
@@ -12421,7 +12514,7 @@ const nextjsNoClientSideRedirect = defineRule({
12421
12514
  severity: "warn",
12422
12515
  recommendation: "Avoid redirects inside useEffect. Use an event handler, middleware, or server-side redirect (App Router: redirect() from next/navigation; Pages Router: getServerSideProps redirect)",
12423
12516
  create: (context) => {
12424
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12517
+ const filename = normalizeFilename$1(context.filename ?? "");
12425
12518
  const isPagesRouterFile = PAGES_DIRECTORY_PATTERN.test(filename);
12426
12519
  return { CallExpression(node) {
12427
12520
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
@@ -12490,7 +12583,7 @@ const nextjsNoHeadImport = defineRule({
12490
12583
  recommendation: "Use the Metadata API instead: `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
12491
12584
  create: (context) => ({ ImportDeclaration(node) {
12492
12585
  if (node.source?.value !== "next/head") return;
12493
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12586
+ const filename = normalizeFilename$1(context.filename ?? "");
12494
12587
  if (!APP_DIRECTORY_PATTERN.test(filename)) return;
12495
12588
  context.report({
12496
12589
  node,
@@ -12507,7 +12600,7 @@ const nextjsNoImgElement = defineRule({
12507
12600
  severity: "warn",
12508
12601
  recommendation: "`import Image from 'next/image'` — provides automatic WebP/AVIF, lazy loading, and responsive srcset",
12509
12602
  create: (context) => {
12510
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12603
+ const filename = normalizeFilename$1(context.filename ?? "");
12511
12604
  const isOgRoute = OG_ROUTE_PATTERN.test(filename);
12512
12605
  return { JSXOpeningElement(node) {
12513
12606
  if (isOgRoute) return;
@@ -12811,7 +12904,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
12811
12904
  };
12812
12905
  const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
12813
12906
  if (remainingDepth <= 0) return [];
12814
- if (isFunctionLike$1(expression)) return expression.body ? [expression.body] : [];
12907
+ if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
12815
12908
  if (isNodeOfType(expression, "CallExpression")) {
12816
12909
  for (const callArgument of expression.arguments ?? []) {
12817
12910
  if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
@@ -12861,7 +12954,7 @@ const nextjsNoSideEffectInGetHandler = defineRule({
12861
12954
  resolveBinding = buildProgramBindingLookup(node);
12862
12955
  },
12863
12956
  ExportNamedDeclaration(node) {
12864
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
12957
+ const filename = normalizeFilename$1(context.filename ?? "");
12865
12958
  if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
12866
12959
  if (CRON_ROUTE_PATTERN.test(filename)) return;
12867
12960
  if (!isExportedGetHandler(node)) return;
@@ -12934,7 +13027,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
12934
13027
  });
12935
13028
  //#endregion
12936
13029
  //#region src/plugin/rules/a11y/no-access-key.ts
12937
- const MESSAGE$27 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
13030
+ const MESSAGE$29 = "`accessKey` should not be used — accessKeys conflict with screen reader and OS-level shortcuts.";
12938
13031
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
12939
13032
  const noAccessKey = defineRule({
12940
13033
  id: "no-access-key",
@@ -12950,7 +13043,7 @@ const noAccessKey = defineRule({
12950
13043
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
12951
13044
  context.report({
12952
13045
  node: accessKey,
12953
- message: MESSAGE$27
13046
+ message: MESSAGE$29
12954
13047
  });
12955
13048
  return;
12956
13049
  }
@@ -12960,7 +13053,7 @@ const noAccessKey = defineRule({
12960
13053
  if (isUndefinedIdentifier(expression)) return;
12961
13054
  context.report({
12962
13055
  node: accessKey,
12963
- message: MESSAGE$27
13056
+ message: MESSAGE$29
12964
13057
  });
12965
13058
  }
12966
13059
  } })
@@ -13269,7 +13362,7 @@ const getEffectFn = (analysis, node) => {
13269
13362
  if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
13270
13363
  if (isNodeOfType(fn, "Identifier")) {
13271
13364
  const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
13272
- if (definitionNode && isFunctionLike$1(definitionNode)) return definitionNode;
13365
+ if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
13273
13366
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13274
13367
  const initializer = definitionNode.init;
13275
13368
  if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
@@ -13362,14 +13455,14 @@ const getUseStateDecl = (analysis, ref) => {
13362
13455
  return node ?? null;
13363
13456
  };
13364
13457
  const isCleanupReturnArgument = (analysis, node) => {
13365
- if (isFunctionLike$1(node)) return true;
13458
+ if (isFunctionLike$2(node)) return true;
13366
13459
  if (isNodeOfType(node, "MemberExpression")) return true;
13367
13460
  if (isNodeOfType(node, "Identifier")) {
13368
13461
  const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
13369
- if (definitionNode && isFunctionLike$1(definitionNode)) return true;
13462
+ if (definitionNode && isFunctionLike$2(definitionNode)) return true;
13370
13463
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13371
13464
  const initializer = definitionNode.init;
13372
- return isFunctionLike$1(initializer);
13465
+ return isFunctionLike$2(initializer);
13373
13466
  }
13374
13467
  }
13375
13468
  if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
@@ -13379,7 +13472,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13379
13472
  if (visited.has(node)) return false;
13380
13473
  visited.add(node);
13381
13474
  if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
13382
- if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
13475
+ if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
13383
13476
  const record = node;
13384
13477
  for (const [key, value] of Object.entries(record)) {
13385
13478
  if (key === "parent") continue;
@@ -13391,7 +13484,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13391
13484
  };
13392
13485
  const hasCleanup = (analysis, node) => {
13393
13486
  const fn = getEffectFn(analysis, node);
13394
- if (!isFunctionLike$1(fn)) return false;
13487
+ if (!isFunctionLike$2(fn)) return false;
13395
13488
  if (!isNodeOfType(fn.body, "BlockStatement")) return false;
13396
13489
  return hasCleanupReturn(analysis, fn.body);
13397
13490
  };
@@ -13405,9 +13498,9 @@ const findContainingNode = (analysis, node) => {
13405
13498
  //#region src/plugin/rules/state-and-effects/no-adjust-state-on-prop-change.ts
13406
13499
  const noAdjustStateOnPropChange = defineRule({
13407
13500
  id: "no-adjust-state-on-prop-change",
13408
- severity: "warn",
13501
+ severity: "error",
13409
13502
  tags: ["test-noise"],
13410
- 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",
13503
+ 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",
13411
13504
  create: (context) => ({ CallExpression(node) {
13412
13505
  if (!isUseEffect(node)) return;
13413
13506
  const analysis = getProgramAnalysis(node);
@@ -13426,14 +13519,14 @@ const noAdjustStateOnPropChange = defineRule({
13426
13519
  if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
13427
13520
  context.report({
13428
13521
  node: callExpr,
13429
- message: "Avoid adjusting state when a prop changes. Instead, adjust the state directly during render, or refactor your state to avoid this need entirely."
13522
+ 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."
13430
13523
  });
13431
13524
  }
13432
13525
  } })
13433
13526
  });
13434
13527
  //#endregion
13435
13528
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
13436
- const MESSAGE$26 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
13529
+ const MESSAGE$28 = "Focusable elements must not have `aria-hidden=\"true\"` — focus would skip the hidden subtree, confusing keyboard users.";
13437
13530
  const noAriaHiddenOnFocusable = defineRule({
13438
13531
  id: "no-aria-hidden-on-focusable",
13439
13532
  tags: ["react-jsx-only"],
@@ -13459,7 +13552,7 @@ const noAriaHiddenOnFocusable = defineRule({
13459
13552
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
13460
13553
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
13461
13554
  node: ariaHidden,
13462
- message: MESSAGE$26
13555
+ message: MESSAGE$28
13463
13556
  });
13464
13557
  } })
13465
13558
  });
@@ -13739,7 +13832,7 @@ const isInsideStaticPlaceholderMap = (node) => {
13739
13832
  let current = node;
13740
13833
  while (current.parent) {
13741
13834
  const parent = current.parent;
13742
- if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13835
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13743
13836
  const callee = parent.callee;
13744
13837
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
13745
13838
  if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
@@ -13758,7 +13851,7 @@ const findIteratorItemName$1 = (node) => {
13758
13851
  let current = node;
13759
13852
  while (current.parent) {
13760
13853
  const parent = current.parent;
13761
- if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13854
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13762
13855
  const callee = parent.callee;
13763
13856
  const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
13764
13857
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
@@ -13826,7 +13919,7 @@ const noArrayIndexAsKey = defineRule({
13826
13919
  });
13827
13920
  //#endregion
13828
13921
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
13829
- const MESSAGE$25 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
13922
+ const MESSAGE$27 = "Array index in `key` doesn't uniquely identify the element — re-renders may use stale state.";
13830
13923
  const SECOND_INDEX_METHODS = new Set([
13831
13924
  "every",
13832
13925
  "filter",
@@ -14028,7 +14121,7 @@ const noArrayIndexKey = defineRule({
14028
14121
  }
14029
14122
  context.report({
14030
14123
  node: keyAttribute,
14031
- message: MESSAGE$25
14124
+ message: MESSAGE$27
14032
14125
  });
14033
14126
  },
14034
14127
  CallExpression(node) {
@@ -14048,7 +14141,7 @@ const noArrayIndexKey = defineRule({
14048
14141
  if (propName !== "key") continue;
14049
14142
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
14050
14143
  node: property,
14051
- message: MESSAGE$25
14144
+ message: MESSAGE$27
14052
14145
  });
14053
14146
  }
14054
14147
  }
@@ -14056,7 +14149,7 @@ const noArrayIndexKey = defineRule({
14056
14149
  });
14057
14150
  //#endregion
14058
14151
  //#region src/plugin/rules/a11y/no-autofocus.ts
14059
- const MESSAGE$24 = "`autoFocus` should not be used — it disrupts users who expect the page focus to remain at the top of the document on load.";
14152
+ const MESSAGE$26 = "`autoFocus` should not be used — it disrupts users who expect the page focus to remain at the top of the document on load.";
14060
14153
  const resolveSettings$21 = (settings) => {
14061
14154
  const reactDoctor = settings?.["react-doctor"];
14062
14155
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -14093,7 +14186,7 @@ const noAutofocus = defineRule({
14093
14186
  category: "Accessibility",
14094
14187
  create: (context) => {
14095
14188
  const settings = resolveSettings$21(context.settings);
14096
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
14189
+ const isTestlikeFile = isTestlikeFilename(context.filename);
14097
14190
  return { JSXOpeningElement(node) {
14098
14191
  if (isTestlikeFile) return;
14099
14192
  const autoFocusAttribute = node.attributes.find((attribute) => {
@@ -14111,7 +14204,7 @@ const noAutofocus = defineRule({
14111
14204
  }
14112
14205
  context.report({
14113
14206
  node: autoFocusAttribute,
14114
- message: MESSAGE$24
14207
+ message: MESSAGE$26
14115
14208
  });
14116
14209
  } };
14117
14210
  }
@@ -14467,7 +14560,7 @@ const noBarrelImport = defineRule({
14467
14560
  if (didReportForFile) return;
14468
14561
  const source = node.source?.value;
14469
14562
  if (typeof source !== "string" || !source.startsWith(".")) return;
14470
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
14563
+ const filename = normalizeFilename$1(context.filename ?? "");
14471
14564
  if (!filename) return;
14472
14565
  const importRequests = getRuntimeImportRequests(node);
14473
14566
  if (importRequests.length === 0) return;
@@ -14602,7 +14695,7 @@ const noChainStateUpdates = defineRule({
14602
14695
  });
14603
14696
  //#endregion
14604
14697
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
14605
- const MESSAGE$23 = "Avoid passing children using a `children` prop — nest them between the JSX tags or pass them as additional `React.createElement` arguments instead.";
14698
+ const MESSAGE$25 = "Avoid passing children using a `children` prop — nest them between the JSX tags or pass them as additional `React.createElement` arguments instead.";
14606
14699
  const noChildrenProp = defineRule({
14607
14700
  id: "no-children-prop",
14608
14701
  severity: "warn",
@@ -14613,7 +14706,7 @@ const noChildrenProp = defineRule({
14613
14706
  if (node.name.name !== "children") return;
14614
14707
  context.report({
14615
14708
  node: node.name,
14616
- message: MESSAGE$23
14709
+ message: MESSAGE$25
14617
14710
  });
14618
14711
  },
14619
14712
  CallExpression(node) {
@@ -14626,7 +14719,7 @@ const noChildrenProp = defineRule({
14626
14719
  const propertyKey = property.key;
14627
14720
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
14628
14721
  node: propertyKey,
14629
- message: MESSAGE$23
14722
+ message: MESSAGE$25
14630
14723
  });
14631
14724
  }
14632
14725
  }
@@ -14634,7 +14727,7 @@ const noChildrenProp = defineRule({
14634
14727
  });
14635
14728
  //#endregion
14636
14729
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
14637
- const MESSAGE$22 = "`React.cloneElement` is uncommon and leads to fragile components.";
14730
+ const MESSAGE$24 = "`React.cloneElement` is uncommon and leads to fragile components.";
14638
14731
  const noCloneElement = defineRule({
14639
14732
  id: "no-clone-element",
14640
14733
  severity: "warn",
@@ -14646,7 +14739,7 @@ const noCloneElement = defineRule({
14646
14739
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
14647
14740
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
14648
14741
  node: callee,
14649
- message: MESSAGE$22
14742
+ message: MESSAGE$24
14650
14743
  });
14651
14744
  return;
14652
14745
  }
@@ -14659,14 +14752,231 @@ const noCloneElement = defineRule({
14659
14752
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
14660
14753
  context.report({
14661
14754
  node: callee,
14662
- message: MESSAGE$22
14755
+ message: MESSAGE$24
14663
14756
  });
14664
14757
  }
14665
14758
  } })
14666
14759
  });
14667
14760
  //#endregion
14761
+ //#region src/plugin/utils/component-or-hook-display-name.ts
14762
+ const hocWrapperCalleeName = (callee) => {
14763
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
14764
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
14765
+ return null;
14766
+ };
14767
+ const displayNameFromFunctionBinding = (functionNode) => {
14768
+ let current = functionNode;
14769
+ for (;;) {
14770
+ const parent = current.parent;
14771
+ if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
14772
+ const calleeName = hocWrapperCalleeName(parent.callee);
14773
+ if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
14774
+ current = parent;
14775
+ continue;
14776
+ }
14777
+ }
14778
+ break;
14779
+ }
14780
+ const binding = current.parent;
14781
+ if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
14782
+ return null;
14783
+ };
14784
+ const componentOrHookDisplayNameForFunction = (functionNode) => {
14785
+ if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
14786
+ return displayNameFromFunctionBinding(functionNode);
14787
+ };
14788
+ const nearestEnclosingFunction = (node) => {
14789
+ let cursor = node.parent;
14790
+ while (cursor) {
14791
+ if (isFunctionLike$2(cursor)) return cursor;
14792
+ cursor = cursor.parent ?? null;
14793
+ }
14794
+ return null;
14795
+ };
14796
+ //#endregion
14797
+ //#region src/plugin/utils/enclosing-component-or-hook-name.ts
14798
+ const enclosingComponentOrHookName = (node) => {
14799
+ const functionNode = nearestEnclosingFunction(node);
14800
+ return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
14801
+ };
14802
+ //#endregion
14803
+ //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
14804
+ const MESSAGE$23 = "createContext() called inside a component or hook — every render creates a brand new Context object, resetting every consumer and disconnecting Provider/Consumer pairs. Move createContext to module scope (outside the component) so the Context identity is stable across renders.";
14805
+ const CONTEXT_MODULES = [
14806
+ "react",
14807
+ "use-context-selector",
14808
+ "react-tracked"
14809
+ ];
14810
+ const isCreateContextCallee = (callee) => {
14811
+ if (isNodeOfType(callee, "Identifier")) {
14812
+ for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
14813
+ return false;
14814
+ }
14815
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
14816
+ const namespaceIdentifier = callee.object;
14817
+ const propertyIdentifier = callee.property;
14818
+ if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
14819
+ if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
14820
+ if (propertyIdentifier.name !== "createContext") return false;
14821
+ const namespaceName = namespaceIdentifier.name;
14822
+ if (isCanonicalReactNamespaceName(namespaceName)) return true;
14823
+ for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
14824
+ return false;
14825
+ }
14826
+ return false;
14827
+ };
14828
+ const noCreateContextInRender = defineRule({
14829
+ id: "no-create-context-in-render",
14830
+ severity: "error",
14831
+ category: "Correctness",
14832
+ recommendation: "Move `createContext(...)` to module scope so its identity is stable across renders.",
14833
+ create: (context) => ({ CallExpression(node) {
14834
+ if (!isCreateContextCallee(node.callee)) return;
14835
+ const componentOrHookName = enclosingComponentOrHookName(node);
14836
+ if (!componentOrHookName) return;
14837
+ context.report({
14838
+ node,
14839
+ message: `${MESSAGE$23} (called inside "${componentOrHookName}")`
14840
+ });
14841
+ } })
14842
+ });
14843
+ //#endregion
14844
+ //#region src/plugin/rules/state-and-effects/no-create-store-in-render.ts
14845
+ const STORE_FACTORIES = [
14846
+ {
14847
+ module: "zustand",
14848
+ exportedName: "create",
14849
+ humanLabel: "zustand.create"
14850
+ },
14851
+ {
14852
+ module: "zustand",
14853
+ exportedName: "createStore",
14854
+ humanLabel: "zustand.createStore"
14855
+ },
14856
+ {
14857
+ module: "zustand/vanilla",
14858
+ exportedName: "createStore",
14859
+ humanLabel: "zustand.createStore"
14860
+ },
14861
+ {
14862
+ module: "zustand/vanilla",
14863
+ exportedName: "create",
14864
+ humanLabel: "zustand.create"
14865
+ },
14866
+ {
14867
+ module: "redux",
14868
+ exportedName: "createStore",
14869
+ humanLabel: "redux.createStore"
14870
+ },
14871
+ {
14872
+ module: "@reduxjs/toolkit",
14873
+ exportedName: "configureStore",
14874
+ humanLabel: "@reduxjs/toolkit.configureStore"
14875
+ },
14876
+ {
14877
+ module: "@reduxjs/toolkit",
14878
+ exportedName: "createSlice",
14879
+ humanLabel: "createSlice"
14880
+ },
14881
+ {
14882
+ module: "jotai",
14883
+ exportedName: "atom",
14884
+ humanLabel: "jotai.atom"
14885
+ },
14886
+ {
14887
+ module: "jotai/vanilla",
14888
+ exportedName: "atom",
14889
+ humanLabel: "jotai.atom"
14890
+ },
14891
+ {
14892
+ module: "jotai",
14893
+ exportedName: "createStore",
14894
+ humanLabel: "jotai.createStore"
14895
+ },
14896
+ {
14897
+ module: "valtio",
14898
+ exportedName: "proxy",
14899
+ humanLabel: "valtio.proxy"
14900
+ },
14901
+ {
14902
+ module: "valtio/vanilla",
14903
+ exportedName: "proxy",
14904
+ humanLabel: "valtio.proxy"
14905
+ },
14906
+ {
14907
+ module: "mobx",
14908
+ exportedName: "observable",
14909
+ humanLabel: "mobx.observable"
14910
+ },
14911
+ {
14912
+ module: "mobx",
14913
+ exportedName: "makeAutoObservable",
14914
+ humanLabel: "mobx.makeAutoObservable"
14915
+ },
14916
+ {
14917
+ module: "mobx",
14918
+ exportedName: "makeObservable",
14919
+ humanLabel: "mobx.makeObservable"
14920
+ },
14921
+ {
14922
+ module: "nanostores",
14923
+ exportedName: "atom",
14924
+ humanLabel: "nanostores.atom"
14925
+ },
14926
+ {
14927
+ module: "nanostores",
14928
+ exportedName: "map",
14929
+ humanLabel: "nanostores.map"
14930
+ },
14931
+ {
14932
+ module: "@xstate/store",
14933
+ exportedName: "createStore",
14934
+ humanLabel: "@xstate/store.createStore"
14935
+ }
14936
+ ];
14937
+ const STORE_FACTORY_LOOKUP = /* @__PURE__ */ new Map();
14938
+ for (const factory of STORE_FACTORIES) {
14939
+ const bucket = STORE_FACTORY_LOOKUP.get(factory.exportedName) ?? [];
14940
+ STORE_FACTORY_LOOKUP.set(factory.exportedName, [...bucket, factory]);
14941
+ }
14942
+ const resolveStoreFactoryForCallee = (callee) => {
14943
+ if (isNodeOfType(callee, "Identifier")) {
14944
+ const localName = callee.name;
14945
+ for (const factoryBucket of STORE_FACTORY_LOOKUP.values()) for (const factory of factoryBucket) if (getImportedNameFromModule(callee, localName, factory.module) === factory.exportedName) return factory;
14946
+ return null;
14947
+ }
14948
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
14949
+ const namespaceIdentifier = callee.object;
14950
+ const propertyIdentifier = callee.property;
14951
+ if (!isNodeOfType(namespaceIdentifier, "Identifier")) return null;
14952
+ if (!isNodeOfType(propertyIdentifier, "Identifier")) return null;
14953
+ const propertyName = propertyIdentifier.name;
14954
+ const factoryBucket = STORE_FACTORY_LOOKUP.get(propertyName);
14955
+ if (!factoryBucket) return null;
14956
+ for (const factory of factoryBucket) if (isImportedFromModule(namespaceIdentifier, namespaceIdentifier.name, factory.module)) return factory;
14957
+ return null;
14958
+ }
14959
+ return null;
14960
+ };
14961
+ const noCreateStoreInRender = defineRule({
14962
+ id: "no-create-store-in-render",
14963
+ severity: "error",
14964
+ category: "Correctness",
14965
+ recommendation: "Hoist the store/atom/observable construction to module scope — render functions and hooks must not allocate state containers.",
14966
+ create: (context) => ({ CallExpression(node) {
14967
+ const factory = resolveStoreFactoryForCallee(node.callee);
14968
+ if (!factory) return;
14969
+ const componentOrHookName = enclosingComponentOrHookName(node);
14970
+ if (!componentOrHookName) return;
14971
+ context.report({
14972
+ node,
14973
+ message: `\`${factory.humanLabel}(...)\` called inside "${componentOrHookName}" allocates a fresh state container on every render — subscribers disconnect, identities (action creators, reducer reference, store instance) churn, and persisted state resets. Hoist the call to module scope.`
14974
+ });
14975
+ } })
14976
+ });
14977
+ //#endregion
14668
14978
  //#region src/plugin/rules/react-builtins/no-danger.ts
14669
- const MESSAGE$21 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
14979
+ const MESSAGE$22 = "Do not use `dangerouslySetInnerHTML` — it injects raw HTML and is a common XSS vector.";
14670
14980
  const noDanger = defineRule({
14671
14981
  id: "no-danger",
14672
14982
  severity: "warn",
@@ -14677,7 +14987,7 @@ const noDanger = defineRule({
14677
14987
  if (!propAttribute) return;
14678
14988
  context.report({
14679
14989
  node: propAttribute.name,
14680
- message: MESSAGE$21
14990
+ message: MESSAGE$22
14681
14991
  });
14682
14992
  },
14683
14993
  CallExpression(node) {
@@ -14689,7 +14999,7 @@ const noDanger = defineRule({
14689
14999
  const propertyKey = property.key;
14690
15000
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
14691
15001
  node: propertyKey,
14692
- message: MESSAGE$21
15002
+ message: MESSAGE$22
14693
15003
  });
14694
15004
  }
14695
15005
  }
@@ -14697,7 +15007,7 @@ const noDanger = defineRule({
14697
15007
  });
14698
15008
  //#endregion
14699
15009
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
14700
- const MESSAGE$20 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
15010
+ const MESSAGE$21 = "Only set one of `children` or `dangerouslySetInnerHTML` — React throws a runtime warning when both are present.";
14701
15011
  const isLineBreak = (child) => {
14702
15012
  if (!isNodeOfType(child, "JSXText")) return false;
14703
15013
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -14766,7 +15076,7 @@ const noDangerWithChildren = defineRule({
14766
15076
  if (!hasChildrenProp && !hasNestedChildren) return;
14767
15077
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
14768
15078
  node: opening,
14769
- message: MESSAGE$20
15079
+ message: MESSAGE$21
14770
15080
  });
14771
15081
  },
14772
15082
  CallExpression(node) {
@@ -14778,7 +15088,7 @@ const noDangerWithChildren = defineRule({
14778
15088
  if (!propsShape.hasDangerously) return;
14779
15089
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
14780
15090
  node,
14781
- message: MESSAGE$20
15091
+ message: MESSAGE$21
14782
15092
  });
14783
15093
  }
14784
15094
  })
@@ -15153,7 +15463,7 @@ const extractDestructuredPropNames = (params) => {
15153
15463
  };
15154
15464
  const getInlineFunctionNode = (node) => {
15155
15465
  if (!node) return null;
15156
- if (isFunctionLike$1(node)) return node;
15466
+ if (isFunctionLike$2(node)) return node;
15157
15467
  if (!isNodeOfType(node, "CallExpression")) return null;
15158
15468
  for (const argument of node.arguments ?? []) {
15159
15469
  const inlineFunctionNode = getInlineFunctionNode(argument);
@@ -15164,7 +15474,7 @@ const getInlineFunctionNode = (node) => {
15164
15474
  const getNearestComponentFunction = (node) => {
15165
15475
  let cursor = node.parent ?? null;
15166
15476
  while (cursor) {
15167
- if (isFunctionLike$1(cursor)) return cursor;
15477
+ if (isFunctionLike$2(cursor)) return cursor;
15168
15478
  cursor = cursor.parent ?? null;
15169
15479
  }
15170
15480
  return null;
@@ -15345,7 +15655,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
15345
15655
  //#endregion
15346
15656
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
15347
15657
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
15348
- const MESSAGE$19 = "Do not use `this.setState` in `componentDidMount`.";
15658
+ const MESSAGE$20 = "Do not use `this.setState` in `componentDidMount`.";
15349
15659
  const resolveSettings$20 = (settings) => {
15350
15660
  const reactDoctor = settings?.["react-doctor"];
15351
15661
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -15363,7 +15673,7 @@ const noDidMountSetState = defineRule({
15363
15673
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15364
15674
  context.report({
15365
15675
  node: node.callee,
15366
- message: MESSAGE$19
15676
+ message: MESSAGE$20
15367
15677
  });
15368
15678
  } };
15369
15679
  }
@@ -15371,7 +15681,7 @@ const noDidMountSetState = defineRule({
15371
15681
  //#endregion
15372
15682
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
15373
15683
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
15374
- const MESSAGE$18 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
15684
+ const MESSAGE$19 = "Do not use `this.setState` in `componentDidUpdate` — it can cause infinite loops.";
15375
15685
  const resolveSettings$19 = (settings) => {
15376
15686
  const reactDoctor = settings?.["react-doctor"];
15377
15687
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -15389,7 +15699,7 @@ const noDidUpdateSetState = defineRule({
15389
15699
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15390
15700
  context.report({
15391
15701
  node: node.callee,
15392
- message: MESSAGE$18
15702
+ message: MESSAGE$19
15393
15703
  });
15394
15704
  } };
15395
15705
  }
@@ -15412,7 +15722,7 @@ const isStateMemberExpression = (node) => {
15412
15722
  };
15413
15723
  //#endregion
15414
15724
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
15415
- const MESSAGE$17 = "Never mutate `this.state` directly.";
15725
+ const MESSAGE$18 = "Never mutate `this.state` directly.";
15416
15726
  const shouldIgnoreMutation = (node) => {
15417
15727
  let isConstructor = false;
15418
15728
  let isInsideCallExpression = false;
@@ -15434,7 +15744,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
15434
15744
  if (shouldIgnoreMutation(reportNode)) return;
15435
15745
  context.report({
15436
15746
  node: reportNode,
15437
- message: MESSAGE$17
15747
+ message: MESSAGE$18
15438
15748
  });
15439
15749
  };
15440
15750
  const noDirectMutationState = defineRule({
@@ -15490,7 +15800,7 @@ const collectFunctionLocalBindings = (functionNode) => {
15490
15800
  const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
15491
15801
  if (!node || typeof node !== "object") return;
15492
15802
  let nextShadowedStateNames = shadowedStateNames;
15493
- if (isFunctionLike$1(node)) {
15803
+ if (isFunctionLike$2(node)) {
15494
15804
  const localBindings = collectFunctionLocalBindings(node);
15495
15805
  if (localBindings.size > 0) {
15496
15806
  const merged = new Set(shadowedStateNames);
@@ -15536,7 +15846,7 @@ const noDirectStateMutation = defineRule({
15536
15846
  if (!isNodeOfType(callee, "MemberExpression")) return;
15537
15847
  if (!isNodeOfType(callee.property, "Identifier")) return;
15538
15848
  const methodName = callee.property.name;
15539
- if (!MUTATING_ARRAY_METHODS$1.has(methodName)) return;
15849
+ if (!MUTATING_ARRAY_METHODS.has(methodName)) return;
15540
15850
  const rootName = getRootIdentifierName(callee.object);
15541
15851
  if (!rootName || !stateValueToSetter.has(rootName)) return;
15542
15852
  if (currentlyShadowed.has(rootName)) return;
@@ -15597,7 +15907,7 @@ const noDisabledZoom = defineRule({
15597
15907
  });
15598
15908
  //#endregion
15599
15909
  //#region src/plugin/rules/a11y/no-distracting-elements.ts
15600
- const buildMessage$14 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
15910
+ const buildMessage$16 = (tag) => `\`<${tag}>\` is distracting and should not be used — replace with semantic, accessible markup.`;
15601
15911
  const DEFAULT_DISTRACTING = ["marquee", "blink"];
15602
15912
  const resolveSettings$18 = (settings) => {
15603
15913
  const reactDoctor = settings?.["react-doctor"];
@@ -15617,7 +15927,7 @@ const noDistractingElements = defineRule({
15617
15927
  const tag = getElementType(node, context.settings);
15618
15928
  if (distractingTags.has(tag)) context.report({
15619
15929
  node: node.name,
15620
- message: buildMessage$14(tag)
15930
+ message: buildMessage$16(tag)
15621
15931
  });
15622
15932
  } };
15623
15933
  }
@@ -16057,6 +16367,69 @@ const noEffectEventInDeps = defineRule({
16057
16367
  }
16058
16368
  });
16059
16369
  //#endregion
16370
+ //#region src/plugin/rules/state-and-effects/no-effect-with-fresh-deps.ts
16371
+ const classifyFreshDependency = (expression) => {
16372
+ const stripped = stripParenExpression(expression);
16373
+ if (isNodeOfType(stripped, "ObjectExpression")) return "object";
16374
+ if (isNodeOfType(stripped, "ArrayExpression")) return "array";
16375
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) return "function";
16376
+ if (isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment")) return "JSX";
16377
+ if (isNodeOfType(stripped, "NewExpression")) return "instance";
16378
+ return null;
16379
+ };
16380
+ const resolveDependencyFreshness = (dep) => {
16381
+ const directKind = classifyFreshDependency(dep);
16382
+ if (directKind) return {
16383
+ kind: directKind,
16384
+ viaBindingName: null
16385
+ };
16386
+ const stripped = stripParenExpression(dep);
16387
+ if (!isNodeOfType(stripped, "Identifier")) return null;
16388
+ const binding = findVariableInitializer(stripped, stripped.name);
16389
+ if (!binding || !binding.initializer) return null;
16390
+ if (binding.scopeOwner.type === "Program") return null;
16391
+ const declarator = binding.bindingIdentifier.parent;
16392
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== binding.initializer) return null;
16393
+ const indirectKind = classifyFreshDependency(binding.initializer);
16394
+ if (!indirectKind) return null;
16395
+ return {
16396
+ kind: indirectKind,
16397
+ viaBindingName: stripped.name
16398
+ };
16399
+ };
16400
+ const noEffectWithFreshDeps = defineRule({
16401
+ id: "no-effect-with-fresh-deps",
16402
+ severity: "error",
16403
+ category: "State & Effects",
16404
+ recommendation: "Move the constructed value into the hook body (so it's recomputed during render) and instead depend on its primitive inputs, or wrap the value in useMemo / useCallback so its reference is stable.",
16405
+ create: (context) => ({ CallExpression(node) {
16406
+ if (!isHookCall$1(node, HOOKS_WITH_DEPS)) return;
16407
+ const args = node.arguments ?? [];
16408
+ if (args.length < 2) return;
16409
+ const depsNode = args[1];
16410
+ if (!depsNode) return;
16411
+ const stripped = stripParenExpression(depsNode);
16412
+ if (!isNodeOfType(stripped, "ArrayExpression")) return;
16413
+ const calleeNode = node.callee;
16414
+ let hookName;
16415
+ if (isNodeOfType(calleeNode, "Identifier")) hookName = calleeNode.name;
16416
+ else if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) hookName = calleeNode.property.name;
16417
+ else hookName = "hook";
16418
+ const elements = stripped.elements ?? [];
16419
+ for (const element of elements) {
16420
+ if (!element) continue;
16421
+ if (isNodeOfType(element, "SpreadElement")) continue;
16422
+ const freshness = resolveDependencyFreshness(element);
16423
+ if (!freshness) continue;
16424
+ const message = freshness.viaBindingName ? `${hookName} dep array element \`${freshness.viaBindingName}\` is a render-local ${freshness.kind} (declared in the same component scope); \`===\` will always fail because the binding is re-allocated each render. Hoist it to module scope or wrap it in useMemo/useCallback.` : `${hookName} dep array contains a freshly-allocated ${freshness.kind}; \`===\` will always fail on this element so the hook runs every render. Move the value into the hook body or memoize it with useMemo/useCallback so its reference is stable.`;
16425
+ context.report({
16426
+ node: element,
16427
+ message
16428
+ });
16429
+ }
16430
+ } })
16431
+ });
16432
+ //#endregion
16060
16433
  //#region src/plugin/rules/security/no-eval.ts
16061
16434
  const noEval = defineRule({
16062
16435
  id: "no-eval",
@@ -16947,7 +17320,7 @@ const ALLOWED_NAMESPACES = new Set([
16947
17320
  "ReactDOM",
16948
17321
  "ReactDom"
16949
17322
  ]);
16950
- const MESSAGE$16 = "Unexpected call to `findDOMNode` — removed in React 19.";
17323
+ const MESSAGE$17 = "Unexpected call to `findDOMNode` — removed in React 19.";
16951
17324
  const noFindDomNode = defineRule({
16952
17325
  id: "no-find-dom-node",
16953
17326
  severity: "warn",
@@ -16957,7 +17330,7 @@ const noFindDomNode = defineRule({
16957
17330
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
16958
17331
  context.report({
16959
17332
  node: callee,
16960
- message: MESSAGE$16
17333
+ message: MESSAGE$17
16961
17334
  });
16962
17335
  return;
16963
17336
  }
@@ -16968,7 +17341,7 @@ const noFindDomNode = defineRule({
16968
17341
  if (callee.property.name !== "findDOMNode") return;
16969
17342
  context.report({
16970
17343
  node: callee.property,
16971
- message: MESSAGE$16
17344
+ message: MESSAGE$17
16972
17345
  });
16973
17346
  }
16974
17347
  } })
@@ -17458,7 +17831,7 @@ const noInlinePropOnMemoComponent = defineRule({
17458
17831
  });
17459
17832
  //#endregion
17460
17833
  //#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
17461
- const buildMessage$13 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
17834
+ const buildMessage$15 = (tag, role) => `Interactive element \`<${tag}>\` cannot have non-interactive role \`${role}\`.`;
17462
17835
  const PRESENTATION_ROLES = ["presentation", "none"];
17463
17836
  const DEFAULT_ALLOWED_ROLES$1 = {
17464
17837
  tr: ["none", "presentation"],
@@ -17502,7 +17875,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
17502
17875
  if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
17503
17876
  context.report({
17504
17877
  node: roleAttribute,
17505
- message: buildMessage$13(elementType, firstRole)
17878
+ message: buildMessage$15(elementType, firstRole)
17506
17879
  });
17507
17880
  } };
17508
17881
  }
@@ -17703,7 +18076,7 @@ const isInsideClassBody = (node) => {
17703
18076
  let current = node.parent;
17704
18077
  while (current) {
17705
18078
  if (isNodeOfType(current, "ClassBody")) return true;
17706
- if (isFunctionLike$1(current)) return false;
18079
+ if (isFunctionLike$2(current)) return false;
17707
18080
  current = current.parent;
17708
18081
  }
17709
18082
  return false;
@@ -17946,7 +18319,7 @@ const noMoment = defineRule({
17946
18319
  });
17947
18320
  //#endregion
17948
18321
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
17949
- const buildMessage$12 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
18322
+ const buildMessage$14 = (componentName) => `Declare only one React component per file. Found extra component: ${componentName}.`;
17950
18323
  const resolveSettings$16 = (settings) => {
17951
18324
  const reactDoctor = settings?.["react-doctor"];
17952
18325
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -18246,7 +18619,7 @@ const noMultiComp = defineRule({
18246
18619
  category: "Architecture",
18247
18620
  create: (context) => {
18248
18621
  const settings = resolveSettings$16(context.settings);
18249
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
18622
+ const isTestlikeFile = isTestlikeFilename(context.filename);
18250
18623
  return { Program(node) {
18251
18624
  if (isTestlikeFile) return;
18252
18625
  const visitContext = {
@@ -18267,7 +18640,7 @@ const noMultiComp = defineRule({
18267
18640
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
18268
18641
  for (const component of flagged.slice(1)) context.report({
18269
18642
  node: component.reportNode,
18270
- message: buildMessage$12(component.name)
18643
+ message: buildMessage$14(component.name)
18271
18644
  });
18272
18645
  } };
18273
18646
  }
@@ -18344,25 +18717,315 @@ const noMutableInDeps = defineRule({
18344
18717
  }
18345
18718
  });
18346
18719
  //#endregion
18347
- //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
18348
- const MESSAGE$15 = "Reducer mutates its current state and returns the same reference. Return a copied object or array so React can observe the update.";
18349
- const MUTATING_ARRAY_METHODS = new Set([
18350
- "copyWithin",
18351
- "fill",
18352
- "pop",
18353
- "push",
18354
- "reverse",
18355
- "shift",
18356
- "sort",
18357
- "splice",
18358
- "unshift"
18359
- ]);
18360
- const MUTATING_COLLECTION_METHODS = new Set([
18361
- "add",
18362
- "clear",
18363
- "delete",
18364
- "set"
18720
+ //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
18721
+ const LODASH_MUTATOR_NAMES = new Set([
18722
+ "set",
18723
+ "unset",
18724
+ "update",
18725
+ "merge",
18726
+ "defaults",
18727
+ "defaultsDeep",
18728
+ "assign",
18729
+ "assignIn",
18730
+ "pull",
18731
+ "pullAll",
18732
+ "pullAllBy",
18733
+ "pullAt",
18734
+ "remove",
18735
+ "fill"
18365
18736
  ]);
18737
+ const LODASH_MUTATING_MODULES = new Set(["lodash", "lodash-es"]);
18738
+ const isLodashMutatingImport = (sourceValue) => {
18739
+ if (LODASH_MUTATING_MODULES.has(sourceValue)) return true;
18740
+ if (sourceValue.startsWith("lodash/fp") || sourceValue.startsWith("lodash-es/fp")) return false;
18741
+ if (sourceValue.startsWith("lodash/") || sourceValue.startsWith("lodash-es/")) return true;
18742
+ return false;
18743
+ };
18744
+ const isLodashMutatorCall = (callExpression) => {
18745
+ if (!isNodeOfType(callExpression, "CallExpression")) return false;
18746
+ const callee = callExpression.callee;
18747
+ if (isNodeOfType(callee, "Identifier")) {
18748
+ if (!LODASH_MUTATOR_NAMES.has(callee.name)) return false;
18749
+ const initializer = findVariableInitializer(callee, callee.name)?.initializer;
18750
+ if (!initializer) return false;
18751
+ if (!isNodeOfType(initializer, "ImportSpecifier") && !isNodeOfType(initializer, "ImportDefaultSpecifier") && !isNodeOfType(initializer, "ImportNamespaceSpecifier")) return false;
18752
+ const importDeclaration = initializer.parent;
18753
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
18754
+ const sourceValue = importDeclaration.source?.value;
18755
+ if (typeof sourceValue !== "string") return false;
18756
+ return isLodashMutatingImport(sourceValue);
18757
+ }
18758
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
18759
+ const propertyName = getStaticMemberPropertyName(callee);
18760
+ if (!propertyName || !LODASH_MUTATOR_NAMES.has(propertyName)) return false;
18761
+ const receiver = callee.object;
18762
+ if (!isNodeOfType(receiver, "Identifier")) return false;
18763
+ const initializer = findVariableInitializer(receiver, receiver.name)?.initializer;
18764
+ if (!initializer) return false;
18765
+ if (!isNodeOfType(initializer, "ImportNamespaceSpecifier") && !isNodeOfType(initializer, "ImportDefaultSpecifier")) return false;
18766
+ const importDeclaration = initializer.parent;
18767
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
18768
+ const sourceValue = importDeclaration.source?.value;
18769
+ if (typeof sourceValue !== "string") return false;
18770
+ return isLodashMutatingImport(sourceValue);
18771
+ }
18772
+ return false;
18773
+ };
18774
+ //#endregion
18775
+ //#region src/plugin/utils/find-exported-function-body.ts
18776
+ const isFunctionLike = (node) => {
18777
+ if (!node) return false;
18778
+ return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
18779
+ };
18780
+ const findExportedFunctionBody = (programRoot, exportedName) => {
18781
+ if (!isNodeOfType(programRoot, "Program")) return null;
18782
+ const localBindings = /* @__PURE__ */ new Map();
18783
+ const namedExports = /* @__PURE__ */ new Map();
18784
+ let defaultExport = null;
18785
+ let defaultExportIdentifierName = null;
18786
+ const recordVariableDeclaration = (declaration) => {
18787
+ for (const declarator of declaration.declarations ?? []) {
18788
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
18789
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
18790
+ const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
18791
+ if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
18792
+ }
18793
+ };
18794
+ for (const statement of programRoot.body ?? []) {
18795
+ if (isNodeOfType(statement, "VariableDeclaration")) {
18796
+ recordVariableDeclaration(statement);
18797
+ continue;
18798
+ }
18799
+ if (isNodeOfType(statement, "FunctionDeclaration") && statement.id) {
18800
+ localBindings.set(statement.id.name, statement);
18801
+ continue;
18802
+ }
18803
+ if (isNodeOfType(statement, "ExportNamedDeclaration")) {
18804
+ const declaration = statement.declaration;
18805
+ if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
18806
+ recordVariableDeclaration(declaration);
18807
+ for (const declarator of declaration.declarations ?? []) {
18808
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
18809
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
18810
+ namedExports.set(declarator.id.name, declarator.id.name);
18811
+ }
18812
+ } else if (declaration && isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
18813
+ localBindings.set(declaration.id.name, declaration);
18814
+ namedExports.set(declaration.id.name, declaration.id.name);
18815
+ }
18816
+ for (const specifier of statement.specifiers ?? []) {
18817
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
18818
+ const local = specifier.local;
18819
+ const exported = specifier.exported;
18820
+ if (!isNodeOfType(local, "Identifier")) continue;
18821
+ const exportedNameSpec = isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null;
18822
+ if (!exportedNameSpec) continue;
18823
+ namedExports.set(exportedNameSpec, local.name);
18824
+ }
18825
+ continue;
18826
+ }
18827
+ if (isNodeOfType(statement, "ExportDefaultDeclaration")) {
18828
+ const declaration = statement.declaration;
18829
+ if (!declaration) continue;
18830
+ if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
18831
+ localBindings.set(declaration.id.name, declaration);
18832
+ defaultExport = declaration;
18833
+ continue;
18834
+ }
18835
+ if (isFunctionLike(declaration)) {
18836
+ defaultExport = declaration;
18837
+ continue;
18838
+ }
18839
+ if (isNodeOfType(declaration, "Identifier")) {
18840
+ defaultExportIdentifierName = declaration.name;
18841
+ continue;
18842
+ }
18843
+ }
18844
+ }
18845
+ if (exportedName === "default") {
18846
+ if (defaultExport) return defaultExport;
18847
+ if (defaultExportIdentifierName) {
18848
+ const binding = localBindings.get(defaultExportIdentifierName);
18849
+ if (binding) return binding;
18850
+ }
18851
+ }
18852
+ const localName = namedExports.get(exportedName);
18853
+ if (!localName) return null;
18854
+ return localBindings.get(localName) ?? null;
18855
+ };
18856
+ const resolveImportedExportName = (importSpecifier) => {
18857
+ if (isNodeOfType(importSpecifier, "ImportSpecifier")) {
18858
+ const imported = importSpecifier.imported;
18859
+ if (isNodeOfType(imported, "Identifier")) return imported.name;
18860
+ if (isNodeOfType(imported, "Literal") && typeof imported.value === "string") return imported.value;
18861
+ return null;
18862
+ }
18863
+ if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
18864
+ return null;
18865
+ };
18866
+ const findReExportSourcesForName = (programRoot, exportedName) => {
18867
+ if (!isNodeOfType(programRoot, "Program")) return [];
18868
+ const exportAllSources = [];
18869
+ for (const statement of programRoot.body ?? []) {
18870
+ if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
18871
+ const sourceValue = statement.source.value;
18872
+ if (typeof sourceValue !== "string") continue;
18873
+ for (const specifier of statement.specifiers ?? []) {
18874
+ if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
18875
+ const exported = specifier.exported;
18876
+ if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) === exportedName) return [sourceValue];
18877
+ }
18878
+ }
18879
+ if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
18880
+ const sourceValue = statement.source.value;
18881
+ if (typeof sourceValue === "string") exportAllSources.push(sourceValue);
18882
+ }
18883
+ }
18884
+ return exportAllSources;
18885
+ };
18886
+ //#endregion
18887
+ //#region src/plugin/utils/attach-parent-references.ts
18888
+ const attachParentReferences = (root) => {
18889
+ const visit = (node, parent) => {
18890
+ const writableNode = node;
18891
+ writableNode.parent = parent;
18892
+ const nodeRecord = node;
18893
+ for (const key of Object.keys(nodeRecord)) {
18894
+ if (key === "parent") continue;
18895
+ const child = nodeRecord[key];
18896
+ if (Array.isArray(child)) {
18897
+ for (const item of child) if (isAstNode(item)) visit(item, node);
18898
+ } else if (isAstNode(child)) visit(child, node);
18899
+ }
18900
+ };
18901
+ visit(root, null);
18902
+ };
18903
+ //#endregion
18904
+ //#region src/plugin/utils/parse-source-file.ts
18905
+ const FILENAME_TO_LANG = {
18906
+ ".ts": "ts",
18907
+ ".tsx": "tsx",
18908
+ ".js": "js",
18909
+ ".jsx": "jsx",
18910
+ ".mjs": "js",
18911
+ ".cjs": "js",
18912
+ ".mts": "ts",
18913
+ ".cts": "ts"
18914
+ };
18915
+ const resolveLang = (filename) => {
18916
+ return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
18917
+ };
18918
+ const parseCache = /* @__PURE__ */ new Map();
18919
+ const parseSourceFile = (absoluteFilePath) => {
18920
+ let fileStat;
18921
+ try {
18922
+ fileStat = fs.statSync(absoluteFilePath);
18923
+ } catch {
18924
+ return null;
18925
+ }
18926
+ if (!fileStat.isFile()) return null;
18927
+ if (fileStat.size > 2e6) return null;
18928
+ const cached = parseCache.get(absoluteFilePath);
18929
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
18930
+ if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
18931
+ parseCache.set(absoluteFilePath, {
18932
+ mtimeMs: fileStat.mtimeMs,
18933
+ size: fileStat.size,
18934
+ program: null
18935
+ });
18936
+ return null;
18937
+ }
18938
+ let sourceText;
18939
+ try {
18940
+ sourceText = fs.readFileSync(absoluteFilePath, "utf8");
18941
+ } catch {
18942
+ parseCache.set(absoluteFilePath, {
18943
+ mtimeMs: fileStat.mtimeMs,
18944
+ size: fileStat.size,
18945
+ program: null
18946
+ });
18947
+ return null;
18948
+ }
18949
+ let parsedProgram = null;
18950
+ try {
18951
+ const result = parseSync(absoluteFilePath, sourceText, {
18952
+ astType: "ts",
18953
+ lang: resolveLang(absoluteFilePath)
18954
+ });
18955
+ if (!result.errors.some((parseError) => parseError.severity === "Error")) {
18956
+ parsedProgram = result.program;
18957
+ attachParentReferences(parsedProgram);
18958
+ }
18959
+ } catch {
18960
+ parsedProgram = null;
18961
+ }
18962
+ parseCache.set(absoluteFilePath, {
18963
+ mtimeMs: fileStat.mtimeMs,
18964
+ size: fileStat.size,
18965
+ program: parsedProgram
18966
+ });
18967
+ return parsedProgram;
18968
+ };
18969
+ //#endregion
18970
+ //#region src/plugin/rules/state-and-effects/utils/resolve-reducer-function.ts
18971
+ const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
18972
+ if (visitedFilePaths.size >= 4) return null;
18973
+ if (visitedFilePaths.has(filePath)) return null;
18974
+ visitedFilePaths.add(filePath);
18975
+ const actualFilePath = resolveBarrelExportFilePath(filePath, exportedName) ?? filePath;
18976
+ const programRoot = parseSourceFile(actualFilePath);
18977
+ if (!programRoot) return null;
18978
+ const exported = findExportedFunctionBody(programRoot, exportedName);
18979
+ if (exported) return exported;
18980
+ for (const reExportSource of findReExportSourcesForName(programRoot, exportedName)) {
18981
+ const nextFilePath = resolveRelativeImportPath(actualFilePath, reExportSource);
18982
+ if (!nextFilePath) continue;
18983
+ const resolved = resolveFunctionExportInFile(nextFilePath, exportedName, visitedFilePaths);
18984
+ if (resolved) return resolved;
18985
+ }
18986
+ return null;
18987
+ };
18988
+ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
18989
+ const resolvedFilePath = resolveRelativeImportPath(fromFilename, source);
18990
+ if (!resolvedFilePath) return null;
18991
+ return resolveFunctionExportInFile(resolvedFilePath, exportedName, /* @__PURE__ */ new Set());
18992
+ };
18993
+ const resolveReducerFunction = (node, currentFilename) => {
18994
+ if (!node) return null;
18995
+ const unwrappedNode = stripParenExpression(node);
18996
+ if (isFunctionLike$2(unwrappedNode)) return {
18997
+ functionNode: unwrappedNode,
18998
+ crossFileSourceDisplay: null
18999
+ };
19000
+ if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
19001
+ const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
19002
+ if (!initializer) return null;
19003
+ const unwrappedInitializer = stripParenExpression(initializer);
19004
+ if (isFunctionLike$2(unwrappedInitializer)) return {
19005
+ functionNode: unwrappedInitializer,
19006
+ crossFileSourceDisplay: null
19007
+ };
19008
+ if (isNodeOfType(initializer, "ImportSpecifier") || isNodeOfType(initializer, "ImportDefaultSpecifier")) {
19009
+ if (!currentFilename) return null;
19010
+ const importDeclaration = initializer.parent;
19011
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
19012
+ const sourceValue = importDeclaration.source?.value;
19013
+ if (typeof sourceValue !== "string") return null;
19014
+ if (!sourceValue.startsWith(".") && !sourceValue.startsWith("/")) return null;
19015
+ const exportedName = resolveImportedExportName(initializer);
19016
+ if (!exportedName) return null;
19017
+ const crossFileFunction = resolveCrossFileFunctionExport(currentFilename, sourceValue, exportedName);
19018
+ if (!crossFileFunction) return null;
19019
+ return {
19020
+ functionNode: crossFileFunction,
19021
+ crossFileSourceDisplay: sourceValue
19022
+ };
19023
+ }
19024
+ return null;
19025
+ };
19026
+ //#endregion
19027
+ //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
19028
+ const MESSAGE$16 = "Reducer mutates its current state and returns the same reference. Return a copied object or array so React can observe the update.";
18366
19029
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
18367
19030
  "copyWithin",
18368
19031
  "fill",
@@ -18381,7 +19044,6 @@ const cloneReducerPathState = (state) => ({
18381
19044
  mutableStateSourceNames: new Set(state.mutableStateSourceNames),
18382
19045
  mutations: [...state.mutations]
18383
19046
  });
18384
- const isFunctionLikeAstNode = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
18385
19047
  const isSpecifierImportedFromReact = (node) => {
18386
19048
  const parent = node.parent ?? null;
18387
19049
  return parent !== null && isNodeOfType(parent, "ImportDeclaration") && parent.source.value === "react";
@@ -18408,19 +19070,16 @@ const isCallToImportedReactUseReducer = (node) => {
18408
19070
  const binding = findVariableInitializer(callee.object, callee.object.name);
18409
19071
  return Boolean(binding?.initializer && isReactNamespaceOrDefaultImportSpecifier(binding.initializer));
18410
19072
  };
18411
- const resolveSameFileReducerFunction = (node) => {
18412
- if (!node) return null;
18413
- const unwrappedNode = stripParenExpression(node);
18414
- if (isFunctionLikeAstNode(unwrappedNode)) return unwrappedNode;
18415
- if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
18416
- const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
18417
- if (!initializer) return null;
18418
- const unwrappedInitializer = stripParenExpression(initializer);
18419
- return isFunctionLikeAstNode(unwrappedInitializer) ? unwrappedInitializer : null;
18420
- };
18421
19073
  const isStaticMethodCallOnNamedObject = (node, objectName, methodNames) => {
18422
19074
  const unwrappedNode = stripParenExpression(node);
18423
- return Boolean(isNodeOfType(unwrappedNode, "CallExpression") && isNodeOfType(unwrappedNode.callee, "MemberExpression") && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === objectName && methodNames.has(getStaticMemberPropertyName(unwrappedNode.callee) ?? ""));
19075
+ if (!isNodeOfType(unwrappedNode, "CallExpression")) return false;
19076
+ if (!isNodeOfType(unwrappedNode.callee, "MemberExpression")) return false;
19077
+ const calleeObject = unwrappedNode.callee.object;
19078
+ if (!isNodeOfType(calleeObject, "Identifier")) return false;
19079
+ if (calleeObject.name !== objectName) return false;
19080
+ if (!methodNames.has(getStaticMemberPropertyName(unwrappedNode.callee) ?? "")) return false;
19081
+ if (findVariableInitializer(calleeObject, calleeObject.name)) return false;
19082
+ return true;
18424
19083
  };
18425
19084
  const isExpressionRootedInMutableReducerStateSource = (node, state) => {
18426
19085
  let current = stripParenExpression(node);
@@ -18456,11 +19115,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
18456
19115
  return false;
18457
19116
  };
18458
19117
  const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
18459
- if (isFunctionLikeAstNode(node)) return [];
19118
+ if (isFunctionLike$2(node)) return [];
18460
19119
  const mutations = [];
18461
19120
  walkAst(node, (child) => {
18462
19121
  const unwrappedChild = stripParenExpression(child);
18463
- if (child !== node && isFunctionLikeAstNode(unwrappedChild)) return false;
19122
+ if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
18464
19123
  if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
18465
19124
  if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
18466
19125
  return;
@@ -18479,6 +19138,10 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
18479
19138
  mutations.push({ node: unwrappedChild });
18480
19139
  return;
18481
19140
  }
19141
+ if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && isLodashMutatorCall(unwrappedChild)) {
19142
+ mutations.push({ node: unwrappedChild });
19143
+ return;
19144
+ }
18482
19145
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
18483
19146
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
18484
19147
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
@@ -18505,18 +19168,47 @@ const restoreOuterIdentityForBlockScopedNames = (blockState, outerState, blockSc
18505
19168
  }
18506
19169
  return nextState;
18507
19170
  };
19171
+ const recordDestructuredAliasNames = (pattern, state) => {
19172
+ if (isNodeOfType(pattern, "ObjectPattern")) {
19173
+ for (const property of pattern.properties ?? []) {
19174
+ if (!isNodeOfType(property, "Property")) continue;
19175
+ const valueNode = property.value;
19176
+ let leafIdentifier = null;
19177
+ if (isNodeOfType(valueNode, "Identifier")) leafIdentifier = valueNode;
19178
+ else if (isNodeOfType(valueNode, "AssignmentPattern") && isNodeOfType(valueNode.left, "Identifier")) leafIdentifier = valueNode.left;
19179
+ if (!leafIdentifier) continue;
19180
+ state.mutableStateSourceNames.add(leafIdentifier.name);
19181
+ }
19182
+ return;
19183
+ }
19184
+ if (isNodeOfType(pattern, "ArrayPattern")) for (const element of pattern.elements ?? []) {
19185
+ if (!element) continue;
19186
+ if (isNodeOfType(element, "Identifier")) {
19187
+ state.mutableStateSourceNames.add(element.name);
19188
+ continue;
19189
+ }
19190
+ if (isNodeOfType(element, "AssignmentPattern") && isNodeOfType(element.left, "Identifier")) {
19191
+ state.mutableStateSourceNames.add(element.left.name);
19192
+ continue;
19193
+ }
19194
+ if (isNodeOfType(element, "RestElement") && isNodeOfType(element.argument, "Identifier")) {}
19195
+ }
19196
+ };
18508
19197
  const updateReducerStateIdentityForVariableDeclaration = (declaration, state) => {
18509
19198
  for (const declarator of declaration.declarations ?? []) {
18510
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
18511
- const name = declarator.id.name;
18512
- state.originalStateReferenceNames.delete(name);
18513
- state.mutableStateSourceNames.delete(name);
18514
- if (isExpressionOriginalReducerStateReference(declarator.init, state)) {
18515
- state.originalStateReferenceNames.add(name);
18516
- state.mutableStateSourceNames.add(name);
19199
+ if (isNodeOfType(declarator.id, "Identifier")) {
19200
+ const name = declarator.id.name;
19201
+ state.originalStateReferenceNames.delete(name);
19202
+ state.mutableStateSourceNames.delete(name);
19203
+ if (isExpressionOriginalReducerStateReference(declarator.init, state)) {
19204
+ state.originalStateReferenceNames.add(name);
19205
+ state.mutableStateSourceNames.add(name);
19206
+ continue;
19207
+ }
19208
+ if (isExpressionReachableFromOriginalReducerState(declarator.init, state)) state.mutableStateSourceNames.add(name);
18517
19209
  continue;
18518
19210
  }
18519
- if (isExpressionReachableFromOriginalReducerState(declarator.init, state)) state.mutableStateSourceNames.add(name);
19211
+ if ((isNodeOfType(declarator.id, "ObjectPattern") || isNodeOfType(declarator.id, "ArrayPattern")) && isExpressionReachableFromOriginalReducerState(declarator.init, state)) recordDestructuredAliasNames(declarator.id, state);
18520
19212
  }
18521
19213
  };
18522
19214
  const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) => {
@@ -18531,24 +19223,40 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
18531
19223
  }
18532
19224
  if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
18533
19225
  };
18534
- const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes) => {
18535
- if (!isFunctionLikeAstNode(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
19226
+ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
19227
+ if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
18536
19228
  const firstParam = functionNode.params?.[0];
18537
19229
  const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
18538
19230
  if (!stateName) return;
18539
19231
  const reportReducerStateMutations = (mutations) => {
19232
+ if (mutations.length === 0) return;
19233
+ if (options.crossFileConsumerCallSite && options.crossFileSourceDisplay) {
19234
+ if (reportedNodes.has(options.crossFileConsumerCallSite)) return;
19235
+ reportedNodes.add(options.crossFileConsumerCallSite);
19236
+ context.report({
19237
+ node: options.crossFileConsumerCallSite,
19238
+ message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
19239
+ });
19240
+ return;
19241
+ }
18540
19242
  for (const mutation of mutations) {
18541
19243
  if (reportedNodes.has(mutation.node)) continue;
18542
19244
  reportedNodes.add(mutation.node);
18543
19245
  context.report({
18544
19246
  node: mutation.node,
18545
- message: MESSAGE$15
19247
+ message: MESSAGE$16
18546
19248
  });
18547
19249
  }
18548
19250
  };
19251
+ let pathBudgetExceeded = false;
18549
19252
  const analyzeReducerStatementListByPath = (statements, initialState) => {
19253
+ if (pathBudgetExceeded) return [cloneReducerPathState(initialState)];
18550
19254
  let activeStates = [cloneReducerPathState(initialState)];
18551
19255
  for (const statement of statements) {
19256
+ if (activeStates.length > 1e3) {
19257
+ pathBudgetExceeded = true;
19258
+ break;
19259
+ }
18552
19260
  const nextStates = [];
18553
19261
  for (const activeState of activeStates) {
18554
19262
  if (isNodeOfType(statement, "ReturnStatement")) {
@@ -18617,18 +19325,23 @@ const noMutatingReducerState = defineRule({
18617
19325
  create: (context) => {
18618
19326
  const analyzedReducers = /* @__PURE__ */ new WeakSet();
18619
19327
  const reportedNodes = /* @__PURE__ */ new WeakSet();
19328
+ const currentFilename = context.filename;
18620
19329
  return { CallExpression(node) {
18621
19330
  if (!isCallToImportedReactUseReducer(node)) return;
18622
- const reducerFunction = resolveSameFileReducerFunction(node.arguments?.[0]);
18623
- if (!reducerFunction || analyzedReducers.has(reducerFunction)) return;
18624
- analyzedReducers.add(reducerFunction);
18625
- analyzeReactUseReducerFunctionForStateMutation(context, reducerFunction, reportedNodes);
19331
+ const resolved = resolveReducerFunction(node.arguments?.[0], currentFilename);
19332
+ if (!resolved) return;
19333
+ if (analyzedReducers.has(resolved.functionNode)) return;
19334
+ analyzedReducers.add(resolved.functionNode);
19335
+ analyzeReactUseReducerFunctionForStateMutation(context, resolved.functionNode, reportedNodes, {
19336
+ crossFileConsumerCallSite: resolved.crossFileSourceDisplay ? node : null,
19337
+ crossFileSourceDisplay: resolved.crossFileSourceDisplay
19338
+ });
18626
19339
  } };
18627
19340
  }
18628
19341
  });
18629
19342
  //#endregion
18630
19343
  //#region src/plugin/rules/react-builtins/no-namespace.ts
18631
- const buildMessage$11 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
19344
+ const buildMessage$13 = (componentName) => `React component \`${componentName}\` must not be in a namespace — React doesn't support them.`;
18632
19345
  const noNamespace = defineRule({
18633
19346
  id: "no-namespace",
18634
19347
  severity: "warn",
@@ -18640,7 +19353,7 @@ const noNamespace = defineRule({
18640
19353
  const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
18641
19354
  context.report({
18642
19355
  node: namespaced,
18643
- message: buildMessage$11(fullName)
19356
+ message: buildMessage$13(fullName)
18644
19357
  });
18645
19358
  },
18646
19359
  CallExpression(node) {
@@ -18651,7 +19364,7 @@ const noNamespace = defineRule({
18651
19364
  if (!firstArgument.value.includes(":")) return;
18652
19365
  context.report({
18653
19366
  node: firstArgument,
18654
- message: buildMessage$11(firstArgument.value)
19367
+ message: buildMessage$13(firstArgument.value)
18655
19368
  });
18656
19369
  }
18657
19370
  })
@@ -18695,7 +19408,7 @@ const noNestedComponentDefinition = defineRule({
18695
19408
  });
18696
19409
  //#endregion
18697
19410
  //#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
18698
- const buildMessage$10 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
19411
+ const buildMessage$12 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
18699
19412
  const INTERACTIVE_HANDLERS = [
18700
19413
  "onClick",
18701
19414
  "onMouseDown",
@@ -18721,13 +19434,13 @@ const noNoninteractiveElementInteractions = defineRule({
18721
19434
  }
18722
19435
  context.report({
18723
19436
  node: node.name,
18724
- message: buildMessage$10(tag)
19437
+ message: buildMessage$12(tag)
18725
19438
  });
18726
19439
  } })
18727
19440
  });
18728
19441
  //#endregion
18729
19442
  //#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
18730
- const buildMessage$9 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
19443
+ const buildMessage$11 = (tag, role) => `Non-interactive element \`<${tag}>\` cannot have interactive role \`${role}\` — use a semantic interactive element instead.`;
18731
19444
  const DEFAULT_ALLOWED_ROLES = {
18732
19445
  ul: [
18733
19446
  "menu",
@@ -18791,14 +19504,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
18791
19504
  if (!isInteractiveRole(firstRole)) return;
18792
19505
  context.report({
18793
19506
  node: roleAttribute,
18794
- message: buildMessage$9(elementType, firstRole)
19507
+ message: buildMessage$11(elementType, firstRole)
18795
19508
  });
18796
19509
  } };
18797
19510
  }
18798
19511
  });
18799
19512
  //#endregion
18800
19513
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
18801
- const MESSAGE$14 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
19514
+ const MESSAGE$15 = "Don't add `tabIndex` to non-interactive elements — keyboard users would have no expected behavior on focus.";
18802
19515
  const resolveSettings$14 = (settings) => {
18803
19516
  const reactDoctor = settings?.["react-doctor"];
18804
19517
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -18825,7 +19538,7 @@ const noNoninteractiveTabindex = defineRule({
18825
19538
  if (numeric === null) {
18826
19539
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
18827
19540
  node: tabIndex,
18828
- message: MESSAGE$14
19541
+ message: MESSAGE$15
18829
19542
  });
18830
19543
  return;
18831
19544
  }
@@ -18838,7 +19551,7 @@ const noNoninteractiveTabindex = defineRule({
18838
19551
  if (!roleAttribute) {
18839
19552
  context.report({
18840
19553
  node: tabIndex,
18841
- message: MESSAGE$14
19554
+ message: MESSAGE$15
18842
19555
  });
18843
19556
  return;
18844
19557
  }
@@ -18852,7 +19565,7 @@ const noNoninteractiveTabindex = defineRule({
18852
19565
  }
18853
19566
  context.report({
18854
19567
  node: tabIndex,
18855
- message: MESSAGE$14
19568
+ message: MESSAGE$15
18856
19569
  });
18857
19570
  } };
18858
19571
  }
@@ -19381,6 +20094,63 @@ const noPropCallbackInEffect = defineRule({
19381
20094
  }
19382
20095
  });
19383
20096
  //#endregion
20097
+ //#region src/plugin/rules/architecture/no-prop-types.ts
20098
+ const PROP_TYPES_PROPERTY = "propTypes";
20099
+ const isPropTypesKey = (key, computed) => {
20100
+ if (!key) return false;
20101
+ if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
20102
+ return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
20103
+ };
20104
+ const getComponentNameFromPropTypesAssignment = (left) => {
20105
+ if (!isNodeOfType(left, "MemberExpression")) return null;
20106
+ if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
20107
+ if (!isNodeOfType(left.object, "Identifier")) return null;
20108
+ if (!isUppercaseName(left.object.name)) return null;
20109
+ return left.object.name;
20110
+ };
20111
+ const getComponentNameFromClassProperty = (node) => {
20112
+ if (!node.static) return null;
20113
+ if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
20114
+ const classBody = node.parent;
20115
+ if (!isNodeOfType(classBody, "ClassBody")) return null;
20116
+ const classNode = classBody.parent;
20117
+ if (!classNode) return null;
20118
+ if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
20119
+ if (!isNodeOfType(classNode, "ClassExpression")) return null;
20120
+ const declarator = classNode.parent;
20121
+ if (!isNodeOfType(declarator, "VariableDeclarator")) return null;
20122
+ if (!isNodeOfType(declarator.id, "Identifier")) return null;
20123
+ if (!isUppercaseName(declarator.id.name)) return null;
20124
+ return declarator.id.name;
20125
+ };
20126
+ const buildMessage$10 = (componentName) => `${componentName}.propTypes — React 19 no longer runs \`propTypes\` checks, so invalid props pass silently. Move the prop contract to TypeScript types and add explicit runtime validation only where data can actually be invalid`;
20127
+ const noPropTypes = defineRule({
20128
+ id: "no-prop-types",
20129
+ requires: ["react:19"],
20130
+ tags: ["test-noise"],
20131
+ severity: "warn",
20132
+ recommendation: "React 19 removed runtime `propTypes` validation — React no longer reads `Component.propTypes`, so invalid props pass silently. Describe props with TypeScript types and move any required runtime validation to explicit checks (or schema parsing) in component code. Only enabled on projects detected as React 19+.",
20133
+ create: (context) => ({
20134
+ AssignmentExpression(node) {
20135
+ if (node.operator !== "=") return;
20136
+ const componentName = getComponentNameFromPropTypesAssignment(node.left);
20137
+ if (!componentName) return;
20138
+ context.report({
20139
+ node: node.left,
20140
+ message: buildMessage$10(componentName)
20141
+ });
20142
+ },
20143
+ PropertyDefinition(node) {
20144
+ const componentName = getComponentNameFromClassProperty(node);
20145
+ if (!componentName) return;
20146
+ context.report({
20147
+ node: node.key,
20148
+ message: buildMessage$10(componentName)
20149
+ });
20150
+ }
20151
+ })
20152
+ });
20153
+ //#endregion
19384
20154
  //#region src/plugin/rules/design/no-pure-black-background.ts
19385
20155
  const noPureBlackBackground = defineRule({
19386
20156
  id: "no-pure-black-background",
@@ -19412,8 +20182,94 @@ const noPureBlackBackground = defineRule({
19412
20182
  })
19413
20183
  });
19414
20184
  //#endregion
20185
+ //#region src/plugin/rules/correctness/no-random-key.ts
20186
+ const ALWAYS_FRESH_DIRECT_CALLEES = new Set([
20187
+ "nanoid",
20188
+ "uuid",
20189
+ "uuidv4",
20190
+ "uuidV4",
20191
+ "v4",
20192
+ "cuid",
20193
+ "cuid2",
20194
+ "createId",
20195
+ "ulid",
20196
+ "objectid",
20197
+ "ObjectId",
20198
+ "shortid"
20199
+ ]);
20200
+ const ALWAYS_FRESH_MEMBER_RECEIVERS = new Map([
20201
+ ["Math", new Set(["random"])],
20202
+ ["Date", new Set(["now"])],
20203
+ ["performance", new Set(["now"])],
20204
+ ["crypto", new Set([
20205
+ "randomUUID",
20206
+ "getRandomValues",
20207
+ "randomBytes"
20208
+ ])]
20209
+ ]);
20210
+ const isAlwaysFreshExpression = (expression) => {
20211
+ const stripped = stripParenExpression(expression);
20212
+ if (isNodeOfType(stripped, "NewExpression")) {
20213
+ if (isNodeOfType(stripped.callee, "Identifier") && stripped.callee.name === "Date") return "new Date()";
20214
+ }
20215
+ if (!isNodeOfType(stripped, "CallExpression")) return null;
20216
+ const callee = stripped.callee;
20217
+ if (isNodeOfType(callee, "Identifier")) {
20218
+ if (!ALWAYS_FRESH_DIRECT_CALLEES.has(callee.name)) return null;
20219
+ const binding = findVariableInitializer(callee, callee.name);
20220
+ if (binding?.initializer && !isNodeOfType(binding.initializer, "ImportSpecifier") && !isNodeOfType(binding.initializer, "ImportDefaultSpecifier") && !isNodeOfType(binding.initializer, "ImportNamespaceSpecifier")) return null;
20221
+ return `${callee.name}()`;
20222
+ }
20223
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
20224
+ const receiver = callee.object;
20225
+ const property = callee.property;
20226
+ if (!isNodeOfType(property, "Identifier")) return null;
20227
+ if (isNodeOfType(receiver, "Identifier")) {
20228
+ if (ALWAYS_FRESH_MEMBER_RECEIVERS.get(receiver.name)?.has(property.name)) return `${receiver.name}.${property.name}()`;
20229
+ }
20230
+ }
20231
+ return null;
20232
+ };
20233
+ const variableLabelForUpdateArgument = (argument) => {
20234
+ if (!argument) return "counter";
20235
+ const stripped = stripParenExpression(argument);
20236
+ if (isNodeOfType(stripped, "Identifier")) return stripped.name;
20237
+ if (isNodeOfType(stripped, "MemberExpression") && !stripped.computed && isNodeOfType(stripped.property, "Identifier")) return stripped.property.name;
20238
+ return "counter";
20239
+ };
20240
+ const looksLikeFreshUpdateExpression = (expression) => {
20241
+ const stripped = stripParenExpression(expression);
20242
+ if (isNodeOfType(stripped, "UpdateExpression")) {
20243
+ const label = variableLabelForUpdateArgument(stripped.argument);
20244
+ return stripped.prefix ? `${stripped.operator}${label}` : `${label}${stripped.operator}`;
20245
+ }
20246
+ if (isNodeOfType(stripped, "AssignmentExpression") && (stripped.operator === "+=" || stripped.operator === "-=")) return `${stripped.operator} side-effect`;
20247
+ return null;
20248
+ };
20249
+ const noRandomKey = defineRule({
20250
+ id: "no-random-key",
20251
+ severity: "error",
20252
+ category: "Correctness",
20253
+ recommendation: "Use a stable identifier from the item itself (`item.id`, a hash of the content, or the item's index when the list order is stable). Never derive the key from a fresh-each-call API.",
20254
+ create: (context) => ({ JSXAttribute(node) {
20255
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
20256
+ if (node.name.name !== "key") return;
20257
+ if (!node.value) return;
20258
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
20259
+ const inner = node.value.expression;
20260
+ if (!inner) return;
20261
+ if (inner.type === "JSXEmptyExpression") return;
20262
+ const freshDescription = isAlwaysFreshExpression(inner) ?? looksLikeFreshUpdateExpression(inner);
20263
+ if (!freshDescription) return;
20264
+ context.report({
20265
+ node: node.value,
20266
+ message: `\`key={${freshDescription}}\` produces a new value on every render. Every list item is treated as a brand-new component — React unmounts and remounts the entire subtree, resetting state/focus/scroll and defeating reconciliation. Use a stable id from the item itself.`
20267
+ });
20268
+ } })
20269
+ });
20270
+ //#endregion
19415
20271
  //#region src/plugin/rules/react-builtins/no-react-children.ts
19416
- const MESSAGE$13 = "`React.Children` is uncommon and leads to fragile components.";
20272
+ const MESSAGE$14 = "`React.Children` is uncommon and leads to fragile components.";
19417
20273
  const isChildrenIdentifier = (node, contextNode) => {
19418
20274
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
19419
20275
  return isImportedFromModule(contextNode, "Children", "react");
@@ -19438,13 +20294,13 @@ const noReactChildren = defineRule({
19438
20294
  if (isChildrenIdentifier(memberObject, node)) {
19439
20295
  context.report({
19440
20296
  node: calleeOuter,
19441
- message: MESSAGE$13
20297
+ message: MESSAGE$14
19442
20298
  });
19443
20299
  return;
19444
20300
  }
19445
20301
  if (isReactNamespaceMember(memberObject, node)) context.report({
19446
20302
  node: calleeOuter,
19447
- message: MESSAGE$13
20303
+ message: MESSAGE$14
19448
20304
  });
19449
20305
  } })
19450
20306
  });
@@ -19640,7 +20496,7 @@ const getTagsForRole = (role) => {
19640
20496
  };
19641
20497
  //#endregion
19642
20498
  //#region src/plugin/rules/a11y/no-redundant-roles.ts
19643
- const buildMessage$8 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
20499
+ const buildMessage$9 = (tag, role) => `\`<${tag}>\` already has implicit role \`${role}\` — remove the redundant \`role\` attribute.`;
19644
20500
  const resolveSettings$13 = (settings) => {
19645
20501
  const reactDoctor = settings?.["react-doctor"];
19646
20502
  return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
@@ -19663,14 +20519,14 @@ const noRedundantRoles = defineRule({
19663
20519
  const allowedHere = settings.exceptions[tag] ?? [];
19664
20520
  if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
19665
20521
  node: roleAttr,
19666
- message: buildMessage$8(tag, role)
20522
+ message: buildMessage$9(tag, role)
19667
20523
  });
19668
20524
  } };
19669
20525
  }
19670
20526
  });
19671
20527
  //#endregion
19672
20528
  //#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
19673
- const buildMessage$7 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
20529
+ const buildMessage$8 = (className) => `${className} does not need \`shouldComponentUpdate\` when extending \`React.PureComponent\`.`;
19674
20530
  const isPureComponentSuper = (superClass) => {
19675
20531
  if (!superClass) return false;
19676
20532
  if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
@@ -19702,7 +20558,7 @@ const noRedundantShouldComponentUpdate = defineRule({
19702
20558
  const className = classNode.id?.name ?? "<anonymous class>";
19703
20559
  context.report({
19704
20560
  node: reportNode,
19705
- message: buildMessage$7(className)
20561
+ message: buildMessage$8(className)
19706
20562
  });
19707
20563
  };
19708
20564
  return {
@@ -19761,7 +20617,7 @@ const noRenderPropChildren = defineRule({
19761
20617
  });
19762
20618
  //#endregion
19763
20619
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
19764
- const MESSAGE$12 = "Do not use the return value from `ReactDOM.render`.";
20620
+ const MESSAGE$13 = "Do not use the return value from `ReactDOM.render`.";
19765
20621
  const isReactDomRenderCall = (node) => {
19766
20622
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
19767
20623
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -19784,7 +20640,7 @@ const noRenderReturnValue = defineRule({
19784
20640
  if (!isUsedAsReturnValue(node.parent)) return;
19785
20641
  context.report({
19786
20642
  node: node.callee,
19787
- message: MESSAGE$12
20643
+ message: MESSAGE$13
19788
20644
  });
19789
20645
  } })
19790
20646
  });
@@ -20179,7 +21035,7 @@ const isTanStackServerFnHandler = (node) => {
20179
21035
  const isInsideServerOnlyScope = (node) => {
20180
21036
  let currentNode = node.parent ?? null;
20181
21037
  while (currentNode) {
20182
- if (isFunctionLike$1(currentNode)) {
21038
+ if (isFunctionLike$2(currentNode)) {
20183
21039
  if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
20184
21040
  }
20185
21041
  currentNode = currentNode.parent ?? null;
@@ -20193,7 +21049,7 @@ const noSecretsInClientCode = defineRule({
20193
21049
  severity: "warn",
20194
21050
  recommendation: "Move secrets to server-only code. Public client environment variables are bundled into browser code and must not contain secrets",
20195
21051
  create: (context) => {
20196
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
21052
+ const filename = normalizeFilename$1(context.filename ?? "");
20197
21053
  const framework = getReactDoctorStringSetting(context.settings, "framework");
20198
21054
  const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
20199
21055
  let shouldUseVariableNameHeuristic = classifySecretFileExposure(filename, {
@@ -20233,6 +21089,363 @@ const noSecretsInClientCode = defineRule({
20233
21089
  }
20234
21090
  });
20235
21091
  //#endregion
21092
+ //#region src/plugin/rules/state-and-effects/no-self-updating-effect.ts
21093
+ const doesConstructFreshReference = (node) => isNodeOfType(node, "ArrayExpression") || isNodeOfType(node, "ObjectExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "Literal") && "regex" in node;
21094
+ const expressionReadsStateValue = (node, stateName) => {
21095
+ if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) return false;
21096
+ if (isNodeOfType(node, "Identifier")) return node.name === stateName;
21097
+ if (isNodeOfType(node, "MemberExpression")) {
21098
+ if (expressionReadsStateValue(node.object, stateName)) return true;
21099
+ return node.computed ? expressionReadsStateValue(node.property, stateName) : false;
21100
+ }
21101
+ if (isNodeOfType(node, "Property")) {
21102
+ if (node.computed && expressionReadsStateValue(node.key, stateName)) return true;
21103
+ return expressionReadsStateValue(node.value, stateName);
21104
+ }
21105
+ const nodeRecord = node;
21106
+ for (const childKey of Object.keys(nodeRecord)) {
21107
+ if (childKey === "parent" || childKey === "type") continue;
21108
+ const childValue = nodeRecord[childKey];
21109
+ if (Array.isArray(childValue)) {
21110
+ for (const childArrayItem of childValue) if (isAstNode(childArrayItem) && expressionReadsStateValue(childArrayItem, stateName)) return true;
21111
+ } else if (isAstNode(childValue) && expressionReadsStateValue(childValue, stateName)) return true;
21112
+ }
21113
+ return false;
21114
+ };
21115
+ const isNonSettlingSetterArgument = (setterCall, stateName) => {
21116
+ const firstArgument = setterCall.arguments?.[0];
21117
+ if (!firstArgument) return false;
21118
+ const argument = stripParenExpression(firstArgument);
21119
+ if (isNodeOfType(argument, "Identifier") && argument.name === stateName) return false;
21120
+ if (isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression")) return true;
21121
+ if (doesConstructFreshReference(argument)) return true;
21122
+ return expressionReadsStateValue(argument, stateName);
21123
+ };
21124
+ const getUnconditionalSetterCall = (statement, setterNames) => {
21125
+ const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
21126
+ if (!isNodeOfType(expression, "CallExpression")) return null;
21127
+ if (!isNodeOfType(expression.callee, "Identifier")) return null;
21128
+ if (!setterNames.has(expression.callee.name)) return null;
21129
+ return expression;
21130
+ };
21131
+ const collectDependencyStateNames = (depsNode) => {
21132
+ const dependencyNames = /* @__PURE__ */ new Set();
21133
+ if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyNames;
21134
+ for (const element of depsNode.elements ?? []) if (isNodeOfType(element, "Identifier")) dependencyNames.add(element.name);
21135
+ return dependencyNames;
21136
+ };
21137
+ const isEarlyReturnGuard = (statement) => {
21138
+ if (!isNodeOfType(statement, "IfStatement")) return false;
21139
+ const consequent = statement.consequent;
21140
+ if (isNodeOfType(consequent, "ReturnStatement")) return true;
21141
+ if (isNodeOfType(consequent, "BlockStatement")) return (consequent.body ?? []).some((inner) => isNodeOfType(inner, "ReturnStatement"));
21142
+ return false;
21143
+ };
21144
+ const numericLiteralValue = (node) => {
21145
+ if (isNodeOfType(node, "Literal") && typeof node.value === "number") return node.value;
21146
+ if (isNodeOfType(node, "UnaryExpression") && node.operator === "-" && isNodeOfType(node.argument, "Literal") && typeof node.argument.value === "number") return -node.argument.value;
21147
+ return null;
21148
+ };
21149
+ const isStateLength = (node, stateName) => {
21150
+ const member = isNodeOfType(node, "ChainExpression") ? node.expression : node;
21151
+ return isNodeOfType(member, "MemberExpression") && !member.computed && isNodeOfType(member.property, "Identifier") && member.property.name === "length" && expressionReadsStateValue(member.object, stateName);
21152
+ };
21153
+ const isNullishLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined";
21154
+ const numericComparisonHolds = (operator, left, right) => {
21155
+ switch (operator) {
21156
+ case "<": return left < right;
21157
+ case "<=": return left <= right;
21158
+ case ">": return left > right;
21159
+ case ">=": return left >= right;
21160
+ case "===":
21161
+ case "==": return left === right;
21162
+ case "!==":
21163
+ case "!=": return left !== right;
21164
+ default: return false;
21165
+ }
21166
+ };
21167
+ const guardExitsWhenStateEmpty = (test, stateName) => {
21168
+ const node = isNodeOfType(test, "ChainExpression") ? test.expression : test;
21169
+ if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return isStateLength(node.argument, stateName);
21170
+ if (isNodeOfType(node, "LogicalExpression")) {
21171
+ if (node.operator === "||") return guardExitsWhenStateEmpty(node.left, stateName) || guardExitsWhenStateEmpty(node.right, stateName);
21172
+ if (node.operator === "&&") return guardExitsWhenStateEmpty(node.left, stateName) && guardExitsWhenStateEmpty(node.right, stateName);
21173
+ return false;
21174
+ }
21175
+ if (isNodeOfType(node, "BinaryExpression")) {
21176
+ const leftIsLength = isStateLength(node.left, stateName);
21177
+ const rightIsLength = isStateLength(node.right, stateName);
21178
+ if (leftIsLength || rightIsLength) {
21179
+ const other = numericLiteralValue(leftIsLength ? node.right : node.left);
21180
+ if (other === null) return false;
21181
+ return leftIsLength ? numericComparisonHolds(node.operator, 0, other) : numericComparisonHolds(node.operator, other, 0);
21182
+ }
21183
+ if (node.operator === "==" || node.operator === "===") {
21184
+ if (expressionReadsStateValue(node.left, stateName) && isNullishLiteral(node.right)) return true;
21185
+ if (expressionReadsStateValue(node.right, stateName) && isNullishLiteral(node.left)) return true;
21186
+ }
21187
+ return false;
21188
+ }
21189
+ return false;
21190
+ };
21191
+ const isEmptyOrFalsyValue = (node) => {
21192
+ if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).length === 0;
21193
+ if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).length === 0;
21194
+ if (isNodeOfType(node, "Literal")) return node.value === null || node.value === "" || node.value === 0 || node.value === false;
21195
+ if (isNodeOfType(node, "Identifier")) return node.name === "undefined";
21196
+ return false;
21197
+ };
21198
+ const functionReturnExpression = (fn) => {
21199
+ if (!isNodeOfType(fn, "ArrowFunctionExpression") && !isNodeOfType(fn, "FunctionExpression")) return null;
21200
+ if (!isNodeOfType(fn.body, "BlockStatement")) return fn.body ? stripParenExpression(fn.body) : null;
21201
+ for (const statement of fn.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) return stripParenExpression(statement.argument);
21202
+ return null;
21203
+ };
21204
+ const isLengthReducingUpdater = (node) => {
21205
+ if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
21206
+ const firstParameter = node.params?.[0];
21207
+ if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
21208
+ const returned = functionReturnExpression(node);
21209
+ if (!returned || !isNodeOfType(returned, "CallExpression")) return false;
21210
+ const callee = returned.callee;
21211
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
21212
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== firstParameter.name) return false;
21213
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "slice") return false;
21214
+ const sliceStart = numericLiteralValue(returned.arguments?.[0]);
21215
+ return sliceStart !== null && sliceStart >= 1;
21216
+ };
21217
+ const writeProvablyConverges = (setterArgument, stateName, earlyReturnGuardTests) => {
21218
+ if (!isEmptyOrFalsyValue(setterArgument) && !isLengthReducingUpdater(setterArgument)) return false;
21219
+ return earlyReturnGuardTests.some((test) => guardExitsWhenStateEmpty(test, stateName));
21220
+ };
21221
+ const SYMBOLIC_DEPTH_LIMIT = 16;
21222
+ const unwrapChain = (node) => {
21223
+ let current = node;
21224
+ for (;;) {
21225
+ const withoutParens = stripParenExpression(current);
21226
+ if (withoutParens !== current) {
21227
+ current = withoutParens;
21228
+ continue;
21229
+ }
21230
+ if (isNodeOfType(current, "ChainExpression")) {
21231
+ current = current.expression;
21232
+ continue;
21233
+ }
21234
+ return current;
21235
+ }
21236
+ };
21237
+ const isUndefinedValue = (node) => isNodeOfType(node, "Identifier") && node.name === "undefined" || isNodeOfType(node, "Literal") && node.value === null;
21238
+ const literalsEqual = (a, b) => isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal") && a.value === b.value;
21239
+ const resolveValueNode = (node, writes, depth, seen) => {
21240
+ if (depth > SYMBOLIC_DEPTH_LIMIT) return null;
21241
+ const current = unwrapChain(node);
21242
+ if (isNodeOfType(current, "Identifier")) {
21243
+ if (seen.has(current.name)) return null;
21244
+ const written = writes.get(current.name);
21245
+ if (written) return resolveValueNode(written, writes, depth + 1, new Set(seen).add(current.name));
21246
+ return current;
21247
+ }
21248
+ if (isNodeOfType(current, "Literal") || isNodeOfType(current, "ArrayExpression") || isNodeOfType(current, "ObjectExpression")) return current;
21249
+ if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier")) {
21250
+ const objectValue = resolveValueNode(current.object, writes, depth + 1, seen);
21251
+ if (objectValue && isNodeOfType(objectValue, "ObjectExpression")) {
21252
+ const propertyKey = current.property.name;
21253
+ const properties = objectValue.properties ?? [];
21254
+ for (let index = properties.length - 1; index >= 0; index--) {
21255
+ const property = properties[index];
21256
+ if (isNodeOfType(property, "SpreadElement")) return null;
21257
+ if (isNodeOfType(property, "Property") && !property.computed && (isNodeOfType(property.key, "Identifier") && property.key.name === propertyKey || isNodeOfType(property.key, "Literal") && property.key.value === propertyKey)) return resolveValueNode(property.value, writes, depth + 1, seen);
21258
+ }
21259
+ }
21260
+ return null;
21261
+ }
21262
+ return null;
21263
+ };
21264
+ const resolveToNumber = (node, writes, depth, seen) => {
21265
+ const value = resolveValueNode(node, writes, depth, seen);
21266
+ if (value && isNodeOfType(value, "Literal") && typeof value.value === "number") return value.value;
21267
+ const current = unwrapChain(node);
21268
+ if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "length") {
21269
+ const objectValue = resolveValueNode(current.object, writes, depth, seen);
21270
+ if (objectValue && isNodeOfType(objectValue, "ArrayExpression")) {
21271
+ const elements = objectValue.elements ?? [];
21272
+ if (!elements.some((element) => element && isNodeOfType(element, "SpreadElement"))) return elements.length;
21273
+ }
21274
+ }
21275
+ return null;
21276
+ };
21277
+ const provablyEqualAfterWrites = (left, right, writes, depth, seen) => {
21278
+ const leftNumber = resolveToNumber(left, writes, depth, seen);
21279
+ const rightNumber = resolveToNumber(right, writes, depth, seen);
21280
+ if (leftNumber !== null && rightNumber !== null) return leftNumber === rightNumber;
21281
+ const a = resolveValueNode(left, writes, depth, seen);
21282
+ const b = resolveValueNode(right, writes, depth, seen);
21283
+ if (!a || !b) return false;
21284
+ if (literalsEqual(a, b)) return true;
21285
+ if (isUndefinedValue(a) && isUndefinedValue(b)) return true;
21286
+ return isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier") && a.name === b.name;
21287
+ };
21288
+ const provablyFalsyAfterWrites = (node, writes, depth, seen) => {
21289
+ const value = resolveValueNode(node, writes, depth, seen);
21290
+ if (value) {
21291
+ if (isUndefinedValue(value)) return true;
21292
+ if (isNodeOfType(value, "Literal")) return value.value === null || value.value === 0 || value.value === false || value.value === "";
21293
+ }
21294
+ return resolveToNumber(node, writes, depth, seen) === 0;
21295
+ };
21296
+ const guardProvenAfterWrites = (test, writes, depth, seen) => {
21297
+ if (depth > SYMBOLIC_DEPTH_LIMIT) return false;
21298
+ const node = unwrapChain(test);
21299
+ if (isNodeOfType(node, "LogicalExpression")) {
21300
+ if (node.operator === "&&") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) && guardProvenAfterWrites(node.right, writes, depth + 1, seen);
21301
+ if (node.operator === "||") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) || guardProvenAfterWrites(node.right, writes, depth + 1, seen);
21302
+ return false;
21303
+ }
21304
+ if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return provablyFalsyAfterWrites(node.argument, writes, depth + 1, seen);
21305
+ if (isNodeOfType(node, "BinaryExpression")) {
21306
+ if (node.operator === "===" || node.operator === "==") return provablyEqualAfterWrites(node.left, node.right, writes, depth + 1, seen);
21307
+ const leftNumber = resolveToNumber(node.left, writes, depth + 1, seen);
21308
+ const rightNumber = resolveToNumber(node.right, writes, depth + 1, seen);
21309
+ if (leftNumber !== null && rightNumber !== null) return numericComparisonHolds(node.operator, leftNumber, rightNumber);
21310
+ return false;
21311
+ }
21312
+ const value = resolveValueNode(node, writes, depth + 1, seen);
21313
+ if (value) {
21314
+ if (isNodeOfType(value, "ArrayExpression") || isNodeOfType(value, "ObjectExpression")) return true;
21315
+ if (isNodeOfType(value, "Literal")) return Boolean(value.value);
21316
+ }
21317
+ return false;
21318
+ };
21319
+ const collectTopLevelWrites = (statements, setterNameToStateName, setterNames) => {
21320
+ const writes = /* @__PURE__ */ new Map();
21321
+ const setterCallNodes = /* @__PURE__ */ new Set();
21322
+ for (const statement of statements) {
21323
+ const setterCall = getUnconditionalSetterCall(statement, setterNames);
21324
+ if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
21325
+ setterCallNodes.add(setterCall);
21326
+ const stateName = setterNameToStateName.get(setterCall.callee.name);
21327
+ if (!stateName) continue;
21328
+ const argument = setterCall.arguments?.[0];
21329
+ if (!argument) continue;
21330
+ const newValue = isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression") ? functionReturnExpression(argument) : stripParenExpression(argument);
21331
+ if (newValue) writes.set(stateName, newValue);
21332
+ }
21333
+ return {
21334
+ writes,
21335
+ setterCallNodes
21336
+ };
21337
+ };
21338
+ const everySetterCall = (root, setterName, inspect) => {
21339
+ let ok = true;
21340
+ const visit = (node) => {
21341
+ if (!ok) return;
21342
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === setterName && !inspect(node)) {
21343
+ ok = false;
21344
+ return;
21345
+ }
21346
+ const record = node;
21347
+ for (const key of Object.keys(record)) {
21348
+ if (key === "parent" || key === "type") continue;
21349
+ const child = record[key];
21350
+ if (Array.isArray(child)) {
21351
+ for (const item of child) if (isAstNode(item)) visit(item);
21352
+ } else if (isAstNode(child)) visit(child);
21353
+ if (!ok) return;
21354
+ }
21355
+ };
21356
+ visit(root);
21357
+ return ok;
21358
+ };
21359
+ const everyWriteToStateDrivesTowardEmpty = (callbackBody, setterName) => everySetterCall(callbackBody, setterName, (call) => {
21360
+ const argument = call.arguments?.[0];
21361
+ if (!argument) return true;
21362
+ const value = stripParenExpression(argument);
21363
+ return isEmptyOrFalsyValue(value) || isLengthReducingUpdater(value);
21364
+ });
21365
+ const everySetterCallIsTopLevel = (callbackBody, setterNames, topLevelSetterCalls) => {
21366
+ let safe = true;
21367
+ const visit = (node) => {
21368
+ if (!safe) return;
21369
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && setterNames.has(node.callee.name) && !topLevelSetterCalls.has(node)) {
21370
+ safe = false;
21371
+ return;
21372
+ }
21373
+ const record = node;
21374
+ for (const key of Object.keys(record)) {
21375
+ if (key === "parent" || key === "type") continue;
21376
+ const child = record[key];
21377
+ if (Array.isArray(child)) {
21378
+ for (const item of child) if (isAstNode(item)) visit(item);
21379
+ } else if (isAstNode(child)) visit(child);
21380
+ if (!safe) return;
21381
+ }
21382
+ };
21383
+ visit(callbackBody);
21384
+ return safe;
21385
+ };
21386
+ const noSelfUpdatingEffect = defineRule({
21387
+ id: "no-self-updating-effect",
21388
+ severity: "warn",
21389
+ tags: ["test-noise"],
21390
+ recommendation: "Remove the feedback loop: derive the value during render, move the write into an event handler, or guard the update so it reaches a fixed point. See https://react.dev/learn/you-might-not-need-an-effect",
21391
+ create: (context) => {
21392
+ const checkFunctionScope = (functionBody) => {
21393
+ if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
21394
+ const useStateBindings = collectUseStateBindings(functionBody);
21395
+ if (useStateBindings.length === 0) return;
21396
+ const setterNameToStateName = /* @__PURE__ */ new Map();
21397
+ for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
21398
+ const setterNames = new Set(setterNameToStateName.keys());
21399
+ for (const statement of functionBody.body ?? []) {
21400
+ if (!isNodeOfType(statement, "ExpressionStatement")) continue;
21401
+ const effectCall = statement.expression;
21402
+ if (!isNodeOfType(effectCall, "CallExpression")) continue;
21403
+ if (!isHookCall$1(effectCall, EFFECT_HOOK_NAMES$1)) continue;
21404
+ if ((effectCall.arguments?.length ?? 0) < 2) continue;
21405
+ const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
21406
+ if (dependencyStateNames.size === 0) continue;
21407
+ const callback = getEffectCallback(effectCall);
21408
+ if (!callback) continue;
21409
+ const callbackStatements = getCallbackStatements(callback);
21410
+ const firstWriteIndex = callbackStatements.findIndex((candidate) => getUnconditionalSetterCall(candidate, setterNames) !== null);
21411
+ const guardCutoff = firstWriteIndex < 0 ? callbackStatements.length : firstWriteIndex;
21412
+ const earlyReturnGuardTests = callbackStatements.slice(0, guardCutoff).filter(isEarlyReturnGuard).map((guard) => guard.test);
21413
+ const { writes: topLevelWrites, setterCallNodes } = collectTopLevelWrites(callbackStatements, setterNameToStateName, setterNames);
21414
+ if (everySetterCallIsTopLevel(callback, setterNames, setterCallNodes) && earlyReturnGuardTests.some((test) => guardProvenAfterWrites(test, topLevelWrites, 0, /* @__PURE__ */ new Set()))) continue;
21415
+ const reportedStateNames = /* @__PURE__ */ new Set();
21416
+ for (const callbackStatement of callbackStatements) {
21417
+ const setterCall = getUnconditionalSetterCall(callbackStatement, setterNames);
21418
+ if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
21419
+ const stateName = setterNameToStateName.get(setterCall.callee.name);
21420
+ if (!stateName || !dependencyStateNames.has(stateName)) continue;
21421
+ if (reportedStateNames.has(stateName)) continue;
21422
+ if (!isNonSettlingSetterArgument(setterCall, stateName)) continue;
21423
+ const firstArgument = setterCall.arguments?.[0];
21424
+ if (firstArgument && writeProvablyConverges(stripParenExpression(firstArgument), stateName, earlyReturnGuardTests) && everyWriteToStateDrivesTowardEmpty(callback, setterCall.callee.name)) continue;
21425
+ reportedStateNames.add(stateName);
21426
+ context.report({
21427
+ node: setterCall,
21428
+ message: `${setterCall.callee.name}() runs unconditionally inside this effect, which depends on \`${stateName}\` — setting the same state the effect reacts to re-runs the effect on every commit and causes a render loop. Derive the value during render, move the write into an event handler, or guard the update so it settles.`
21429
+ });
21430
+ }
21431
+ }
21432
+ };
21433
+ return {
21434
+ FunctionDeclaration(node) {
21435
+ const functionName = node.id?.name;
21436
+ if (!functionName || !isUppercaseName(functionName) && !isReactHookName(functionName)) return;
21437
+ checkFunctionScope(node.body);
21438
+ },
21439
+ VariableDeclarator(node) {
21440
+ const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
21441
+ if (!isComponentAssignment(node) && !isHookAssignment) return;
21442
+ if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
21443
+ checkFunctionScope(node.init.body);
21444
+ }
21445
+ };
21446
+ }
21447
+ });
21448
+ //#endregion
20236
21449
  //#region src/plugin/utils/get-parent-component.ts
20237
21450
  const getParentComponent = (node) => {
20238
21451
  let ancestor = node.parent;
@@ -20244,7 +21457,7 @@ const getParentComponent = (node) => {
20244
21457
  };
20245
21458
  //#endregion
20246
21459
  //#region src/plugin/rules/react-builtins/no-set-state.ts
20247
- const MESSAGE$11 = "Do not use `this.setState` in components.";
21460
+ const MESSAGE$12 = "Do not use `this.setState` in components.";
20248
21461
  const noSetState = defineRule({
20249
21462
  id: "no-set-state",
20250
21463
  severity: "warn",
@@ -20258,7 +21471,7 @@ const noSetState = defineRule({
20258
21471
  if (!getParentComponent(node)) return;
20259
21472
  context.report({
20260
21473
  node: node.callee,
20261
- message: MESSAGE$11
21474
+ message: MESSAGE$12
20262
21475
  });
20263
21476
  } })
20264
21477
  });
@@ -20417,7 +21630,7 @@ const isAbstractRole = (openingElement, settings) => {
20417
21630
  };
20418
21631
  //#endregion
20419
21632
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
20420
- const MESSAGE$10 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
21633
+ const MESSAGE$11 = "Static HTML elements with event handlers require a role — add `role=\"…\"` or use a semantic HTML element instead.";
20421
21634
  const DEFAULT_HANDLERS = [
20422
21635
  "onClick",
20423
21636
  "onMouseDown",
@@ -20448,7 +21661,7 @@ const noStaticElementInteractions = defineRule({
20448
21661
  category: "Accessibility",
20449
21662
  create: (context) => {
20450
21663
  const settings = resolveSettings$12(context.settings);
20451
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
21664
+ const isTestlikeFile = isTestlikeFilename(context.filename);
20452
21665
  return { JSXOpeningElement(node) {
20453
21666
  if (isTestlikeFile) return;
20454
21667
  let hasNonBlockerHandler = false;
@@ -20476,7 +21689,7 @@ const noStaticElementInteractions = defineRule({
20476
21689
  if (!roleAttribute || !roleAttribute.value) {
20477
21690
  context.report({
20478
21691
  node: node.name,
20479
- message: MESSAGE$10
21692
+ message: MESSAGE$11
20480
21693
  });
20481
21694
  return;
20482
21695
  }
@@ -20486,14 +21699,14 @@ const noStaticElementInteractions = defineRule({
20486
21699
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
20487
21700
  context.report({
20488
21701
  node: node.name,
20489
- message: MESSAGE$10
21702
+ message: MESSAGE$11
20490
21703
  });
20491
21704
  return;
20492
21705
  }
20493
21706
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
20494
21707
  context.report({
20495
21708
  node: node.name,
20496
- message: MESSAGE$10
21709
+ message: MESSAGE$11
20497
21710
  });
20498
21711
  } };
20499
21712
  }
@@ -20525,7 +21738,7 @@ const noStringRefs = defineRule({
20525
21738
  recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
20526
21739
  create: (context) => {
20527
21740
  const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
20528
- const isTestlikeFile = isTestlikeFilename(context.getFilename?.());
21741
+ const isTestlikeFile = isTestlikeFilename(context.filename);
20529
21742
  return {
20530
21743
  JSXAttribute(node) {
20531
21744
  if (isTestlikeFile) return;
@@ -20549,7 +21762,7 @@ const noStringRefs = defineRule({
20549
21762
  });
20550
21763
  //#endregion
20551
21764
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
20552
- const MESSAGE$9 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
21765
+ const MESSAGE$10 = "Stateless functional components shouldn't use `this` — read props/context from function parameters.";
20553
21766
  const isInsideClassMethod = (node, customClassFactoryNames) => {
20554
21767
  let ancestor = node.parent;
20555
21768
  while (ancestor) {
@@ -20617,7 +21830,7 @@ const noThisInSfc = defineRule({
20617
21830
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
20618
21831
  context.report({
20619
21832
  node,
20620
- message: MESSAGE$9
21833
+ message: MESSAGE$10
20621
21834
  });
20622
21835
  } };
20623
21836
  }
@@ -20799,7 +22012,7 @@ const ESCAPED_VERSIONS = {
20799
22012
  ">": "`&gt;` / `&#62;`",
20800
22013
  "}": "`&#125;` (or wrap the literal in `{'}'}`)"
20801
22014
  };
20802
- const buildMessage$6 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
22015
+ const buildMessage$7 = (character) => `\`${character}\` in JSX text can be confused with markup — escape with ${ESCAPED_VERSIONS[character]}.`;
20803
22016
  const noUnescapedEntities = defineRule({
20804
22017
  id: "no-unescaped-entities",
20805
22018
  severity: "warn",
@@ -20810,7 +22023,7 @@ const noUnescapedEntities = defineRule({
20810
22023
  for (const character of value) if (character in ESCAPED_VERSIONS) {
20811
22024
  context.report({
20812
22025
  node,
20813
- message: buildMessage$6(character)
22026
+ message: buildMessage$7(character)
20814
22027
  });
20815
22028
  return;
20816
22029
  }
@@ -21831,7 +23044,7 @@ const SAFER_REPLACEMENT = {
21831
23044
  componentWillUpdate: "componentDidUpdate",
21832
23045
  UNSAFE_componentWillUpdate: "componentDidUpdate"
21833
23046
  };
21834
- const buildMessage$5 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
23047
+ const buildMessage$6 = (methodName) => `Unsafe lifecycle method \`${methodName}\` — use \`${SAFER_REPLACEMENT[methodName] ?? "an alternative lifecycle method"}\` instead.`;
21835
23048
  const resolveSettings$9 = (settings) => {
21836
23049
  const reactDoctor = settings?.["react-doctor"];
21837
23050
  return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
@@ -21879,7 +23092,7 @@ const noUnsafe = defineRule({
21879
23092
  if (!getParentComponent(node)) return;
21880
23093
  context.report({
21881
23094
  node: node.key,
21882
- message: buildMessage$5(name)
23095
+ message: buildMessage$6(name)
21883
23096
  });
21884
23097
  },
21885
23098
  Property(node) {
@@ -21890,7 +23103,7 @@ const noUnsafe = defineRule({
21890
23103
  if (isEs5Component(ancestor)) {
21891
23104
  context.report({
21892
23105
  node: node.key,
21893
- message: buildMessage$5(name)
23106
+ message: buildMessage$6(name)
21894
23107
  });
21895
23108
  return;
21896
23109
  }
@@ -21902,7 +23115,7 @@ const noUnsafe = defineRule({
21902
23115
  });
21903
23116
  //#endregion
21904
23117
  //#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
21905
- const buildMessage$4 = (parentName, isInProp, allowAsProps) => {
23118
+ const buildMessage$5 = (parentName, isInProp, allowAsProps) => {
21906
23119
  let message = "Don't define components inside another component";
21907
23120
  if (parentName) message += ` (\`${parentName}\`)`;
21908
23121
  message += " — extract it to module scope.";
@@ -21971,7 +23184,7 @@ const isReactClassComponent = (classNode) => {
21971
23184
  const findEnclosingComponent = (node) => {
21972
23185
  let walker = node.parent;
21973
23186
  while (walker) {
21974
- if (isFunctionLike$1(walker)) {
23187
+ if (isFunctionLike$2(walker)) {
21975
23188
  const componentName = inferFunctionLikeName(walker);
21976
23189
  if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
21977
23190
  component: walker,
@@ -22137,7 +23350,7 @@ const noUnstableNestedComponents = defineRule({
22137
23350
  if (!enclosing) return;
22138
23351
  context.report({
22139
23352
  node: reportNode,
22140
- message: buildMessage$4(enclosing.name, propInfo !== null, settings.allowAsProps)
23353
+ message: buildMessage$5(enclosing.name, propInfo !== null, settings.allowAsProps)
22141
23354
  });
22142
23355
  };
22143
23356
  const checkFunctionLike = (node) => {
@@ -22172,15 +23385,6 @@ const noUnstableNestedComponents = defineRule({
22172
23385
  }
22173
23386
  });
22174
23387
  //#endregion
22175
- //#region src/plugin/utils/is-canonical-react-namespace-name.ts
22176
- const isCanonicalReactNamespaceName = (namespaceName) => {
22177
- if (namespaceName === "React") return true;
22178
- if (namespaceName === "react") return true;
22179
- if (namespaceName.startsWith("_react")) return true;
22180
- if (namespaceName.startsWith("_React")) return true;
22181
- return false;
22182
- };
22183
- //#endregion
22184
23388
  //#region src/plugin/rules/performance/no-usememo-simple-expression.ts
22185
23389
  const isSimpleExpression = (node) => {
22186
23390
  if (!node) return false;
@@ -22269,7 +23473,7 @@ const noWideLetterSpacing = defineRule({
22269
23473
  //#endregion
22270
23474
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
22271
23475
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
22272
- const MESSAGE$8 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
23476
+ const MESSAGE$9 = "Do not use `this.setState` in `componentWillUpdate` — schedule the update via `componentDidUpdate` instead.";
22273
23477
  const resolveSettings$7 = (settings) => {
22274
23478
  const reactDoctor = settings?.["react-doctor"];
22275
23479
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -22302,7 +23506,7 @@ const noWillUpdateSetState = defineRule({
22302
23506
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
22303
23507
  context.report({
22304
23508
  node: node.callee,
22305
- message: MESSAGE$8
23509
+ message: MESSAGE$9
22306
23510
  });
22307
23511
  } };
22308
23512
  }
@@ -22660,7 +23864,7 @@ const isFileNameAllowed = (filename, checkJS) => {
22660
23864
  };
22661
23865
  const onlyExportComponents = defineRule({
22662
23866
  id: "only-export-components",
22663
- severity: "error",
23867
+ severity: "warn",
22664
23868
  recommendation: "Move non-component exports out of files that export components.",
22665
23869
  category: "Architecture",
22666
23870
  create: (context) => {
@@ -22671,7 +23875,7 @@ const onlyExportComponents = defineRule({
22671
23875
  allowConstantExport: settings.allowConstantExport
22672
23876
  };
22673
23877
  return { Program(node) {
22674
- if (!isFileNameAllowed(context.getFilename ? normalizeFilename$1(context.getFilename()) : void 0, settings.checkJS)) return;
23878
+ if (!isFileNameAllowed(normalizeFilename$1(context.filename ?? ""), settings.checkJS)) return;
22675
23879
  const allNodes = collectAllNodes(node);
22676
23880
  const exports = [];
22677
23881
  let hasReactExport = false;
@@ -22915,7 +24119,7 @@ const REACT_HOOK_NAMES = new Set([
22915
24119
  "useSyncExternalStore",
22916
24120
  "useTransition"
22917
24121
  ]);
22918
- const buildMessage$3 = (importedNames) => `Import ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`preact/hooks\` (or \`preact/compat\`) — importing hooks from \`react\` in a pure-Preact project loads a second copy of Preact's hook state and triggers \`__H\` undefined errors.`;
24122
+ const buildMessage$4 = (importedNames) => `Import ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`preact/hooks\` (or \`preact/compat\`) — importing hooks from \`react\` in a pure-Preact project loads a second copy of Preact's hook state and triggers \`__H\` undefined errors.`;
22919
24123
  const preactNoReactHooksImport = defineRule({
22920
24124
  id: "preact-no-react-hooks-import",
22921
24125
  requires: ["pure-preact"],
@@ -22938,7 +24142,7 @@ const preactNoReactHooksImport = defineRule({
22938
24142
  });
22939
24143
  context.report({
22940
24144
  node,
22941
- message: buildMessage$3(importedNames)
24145
+ message: buildMessage$4(importedNames)
22942
24146
  });
22943
24147
  } })
22944
24148
  });
@@ -22995,7 +24199,7 @@ const preactNoRenderArguments = defineRule({
22995
24199
  });
22996
24200
  //#endregion
22997
24201
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
22998
- const MESSAGE$7 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
24202
+ const MESSAGE$8 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
22999
24203
  const preactPreferOndblclick = defineRule({
23000
24204
  id: "preact-prefer-ondblclick",
23001
24205
  requires: ["pure-preact"],
@@ -23009,7 +24213,7 @@ const preactPreferOndblclick = defineRule({
23009
24213
  if (!onDoubleClickAttribute) return;
23010
24214
  context.report({
23011
24215
  node: onDoubleClickAttribute,
23012
- message: MESSAGE$7
24216
+ message: MESSAGE$8
23013
24217
  });
23014
24218
  } })
23015
24219
  });
@@ -23117,7 +24321,7 @@ const preferEs6Class = defineRule({
23117
24321
  });
23118
24322
  //#endregion
23119
24323
  //#region src/plugin/rules/react-builtins/prefer-function-component.ts
23120
- const MESSAGE$6 = "Class component should be written as a function component — use hooks instead.";
24324
+ const MESSAGE$7 = "Class component should be written as a function component — use hooks instead.";
23121
24325
  const resolveSettings$4 = (settings) => {
23122
24326
  const reactDoctor = settings?.["react-doctor"];
23123
24327
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
@@ -23155,7 +24359,7 @@ const preferFunctionComponent = defineRule({
23155
24359
  const reportNode = node.id ?? node;
23156
24360
  context.report({
23157
24361
  node: reportNode,
23158
- message: MESSAGE$6
24362
+ message: MESSAGE$7
23159
24363
  });
23160
24364
  };
23161
24365
  return {
@@ -23212,6 +24416,276 @@ const preferHtmlDialog = defineRule({
23212
24416
  } })
23213
24417
  });
23214
24418
  //#endregion
24419
+ //#region src/plugin/utils/function-returns-object-literal.ts
24420
+ const unwrapExpression = (node) => {
24421
+ let current = node;
24422
+ for (;;) {
24423
+ if ((current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") && "expression" in current && isAstNode(current.expression)) {
24424
+ current = current.expression;
24425
+ continue;
24426
+ }
24427
+ return current;
24428
+ }
24429
+ };
24430
+ const doesFunctionReturnsObjectLiteral = (functionNode) => {
24431
+ if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
24432
+ const body = functionNode.body;
24433
+ if (body && body.type !== "BlockStatement") return unwrapExpression(body).type === "ObjectExpression";
24434
+ }
24435
+ const body = functionNode.body;
24436
+ if (!body || body.type !== "BlockStatement") return false;
24437
+ let returnsObject = false;
24438
+ const visit = (node) => {
24439
+ if (returnsObject) return;
24440
+ if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
24441
+ if (unwrapExpression(node.argument).type === "ObjectExpression") returnsObject = true;
24442
+ return;
24443
+ }
24444
+ const nodeRecord = node;
24445
+ for (const key of Object.keys(nodeRecord)) {
24446
+ if (key === "parent") continue;
24447
+ const child = nodeRecord[key];
24448
+ if (Array.isArray(child)) for (const item of child) {
24449
+ if (!isAstNode(item)) continue;
24450
+ if (FUNCTION_LIKE_TYPES$1.has(item.type)) continue;
24451
+ visit(item);
24452
+ if (returnsObject) return;
24453
+ }
24454
+ else if (isAstNode(child)) {
24455
+ if (FUNCTION_LIKE_TYPES$1.has(child.type)) continue;
24456
+ visit(child);
24457
+ }
24458
+ }
24459
+ };
24460
+ visit(body);
24461
+ return returnsObject;
24462
+ };
24463
+ //#endregion
24464
+ //#region src/plugin/utils/enclosing-component-or-hook-scope.ts
24465
+ const enclosingComponentOrHookScope = (startNode, ownScopeFor) => {
24466
+ const functionNode = nearestEnclosingFunction(startNode);
24467
+ if (!functionNode) return null;
24468
+ const displayName = componentOrHookDisplayNameForFunction(functionNode);
24469
+ if (!displayName) return null;
24470
+ if (!isReactHookName(displayName) && doesFunctionReturnsObjectLiteral(functionNode)) return null;
24471
+ const bodyScope = ownScopeFor(functionNode);
24472
+ if (!bodyScope) return null;
24473
+ return {
24474
+ functionNode,
24475
+ bodyScope,
24476
+ displayName
24477
+ };
24478
+ };
24479
+ //#endregion
24480
+ //#region src/plugin/rules/architecture/prefer-module-scope-pure-function.ts
24481
+ const isAssignedToComponentMember = (functionNode) => {
24482
+ const parent = functionNode.parent;
24483
+ if (!parent) return false;
24484
+ return isNodeOfType(parent, "AssignmentExpression") && isNodeOfType(parent.left, "MemberExpression");
24485
+ };
24486
+ const hasComponentLocalCaptures = (functionNode, bodyScope, scopes) => {
24487
+ const captures = closureCaptures(functionNode, scopes);
24488
+ for (const capture of captures) {
24489
+ const symbol = capture.resolvedSymbol;
24490
+ if (!symbol) continue;
24491
+ if (isDescendantScope(symbol.scope, bodyScope)) return true;
24492
+ }
24493
+ return false;
24494
+ };
24495
+ const preferModuleScopePureFunction = defineRule({
24496
+ id: "prefer-module-scope-pure-function",
24497
+ tags: ["test-noise"],
24498
+ severity: "warn",
24499
+ category: "Architecture",
24500
+ recommendation: "Move the function to module scope (above the component). It doesn't reference any local state, so the per-render allocation is wasted.",
24501
+ create: (context) => {
24502
+ const report = (functionNode, name, componentName) => {
24503
+ context.report({
24504
+ node: functionNode,
24505
+ message: `\`${name}\` inside \`${componentName}\` doesn't close over any local state. Move it to module scope so it isn't reallocated every render and the component file stays focused on rendering logic.`
24506
+ });
24507
+ };
24508
+ const checkNamedFunction = (functionNode, bindingName) => {
24509
+ if (isAssignedToComponentMember(functionNode)) return;
24510
+ const component = enclosingComponentOrHookScope(functionNode, context.scopes.ownScopeFor);
24511
+ if (!component) return;
24512
+ const ownScope = context.scopes.ownScopeFor(functionNode);
24513
+ if (!ownScope) return;
24514
+ if (ownScope === component.bodyScope) return;
24515
+ if (!isDescendantScope(ownScope, component.bodyScope)) return;
24516
+ if (hasComponentLocalCaptures(functionNode, component.bodyScope, context.scopes)) return;
24517
+ report(functionNode, bindingName, component.displayName);
24518
+ };
24519
+ return {
24520
+ VariableDeclarator(node) {
24521
+ if (!isNodeOfType(node.id, "Identifier")) return;
24522
+ const initializer = node.init;
24523
+ if (!initializer) return;
24524
+ if (!isNodeOfType(initializer, "ArrowFunctionExpression") && !isNodeOfType(initializer, "FunctionExpression")) return;
24525
+ const bindingName = node.id.name;
24526
+ if (/^[A-Z]/.test(bindingName)) return;
24527
+ checkNamedFunction(initializer, bindingName);
24528
+ },
24529
+ FunctionDeclaration(node) {
24530
+ if (!node.id?.name) return;
24531
+ const bindingName = node.id.name;
24532
+ if (/^[A-Z]/.test(bindingName)) return;
24533
+ checkNamedFunction(node, bindingName);
24534
+ }
24535
+ };
24536
+ }
24537
+ });
24538
+ //#endregion
24539
+ //#region src/plugin/rules/architecture/prefer-module-scope-static-value.ts
24540
+ const MUTATING_RECEIVER_METHOD_NAMES = new Set([...MUTATING_ARRAY_METHODS, ...MUTATING_COLLECTION_METHODS]);
24541
+ const isMutationContext = (referenceIdentifier) => {
24542
+ const parent = referenceIdentifier.parent;
24543
+ if (!parent) return false;
24544
+ if (isNodeOfType(parent, "AssignmentExpression") && parent.left === referenceIdentifier) return true;
24545
+ if (isNodeOfType(parent, "UpdateExpression") && parent.argument === referenceIdentifier) return true;
24546
+ if (isNodeOfType(parent, "MemberExpression") && parent.object === referenceIdentifier) {
24547
+ const grandparent = parent.parent;
24548
+ if (!grandparent) return false;
24549
+ if (isNodeOfType(grandparent, "AssignmentExpression") && grandparent.left === parent) return true;
24550
+ if (isNodeOfType(grandparent, "UpdateExpression") && grandparent.argument === parent) return true;
24551
+ if (isNodeOfType(grandparent, "UnaryExpression") && grandparent.operator === "delete" && grandparent.argument === parent) return true;
24552
+ if (isNodeOfType(grandparent, "CallExpression") && grandparent.callee === parent && !parent.computed && isNodeOfType(parent.property, "Identifier") && MUTATING_RECEIVER_METHOD_NAMES.has(parent.property.name)) return true;
24553
+ }
24554
+ return false;
24555
+ };
24556
+ const isBindingMutatedAfterInit = (declaratorNode, bodyScope, scopes) => {
24557
+ if (!isNodeOfType(declaratorNode.id, "Identifier")) return false;
24558
+ const symbol = scopes.symbolFor(declaratorNode.id);
24559
+ if (!symbol) return false;
24560
+ for (const reference of symbol.references) {
24561
+ if (reference.identifier === declaratorNode.id) continue;
24562
+ if (reference.identifier === declaratorNode.init) continue;
24563
+ if (!isDescendantScope(reference.scope, bodyScope) && reference.scope !== bodyScope) continue;
24564
+ if (isMutationContext(reference.identifier)) return true;
24565
+ }
24566
+ return false;
24567
+ };
24568
+ const hasComponentLocalReferences = (expression, bodyScope, scopes) => {
24569
+ let foundLocal = false;
24570
+ walkAst(expression, (node) => {
24571
+ if (foundLocal) return false;
24572
+ if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) {
24573
+ foundLocal = true;
24574
+ return false;
24575
+ }
24576
+ const reference = scopes.referenceFor(node);
24577
+ if (reference?.resolvedSymbol && isDescendantScope(reference.resolvedSymbol.scope, bodyScope)) {
24578
+ foundLocal = true;
24579
+ return false;
24580
+ }
24581
+ });
24582
+ return foundLocal;
24583
+ };
24584
+ const isHoistableValueExpression = (expression) => {
24585
+ const stripped = stripParenExpression(expression);
24586
+ return isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ObjectExpression");
24587
+ };
24588
+ const preferModuleScopeStaticValue = defineRule({
24589
+ id: "prefer-module-scope-static-value",
24590
+ tags: ["test-noise"],
24591
+ severity: "warn",
24592
+ category: "Architecture",
24593
+ recommendation: "Move the constant to module scope (above the component). It doesn't reference any local state, so the per-render allocation is wasted and any memoised consumer sees a fresh reference each render.",
24594
+ create: (context) => ({ VariableDeclarator(node) {
24595
+ if (!isNodeOfType(node.id, "Identifier")) return;
24596
+ const initializer = node.init;
24597
+ if (!initializer) return;
24598
+ if (!isHoistableValueExpression(initializer)) return;
24599
+ const component = enclosingComponentOrHookScope(node, context.scopes.ownScopeFor);
24600
+ if (!component) return;
24601
+ if (hasComponentLocalReferences(initializer, component.bodyScope, context.scopes)) return;
24602
+ if (isBindingMutatedAfterInit(node, component.bodyScope, context.scopes)) return;
24603
+ const bindingName = node.id.name;
24604
+ context.report({
24605
+ node,
24606
+ message: `\`${bindingName}\` inside \`${component.displayName}\` doesn't depend on any local state. Move it to module scope so the allocation happens once and memoised consumers see a stable reference.`
24607
+ });
24608
+ } })
24609
+ });
24610
+ //#endregion
24611
+ //#region src/plugin/rules/performance/prefer-stable-empty-fallback.ts
24612
+ const isEmptyArrayLiteral$1 = (expression) => {
24613
+ const stripped = stripParenExpression(expression);
24614
+ return isNodeOfType(stripped, "ArrayExpression") && (stripped.elements ?? []).length === 0;
24615
+ };
24616
+ const isEmptyObjectLiteral = (expression) => {
24617
+ const stripped = stripParenExpression(expression);
24618
+ return isNodeOfType(stripped, "ObjectExpression") && (stripped.properties ?? []).length === 0;
24619
+ };
24620
+ const isStableNonEmptyExpression = (expression) => {
24621
+ const stripped = stripParenExpression(expression);
24622
+ if (isNodeOfType(stripped, "Identifier")) return true;
24623
+ if (isNodeOfType(stripped, "ThisExpression")) return true;
24624
+ if (isNodeOfType(stripped, "MemberExpression")) {
24625
+ if (stripped.computed) return false;
24626
+ const object = stripped.object;
24627
+ if (!object) return false;
24628
+ return isStableNonEmptyExpression(object);
24629
+ }
24630
+ return false;
24631
+ };
24632
+ const matchEmptyFallbackInLogicalExpression = (expression) => {
24633
+ const stripped = stripParenExpression(expression);
24634
+ if (!isNodeOfType(stripped, "LogicalExpression")) return null;
24635
+ if (stripped.operator !== "||" && stripped.operator !== "??") return null;
24636
+ const left = stripped.left;
24637
+ const right = stripped.right;
24638
+ if (!left || !right) return null;
24639
+ if (isEmptyArrayLiteral$1(right) && isStableNonEmptyExpression(left)) return {
24640
+ emptyKind: "array",
24641
+ emptyNode: right,
24642
+ nonEmptyExpression: left
24643
+ };
24644
+ if (isEmptyObjectLiteral(right) && isStableNonEmptyExpression(left)) return {
24645
+ emptyKind: "object",
24646
+ emptyNode: right,
24647
+ nonEmptyExpression: left
24648
+ };
24649
+ return null;
24650
+ };
24651
+ const buildMessage$3 = (emptyKind) => {
24652
+ return `Fallback \`${emptyKind === "array" ? "[]" : "{}"}\` allocates a fresh ${emptyKind} on every render where the left-hand value is falsy — the memoised child sees a different reference and re-renders. Hoist a module-level constant (e.g. \`${emptyKind === "array" ? "const EMPTY_ITEMS: Item[] = []" : "const EMPTY_CONFIG: Config = {}"}\`) and use it as the fallback.`;
24653
+ };
24654
+ const preferStableEmptyFallback = defineRule({
24655
+ id: "prefer-stable-empty-fallback",
24656
+ tags: ["react-jsx-only", "test-noise"],
24657
+ severity: "warn",
24658
+ category: "Performance",
24659
+ disabledBy: ["react-compiler"],
24660
+ recommendation: "Hoist a module-level `const EMPTY = []` (or `{}`) and use that as the `||` / `??` fallback so the consumer sees a stable reference.",
24661
+ create: (context) => {
24662
+ let memoRegistry = null;
24663
+ return {
24664
+ Program(node) {
24665
+ memoRegistry = buildSameFileMemoRegistry(node);
24666
+ },
24667
+ JSXAttribute(node) {
24668
+ if (!isInsideFunctionScope(node)) return;
24669
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
24670
+ if (!node.value) return;
24671
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24672
+ const innerExpression = node.value.expression;
24673
+ if (!innerExpression) return;
24674
+ if (innerExpression.type === "JSXEmptyExpression") return;
24675
+ const parentJsxOpening = node.parent;
24676
+ const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
24677
+ if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
24678
+ const fallback = matchEmptyFallbackInLogicalExpression(innerExpression);
24679
+ if (!fallback) return;
24680
+ context.report({
24681
+ node: fallback.emptyNode,
24682
+ message: buildMessage$3(fallback.emptyKind)
24683
+ });
24684
+ }
24685
+ };
24686
+ }
24687
+ });
24688
+ //#endregion
23215
24689
  //#region src/plugin/rules/a11y/prefer-tag-over-role.ts
23216
24690
  const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
23217
24691
  const preferTagOverRole = defineRule({
@@ -23717,106 +25191,6 @@ const queryStableQueryClient = defineRule({
23717
25191
  }
23718
25192
  });
23719
25193
  //#endregion
23720
- //#region src/plugin/rules/architecture/react-compiler-destructure-method.ts
23721
- const HOOK_OBJECTS_WITH_METHODS = new Map([
23722
- ["useRouter", new Set([
23723
- "push",
23724
- "replace",
23725
- "back",
23726
- "forward",
23727
- "refresh",
23728
- "prefetch"
23729
- ])],
23730
- ["useNavigation", new Set([
23731
- "navigate",
23732
- "push",
23733
- "goBack",
23734
- "popToTop",
23735
- "reset",
23736
- "replace",
23737
- "dispatch"
23738
- ])],
23739
- ["useSearchParams", new Set([
23740
- "get",
23741
- "getAll",
23742
- "has",
23743
- "set"
23744
- ])]
23745
- ]);
23746
- const HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING = new Map([["useNavigation", new Set(["@react-navigation/native", "@react-navigation/core"])]]);
23747
- const isUnsafeMethodDestructureHookImport = (node, hookSource) => {
23748
- const moduleSources = HOOK_IMPORT_SOURCES_WITH_UNSAFE_METHOD_DESTRUCTURING.get(hookSource);
23749
- if (!moduleSources) return false;
23750
- for (const moduleSource of moduleSources) if (isImportedFromModule(node, hookSource, moduleSource)) return true;
23751
- return false;
23752
- };
23753
- const buildHookBindingMap = (componentBody) => {
23754
- const result = /* @__PURE__ */ new Map();
23755
- if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return result;
23756
- for (const statement of componentBody.body ?? []) {
23757
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
23758
- for (const declarator of statement.declarations ?? []) {
23759
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
23760
- if (!isNodeOfType(declarator.init, "CallExpression")) continue;
23761
- const callee = declarator.init.callee;
23762
- if (!isNodeOfType(callee, "Identifier")) continue;
23763
- result.set(declarator.id.name, callee.name);
23764
- }
23765
- }
23766
- return result;
23767
- };
23768
- const reactCompilerDestructureMethod = defineRule({
23769
- id: "react-compiler-destructure-method",
23770
- tags: ["test-noise"],
23771
- severity: "warn",
23772
- recommendation: "Destructure the method up front: `const { push } = useRouter()` then call `push(...)` directly — clearer dependency graph and easier for React Compiler to memoize",
23773
- create: (context) => {
23774
- const hookBindingMapStack = [];
23775
- const isComponent = (node) => {
23776
- if (isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.id?.name && isUppercaseName(node.id.name));
23777
- if (isNodeOfType(node, "VariableDeclarator")) return isComponentAssignment(node);
23778
- return false;
23779
- };
23780
- const enter = (node) => {
23781
- if (!isComponent(node)) return;
23782
- let body;
23783
- if (isNodeOfType(node, "FunctionDeclaration")) body = node.body;
23784
- else if (isNodeOfType(node, "VariableDeclarator")) {
23785
- const initializer = node.init;
23786
- body = isInlineFunctionExpression(initializer) ? initializer.body : null;
23787
- }
23788
- hookBindingMapStack.push(buildHookBindingMap(body));
23789
- };
23790
- const exit = (node) => {
23791
- if (isComponent(node)) hookBindingMapStack.pop();
23792
- };
23793
- return {
23794
- FunctionDeclaration: enter,
23795
- "FunctionDeclaration:exit": exit,
23796
- VariableDeclarator: enter,
23797
- "VariableDeclarator:exit": exit,
23798
- MemberExpression(node) {
23799
- if (hookBindingMapStack.length === 0) return;
23800
- if (node.computed) return;
23801
- if (!isNodeOfType(node.object, "Identifier")) return;
23802
- if (!isNodeOfType(node.property, "Identifier")) return;
23803
- const bindingName = node.object.name;
23804
- const methodName = node.property.name;
23805
- const hookSource = hookBindingMapStack[hookBindingMapStack.length - 1].get(bindingName);
23806
- if (!hookSource) return;
23807
- const allowedMethods = HOOK_OBJECTS_WITH_METHODS.get(hookSource);
23808
- if (!allowedMethods || !allowedMethods.has(methodName)) return;
23809
- if (isUnsafeMethodDestructureHookImport(node, hookSource)) return;
23810
- if (!isNodeOfType(node.parent, "CallExpression") || node.parent.callee !== node) return;
23811
- context.report({
23812
- node,
23813
- 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`
23814
- });
23815
- }
23816
- };
23817
- }
23818
- });
23819
- //#endregion
23820
25194
  //#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
23821
25195
  const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
23822
25196
  ["useMemo", "Remove `useMemo` — React Compiler auto-memoizes every value in this component. Manual `useMemo` adds noise without improving performance."],
@@ -23862,91 +25236,216 @@ const reactCompilerNoManualMemoization = defineRule({
23862
25236
  } })
23863
25237
  });
23864
25238
  //#endregion
23865
- //#region src/plugin/utils/has-binding-named.ts
23866
- const hasBindingNamed = (root, bindingName) => {
23867
- const collected = /* @__PURE__ */ new Set();
23868
- const visit = (node) => {
23869
- switch (node.type) {
23870
- case "VariableDeclarator":
23871
- if ("id" in node && node.id) collectPatternNames(node.id, collected);
23872
- break;
23873
- case "FunctionDeclaration":
23874
- case "FunctionExpression":
23875
- case "ClassDeclaration":
23876
- case "ClassExpression":
23877
- if ("id" in node && node.id && node.id.type === "Identifier") {
23878
- const idNode = node.id;
23879
- if (typeof idNode.name === "string") collected.add(idNode.name);
23880
- }
23881
- break;
23882
- case "ArrowFunctionExpression": break;
23883
- case "ImportDefaultSpecifier":
23884
- case "ImportNamespaceSpecifier":
23885
- case "ImportSpecifier":
23886
- if ("local" in node && node.local && node.local.type === "Identifier") {
23887
- const local = node.local;
23888
- if (typeof local.name === "string") collected.add(local.name);
23889
- }
23890
- break;
23891
- case "TSImportEqualsDeclaration":
23892
- case "TSEnumDeclaration":
23893
- case "TSTypeAliasDeclaration":
23894
- case "TSInterfaceDeclaration":
23895
- case "TSModuleDeclaration": {
23896
- const idNode = node.id;
23897
- if (idNode && idNode.type === "Identifier") {
23898
- const idObject = idNode;
23899
- if (typeof idObject.name === "string") collected.add(idObject.name);
23900
- }
23901
- break;
23902
- }
23903
- default: break;
23904
- }
23905
- if ("params" in node && Array.isArray(node.params)) for (const param of node.params) collectPatternNames(param, collected);
23906
- if (collected.has(bindingName)) return;
23907
- const nodeRecord = node;
23908
- for (const key of Object.keys(nodeRecord)) {
23909
- if (key === "parent") continue;
23910
- const child = nodeRecord[key];
23911
- if (Array.isArray(child)) {
23912
- for (const item of child) if (isAstNode(item)) visit(item);
23913
- } else if (isAstNode(child)) visit(child);
23914
- if (collected.has(bindingName)) return;
23915
- }
23916
- };
23917
- visit(root);
23918
- return collected.has(bindingName);
23919
- };
23920
- //#endregion
23921
25239
  //#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
23922
- const MESSAGE$5 = "`React` must be in scope when using JSX (the classic JSX transform expands `<a/>` to `React.createElement('a')`).";
25240
+ const MESSAGE$6 = "`React` must be in scope when using JSX (the classic JSX transform expands `<a/>` to `React.createElement('a')`).";
23923
25241
  const reactInJsxScope = defineRule({
23924
25242
  id: "react-in-jsx-scope",
23925
25243
  severity: "warn",
23926
25244
  defaultEnabled: false,
23927
25245
  recommendation: "If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import `React` at the top of the file.",
23928
- create: (context) => {
23929
- let didCheckBindingForFile = false;
23930
- let isReactBound = false;
23931
- const ensureBindingChecked = (jsxNode) => {
23932
- if (didCheckBindingForFile) return isReactBound;
23933
- didCheckBindingForFile = true;
23934
- const programRoot = findProgramRoot(jsxNode);
23935
- isReactBound = programRoot ? hasBindingNamed(programRoot, "React") : false;
23936
- return isReactBound;
25246
+ create: (context) => ({
25247
+ JSXOpeningElement(node) {
25248
+ if (findVariableInitializer(node, "React")) return;
25249
+ context.report({
25250
+ node: node.name,
25251
+ message: MESSAGE$6
25252
+ });
25253
+ },
25254
+ JSXFragment(node) {
25255
+ if (findVariableInitializer(node, "React")) return;
25256
+ context.report({
25257
+ node: node.openingFragment,
25258
+ message: MESSAGE$6
25259
+ });
25260
+ }
25261
+ })
25262
+ });
25263
+ //#endregion
25264
+ //#region src/plugin/utils/collect-react-redux-selector-aliases.ts
25265
+ const REACT_REDUX_MODULE = "react-redux";
25266
+ const collectReactReduxSelectorAliases = (programRoot) => {
25267
+ const aliases = /* @__PURE__ */ new Set();
25268
+ if (!isNodeOfType(programRoot, "Program")) return aliases;
25269
+ for (const topLevel of programRoot.body ?? []) {
25270
+ if (!isNodeOfType(topLevel, "ImportDeclaration")) continue;
25271
+ if (typeof topLevel.source?.value !== "string") continue;
25272
+ if (topLevel.source.value !== REACT_REDUX_MODULE) continue;
25273
+ for (const specifier of topLevel.specifiers ?? []) {
25274
+ if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
25275
+ const imported = specifier.imported;
25276
+ if ((imported && "name" in imported && typeof imported.name === "string" ? imported.name : imported && "value" in imported && typeof imported.value === "string" ? imported.value : null) !== "useSelector") continue;
25277
+ const local = specifier.local;
25278
+ if (isNodeOfType(local, "Identifier")) aliases.add(local.name);
25279
+ }
25280
+ }
25281
+ const collectDeclarations = (node) => {
25282
+ if (!isNodeOfType(node, "VariableDeclaration")) return;
25283
+ for (const declarator of node.declarations ?? []) {
25284
+ if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
25285
+ if (!isNodeOfType(declarator.id, "Identifier")) continue;
25286
+ if (!declarator.init) continue;
25287
+ const initialiser = stripParenExpression(declarator.init);
25288
+ if (!isNodeOfType(initialiser, "Identifier")) continue;
25289
+ if (!aliases.has(initialiser.name)) continue;
25290
+ aliases.add(declarator.id.name);
25291
+ }
25292
+ };
25293
+ for (const topLevel of programRoot.body ?? []) if (isNodeOfType(topLevel, "VariableDeclaration")) collectDeclarations(topLevel);
25294
+ else if (isNodeOfType(topLevel, "ExportNamedDeclaration") && topLevel.declaration) collectDeclarations(topLevel.declaration);
25295
+ return aliases;
25296
+ };
25297
+ const isUseSelectorIdentifier = (calleeNode, aliases) => {
25298
+ if (!isNodeOfType(calleeNode, "Identifier")) return false;
25299
+ if (aliases.has(calleeNode.name)) return true;
25300
+ if (calleeNode.name !== "useSelector") return false;
25301
+ return isImportedFromModule(calleeNode, calleeNode.name, REACT_REDUX_MODULE);
25302
+ };
25303
+ //#endregion
25304
+ //#region src/plugin/rules/state-and-effects/utils/inline-use-selector-function.ts
25305
+ const inlineUseSelectorFunction = (callNode, aliases) => {
25306
+ if (!isUseSelectorIdentifier(callNode.callee, aliases)) return null;
25307
+ const args = callNode.arguments ?? [];
25308
+ if (args.length === 0 || args.length >= 2) return null;
25309
+ const selectorArgument = stripParenExpression(args[0]);
25310
+ if (isNodeOfType(selectorArgument, "ArrowFunctionExpression") || isNodeOfType(selectorArgument, "FunctionExpression")) return selectorArgument;
25311
+ return null;
25312
+ };
25313
+ //#endregion
25314
+ //#region src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts
25315
+ const ALLOCATING_ARRAY_METHODS = new Set([
25316
+ "filter",
25317
+ "map",
25318
+ "flatMap",
25319
+ "slice",
25320
+ "concat",
25321
+ "toSorted",
25322
+ "toReversed",
25323
+ "toSpliced",
25324
+ "with"
25325
+ ]);
25326
+ const ALLOCATING_NAMESPACE_CALLS = new Map([["Object", new Set([
25327
+ "keys",
25328
+ "values",
25329
+ "entries",
25330
+ "fromEntries",
25331
+ "assign"
25332
+ ])], ["Array", new Set(["from", "of"])]]);
25333
+ const MESSAGE_DERIVATION = (methodName) => `useSelector callback derives a new array via \`.${methodName}(...)\` on every store update — the default \`===\` equality check always fails on a fresh allocation, re-rendering the component on every dispatched action. Select the raw slice (\`useSelector(s => s.users)\`) and derive with \`useMemo\`, or hoist the derivation into a memoised \`createSelector\` from \`reselect\`.`;
25334
+ const MESSAGE_NAMESPACE = (namespace, methodName) => `useSelector callback returns a fresh collection from \`${namespace}.${methodName}(...)\` on every store update — the default \`===\` equality check always fails, re-rendering on every dispatched action. Select the raw slice and derive with \`useMemo\` or \`reselect\`.`;
25335
+ const getAllocatingCallSiteDescription = (expression) => {
25336
+ const stripped = stripParenExpression(expression);
25337
+ if (!isNodeOfType(stripped, "CallExpression")) return null;
25338
+ const callee = stripped.callee;
25339
+ if (!isNodeOfType(callee, "MemberExpression")) return null;
25340
+ if (callee.computed) return null;
25341
+ if (!isNodeOfType(callee.property, "Identifier")) return null;
25342
+ const methodName = callee.property.name;
25343
+ if (isNodeOfType(callee.object, "Identifier")) {
25344
+ const namespaceName = callee.object.name;
25345
+ if (ALLOCATING_NAMESPACE_CALLS.get(namespaceName)?.has(methodName)) return {
25346
+ kind: "namespace",
25347
+ namespace: namespaceName,
25348
+ method: methodName
23937
25349
  };
25350
+ }
25351
+ if (ALLOCATING_ARRAY_METHODS.has(methodName)) return {
25352
+ kind: "method",
25353
+ method: methodName
25354
+ };
25355
+ return null;
25356
+ };
25357
+ const findReturnedAllocatingCall = (expression) => {
25358
+ const stripped = stripParenExpression(expression);
25359
+ const direct = getAllocatingCallSiteDescription(stripped);
25360
+ if (direct) return {
25361
+ ...direct,
25362
+ node: stripped
25363
+ };
25364
+ if (isNodeOfType(stripped, "ConditionalExpression")) return findReturnedAllocatingCall(stripped.consequent) ?? findReturnedAllocatingCall(stripped.alternate);
25365
+ if (isNodeOfType(stripped, "LogicalExpression")) return findReturnedAllocatingCall(stripped.left) ?? findReturnedAllocatingCall(stripped.right);
25366
+ if (isNodeOfType(stripped, "SequenceExpression")) {
25367
+ const lastExpression = stripped.expressions[stripped.expressions.length - 1];
25368
+ return lastExpression ? findReturnedAllocatingCall(lastExpression) : null;
25369
+ }
25370
+ return null;
25371
+ };
25372
+ const reduxUseselectorInlineDerivation = defineRule({
25373
+ id: "redux-useselector-inline-derivation",
25374
+ severity: "warn",
25375
+ category: "Performance",
25376
+ disabledBy: ["react-compiler"],
25377
+ recommendation: "Select the raw slice and derive with `useMemo`, or use `createSelector` from `reselect`.",
25378
+ create: (context) => {
25379
+ let aliases = /* @__PURE__ */ new Set();
23938
25380
  return {
23939
- JSXOpeningElement(node) {
23940
- if (ensureBindingChecked(node)) return;
23941
- context.report({
23942
- node: node.name,
23943
- message: MESSAGE$5
25381
+ Program(node) {
25382
+ aliases = collectReactReduxSelectorAliases(node);
25383
+ },
25384
+ CallExpression(node) {
25385
+ const selectorArgument = inlineUseSelectorFunction(node, aliases);
25386
+ if (!selectorArgument) return;
25387
+ const body = selectorArgument.body;
25388
+ if (!body) return;
25389
+ const returnedExpressions = [];
25390
+ if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
25391
+ if (node !== body && isFunctionLike$2(node)) return false;
25392
+ if (isNodeOfType(node, "ReturnStatement")) {
25393
+ if (node.argument) returnedExpressions.push(node.argument);
25394
+ return false;
25395
+ }
23944
25396
  });
25397
+ else returnedExpressions.push(body);
25398
+ for (const returnedExpression of returnedExpressions) {
25399
+ const allocatingCall = findReturnedAllocatingCall(returnedExpression);
25400
+ if (!allocatingCall) continue;
25401
+ const reportMessage = allocatingCall.kind === "method" ? MESSAGE_DERIVATION(allocatingCall.method) : MESSAGE_NAMESPACE(allocatingCall.namespace, allocatingCall.method);
25402
+ context.report({
25403
+ node: allocatingCall.node,
25404
+ message: reportMessage
25405
+ });
25406
+ return;
25407
+ }
25408
+ }
25409
+ };
25410
+ }
25411
+ });
25412
+ //#endregion
25413
+ //#region src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts
25414
+ const MESSAGE$5 = "useSelector returning a new object/array re-renders on every dispatched action — the default `===` equality check always fails on a fresh reference. Either return a primitive, split into multiple useSelector calls, or pass `shallowEqual` (or a custom equality fn) as the second argument.";
25415
+ const isConciseBodyReturningCollection = (functionNode) => {
25416
+ if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
25417
+ const rawBody = functionNode.body;
25418
+ if (!rawBody) return false;
25419
+ if (!isNodeOfType(rawBody, "BlockStatement")) {
25420
+ const conciseExpression = stripParenExpression(rawBody);
25421
+ return isNodeOfType(conciseExpression, "ObjectExpression") || isNodeOfType(conciseExpression, "ArrayExpression");
25422
+ }
25423
+ const statements = rawBody.body ?? [];
25424
+ if (statements.length === 0) return false;
25425
+ const lastStatement = statements[statements.length - 1];
25426
+ if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
25427
+ if (!lastStatement.argument) return false;
25428
+ const returnedExpression = stripParenExpression(lastStatement.argument);
25429
+ return isNodeOfType(returnedExpression, "ObjectExpression") || isNodeOfType(returnedExpression, "ArrayExpression");
25430
+ };
25431
+ const reduxUseselectorReturnsNewCollection = defineRule({
25432
+ id: "redux-useselector-returns-new-collection",
25433
+ severity: "warn",
25434
+ category: "Performance",
25435
+ disabledBy: ["react-compiler"],
25436
+ recommendation: "Return a primitive, split into multiple useSelector calls, or pass `shallowEqual` from `react-redux` as the second argument.",
25437
+ create: (context) => {
25438
+ let aliases = /* @__PURE__ */ new Set();
25439
+ return {
25440
+ Program(node) {
25441
+ aliases = collectReactReduxSelectorAliases(node);
23945
25442
  },
23946
- JSXFragment(node) {
23947
- if (ensureBindingChecked(node)) return;
25443
+ CallExpression(node) {
25444
+ const selectorArgument = inlineUseSelectorFunction(node, aliases);
25445
+ if (!selectorArgument) return;
25446
+ if (!isConciseBodyReturningCollection(selectorArgument)) return;
23948
25447
  context.report({
23949
- node: node.openingFragment,
25448
+ node,
23950
25449
  message: MESSAGE$5
23951
25450
  });
23952
25451
  }
@@ -24192,7 +25691,7 @@ const renderingSvgPrecision = defineRule({
24192
25691
  category: "Performance",
24193
25692
  recommendation: "Truncate path/points/transform decimals to 1–2 digits — sub-pixel precision adds bytes with no visible difference",
24194
25693
  create: (context) => {
24195
- const filename = context.getFilename?.();
25694
+ const filename = context.filename;
24196
25695
  const isAutoGenerated = isAutoGeneratedSvgFile(filename ? normalizeFilename$1(filename) : void 0);
24197
25696
  return { JSXAttribute(node) {
24198
25697
  if (isAutoGenerated) return;
@@ -24224,7 +25723,7 @@ const hasOwnAwait = (functionBody) => {
24224
25723
  let found = false;
24225
25724
  walkAst(functionBody, (child) => {
24226
25725
  if (found) return;
24227
- if (child !== functionBody && isFunctionLike$1(child)) return false;
25726
+ if (child !== functionBody && isFunctionLike$2(child)) return false;
24228
25727
  if (isNodeOfType(child, "AwaitExpression")) found = true;
24229
25728
  });
24230
25729
  return found;
@@ -24243,7 +25742,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
24243
25742
  let found = false;
24244
25743
  walkAst(componentBody, (child) => {
24245
25744
  if (found) return;
24246
- if (!isFunctionLike$1(child)) return;
25745
+ if (!isFunctionLike$2(child)) return;
24247
25746
  const functionBody = child.body;
24248
25747
  if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
24249
25748
  if (callsIdentifier(functionBody, setterName)) found = true;
@@ -24697,6 +26196,33 @@ const rerenderFunctionalSetstate = defineRule({
24697
26196
  } })
24698
26197
  });
24699
26198
  //#endregion
26199
+ //#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
26200
+ const rerenderLazyRefInit = defineRule({
26201
+ id: "rerender-lazy-ref-init",
26202
+ tags: ["test-noise"],
26203
+ severity: "warn",
26204
+ category: "Performance",
26205
+ recommendation: "Initialize lazily: `const ref = useRef<T | null>(null); if (ref.current === null) ref.current = expensiveCall();`",
26206
+ create: (context) => ({ CallExpression(node) {
26207
+ if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
26208
+ const initializer = node.arguments[0];
26209
+ const isPlainCall = isNodeOfType(initializer, "CallExpression");
26210
+ const isNewCall = isNodeOfType(initializer, "NewExpression");
26211
+ if (!isPlainCall && !isNewCall) return;
26212
+ const callee = initializer.callee;
26213
+ const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
26214
+ const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
26215
+ if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
26216
+ if (isPlainCall && isReactHookName(calleeName)) return;
26217
+ const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
26218
+ const lazyFix = isNewCall ? `ref.current = new ${calleeName}();` : `ref.current = ${calleeName}();`;
26219
+ context.report({
26220
+ node: initializer,
26221
+ message: `useRef(${callShape}) allocates a fresh value on every render — useRef has no lazy-init form, so the allocation is discarded after the first render. Use \`const ref = useRef(null); if (ref.current === null) ${lazyFix}\` or \`useMemo\` instead.`
26222
+ });
26223
+ } })
26224
+ });
26225
+ //#endregion
24700
26226
  //#region src/plugin/rules/state-and-effects/rerender-lazy-state-init.ts
24701
26227
  const rerenderLazyStateInit = defineRule({
24702
26228
  id: "rerender-lazy-state-init",
@@ -25524,7 +27050,8 @@ const classifyPackagePlatform = (filename) => {
25524
27050
  //#endregion
25525
27051
  //#region src/plugin/utils/is-expo-managed-file.ts
25526
27052
  const isExpoManagedFileActive = (context) => {
25527
- const filename = context.getFilename?.() ? normalizeFilename$1(context.getFilename()) : void 0;
27053
+ const rawFilename = context.filename;
27054
+ const filename = rawFilename ? normalizeFilename$1(rawFilename) : void 0;
25528
27055
  if (filename) {
25529
27056
  const packagePlatform = classifyPackagePlatform(filename);
25530
27057
  if (packagePlatform === "expo") return true;
@@ -30145,7 +31672,7 @@ const isUseEffectEventSymbol = (symbol) => {
30145
31672
  const findEnclosingComponentOrHookFunction = (node) => {
30146
31673
  let current = node.parent;
30147
31674
  while (current) {
30148
- if (isFunctionLike$1(current)) {
31675
+ if (isFunctionLike$2(current)) {
30149
31676
  const resolvedName = inferFunctionName(current);
30150
31677
  if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
30151
31678
  }
@@ -30166,7 +31693,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
30166
31693
  const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
30167
31694
  let current = node.parent;
30168
31695
  while (current) {
30169
- if (isFunctionLike$1(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
31696
+ if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
30170
31697
  current = current.parent ?? null;
30171
31698
  }
30172
31699
  return false;
@@ -30500,7 +32027,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
30500
32027
  let foundAuthCall = false;
30501
32028
  for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
30502
32029
  if (foundAuthCall) return;
30503
- if (isFunctionLike$1(child)) return false;
32030
+ if (isFunctionLike$2(child)) return false;
30504
32031
  if (!isNodeOfType(child, "CallExpression")) return;
30505
32032
  if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
30506
32033
  });
@@ -30692,7 +32219,7 @@ const serverFetchWithoutRevalidate = defineRule({
30692
32219
  let isServerSideFile = false;
30693
32220
  return {
30694
32221
  Program(node) {
30695
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
32222
+ const filename = normalizeFilename$1(context.filename ?? "");
30696
32223
  if (!APP_ROUTER_FILE_PATTERN.test(filename)) {
30697
32224
  isServerSideFile = false;
30698
32225
  return;
@@ -30805,7 +32332,7 @@ const serverHoistStaticIo = defineRule({
30805
32332
  inspectHandlerBody(context, declaration.body, `${handlerName} route handler`, collectIdentifierParams(declaration.params ?? []));
30806
32333
  },
30807
32334
  ExportDefaultDeclaration(node) {
30808
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
32335
+ const filename = normalizeFilename$1(context.filename ?? "");
30809
32336
  if (!PAGES_ROUTER_API_PATH_PATTERN.test(filename)) return;
30810
32337
  const declaration = node.declaration;
30811
32338
  if (!declaration || !isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "FunctionExpression") && !isNodeOfType(declaration, "ArrowFunctionExpression")) return;
@@ -31356,7 +32883,7 @@ const tanstackStartMissingHeadContent = defineRule({
31356
32883
  };
31357
32884
  return {
31358
32885
  Program(node) {
31359
- const filename = context.getFilename?.() ?? "";
32886
+ const filename = context.filename ?? "";
31360
32887
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31361
32888
  const statements = node.body ?? [];
31362
32889
  for (const statement of statements) collectImportBindings(statement);
@@ -31366,17 +32893,17 @@ const tanstackStartMissingHeadContent = defineRule({
31366
32893
  }
31367
32894
  },
31368
32895
  ImportDeclaration(node) {
31369
- const filename = context.getFilename?.() ?? "";
32896
+ const filename = context.filename ?? "";
31370
32897
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31371
32898
  collectImportBindings(node);
31372
32899
  },
31373
32900
  VariableDeclarator(node) {
31374
- const filename = context.getFilename?.() ?? "";
32901
+ const filename = context.filename ?? "";
31375
32902
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31376
32903
  collectVariableAlias(node);
31377
32904
  },
31378
32905
  JSXOpeningElement(node) {
31379
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
32906
+ const filename = normalizeFilename$1(context.filename ?? "");
31380
32907
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31381
32908
  if (isNodeOfType(node.name, "JSXIdentifier")) {
31382
32909
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
@@ -31391,7 +32918,7 @@ const tanstackStartMissingHeadContent = defineRule({
31391
32918
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
31392
32919
  },
31393
32920
  "Program:exit"(programNode) {
31394
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
32921
+ const filename = normalizeFilename$1(context.filename ?? "");
31395
32922
  if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
31396
32923
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
31397
32924
  node: programNode,
@@ -31410,7 +32937,7 @@ const tanstackStartNoAnchorElement = defineRule({
31410
32937
  severity: "warn",
31411
32938
  recommendation: "`import { Link } from '@tanstack/react-router'` — enables type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
31412
32939
  create: (context) => ({ JSXOpeningElement(node) {
31413
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
32940
+ const filename = normalizeFilename$1(context.filename ?? "");
31414
32941
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31415
32942
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
31416
32943
  const hrefAttribute = (node.attributes ?? []).find((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "href");
@@ -31484,7 +33011,7 @@ const tanstackStartNoNavigateInRender = defineRule({
31484
33011
  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));
31485
33012
  return {
31486
33013
  CallExpression(node) {
31487
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
33014
+ const filename = normalizeFilename$1(context.filename ?? "");
31488
33015
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31489
33016
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
31490
33017
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
@@ -31494,17 +33021,17 @@ const tanstackStartNoNavigateInRender = defineRule({
31494
33021
  });
31495
33022
  },
31496
33023
  "CallExpression:exit"(node) {
31497
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
33024
+ const filename = normalizeFilename$1(context.filename ?? "");
31498
33025
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31499
33026
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
31500
33027
  },
31501
33028
  JSXAttribute(node) {
31502
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
33029
+ const filename = normalizeFilename$1(context.filename ?? "");
31503
33030
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31504
33031
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
31505
33032
  },
31506
33033
  "JSXAttribute:exit"(node) {
31507
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
33034
+ const filename = normalizeFilename$1(context.filename ?? "");
31508
33035
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31509
33036
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
31510
33037
  }
@@ -31585,7 +33112,7 @@ const tanstackStartNoUseEffectFetch = defineRule({
31585
33112
  severity: "warn",
31586
33113
  recommendation: "Fetch data in the route `loader` instead — the router coordinates loading before rendering to avoid waterfalls",
31587
33114
  create: (context) => ({ CallExpression(node) {
31588
- const filename = normalizeFilename$1(context.getFilename?.() ?? "");
33115
+ const filename = normalizeFilename$1(context.filename ?? "");
31589
33116
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
31590
33117
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
31591
33118
  const callback = node.arguments?.[0];
@@ -33087,6 +34614,28 @@ const reactDoctorRules = [
33087
34614
  category: "Architecture"
33088
34615
  }
33089
34616
  },
34617
+ {
34618
+ key: "react-doctor/no-create-context-in-render",
34619
+ id: "no-create-context-in-render",
34620
+ source: "react-doctor",
34621
+ originallyExternal: false,
34622
+ rule: {
34623
+ ...noCreateContextInRender,
34624
+ framework: "global",
34625
+ category: "Correctness"
34626
+ }
34627
+ },
34628
+ {
34629
+ key: "react-doctor/no-create-store-in-render",
34630
+ id: "no-create-store-in-render",
34631
+ source: "react-doctor",
34632
+ originallyExternal: false,
34633
+ rule: {
34634
+ ...noCreateStoreInRender,
34635
+ framework: "global",
34636
+ category: "Correctness"
34637
+ }
34638
+ },
33090
34639
  {
33091
34640
  key: "react-doctor/no-danger",
33092
34641
  id: "no-danger",
@@ -33285,6 +34834,17 @@ const reactDoctorRules = [
33285
34834
  category: "State & Effects"
33286
34835
  }
33287
34836
  },
34837
+ {
34838
+ key: "react-doctor/no-effect-with-fresh-deps",
34839
+ id: "no-effect-with-fresh-deps",
34840
+ source: "react-doctor",
34841
+ originallyExternal: false,
34842
+ rule: {
34843
+ ...noEffectWithFreshDeps,
34844
+ framework: "global",
34845
+ category: "State & Effects"
34846
+ }
34847
+ },
33288
34848
  {
33289
34849
  key: "react-doctor/no-eval",
33290
34850
  id: "no-eval",
@@ -33758,6 +35318,17 @@ const reactDoctorRules = [
33758
35318
  category: "State & Effects"
33759
35319
  }
33760
35320
  },
35321
+ {
35322
+ key: "react-doctor/no-prop-types",
35323
+ id: "no-prop-types",
35324
+ source: "react-doctor",
35325
+ originallyExternal: false,
35326
+ rule: {
35327
+ ...noPropTypes,
35328
+ framework: "global",
35329
+ category: "Architecture"
35330
+ }
35331
+ },
33761
35332
  {
33762
35333
  key: "react-doctor/no-pure-black-background",
33763
35334
  id: "no-pure-black-background",
@@ -33769,6 +35340,17 @@ const reactDoctorRules = [
33769
35340
  category: "Architecture"
33770
35341
  }
33771
35342
  },
35343
+ {
35344
+ key: "react-doctor/no-random-key",
35345
+ id: "no-random-key",
35346
+ source: "react-doctor",
35347
+ originallyExternal: false,
35348
+ rule: {
35349
+ ...noRandomKey,
35350
+ framework: "global",
35351
+ category: "Correctness"
35352
+ }
35353
+ },
33772
35354
  {
33773
35355
  key: "react-doctor/no-react-children",
33774
35356
  id: "no-react-children",
@@ -33890,6 +35472,17 @@ const reactDoctorRules = [
33890
35472
  category: "Security"
33891
35473
  }
33892
35474
  },
35475
+ {
35476
+ key: "react-doctor/no-self-updating-effect",
35477
+ id: "no-self-updating-effect",
35478
+ source: "react-doctor",
35479
+ originallyExternal: false,
35480
+ rule: {
35481
+ ...noSelfUpdatingEffect,
35482
+ framework: "global",
35483
+ category: "State & Effects"
35484
+ }
35485
+ },
33893
35486
  {
33894
35487
  key: "react-doctor/no-set-state",
33895
35488
  id: "no-set-state",
@@ -34198,6 +35791,39 @@ const reactDoctorRules = [
34198
35791
  category: "Accessibility"
34199
35792
  }
34200
35793
  },
35794
+ {
35795
+ key: "react-doctor/prefer-module-scope-pure-function",
35796
+ id: "prefer-module-scope-pure-function",
35797
+ source: "react-doctor",
35798
+ originallyExternal: false,
35799
+ rule: {
35800
+ ...preferModuleScopePureFunction,
35801
+ framework: "global",
35802
+ category: "Architecture"
35803
+ }
35804
+ },
35805
+ {
35806
+ key: "react-doctor/prefer-module-scope-static-value",
35807
+ id: "prefer-module-scope-static-value",
35808
+ source: "react-doctor",
35809
+ originallyExternal: false,
35810
+ rule: {
35811
+ ...preferModuleScopeStaticValue,
35812
+ framework: "global",
35813
+ category: "Architecture"
35814
+ }
35815
+ },
35816
+ {
35817
+ key: "react-doctor/prefer-stable-empty-fallback",
35818
+ id: "prefer-stable-empty-fallback",
35819
+ source: "react-doctor",
35820
+ originallyExternal: false,
35821
+ rule: {
35822
+ ...preferStableEmptyFallback,
35823
+ framework: "global",
35824
+ category: "Performance"
35825
+ }
35826
+ },
34201
35827
  {
34202
35828
  key: "react-doctor/prefer-tag-over-role",
34203
35829
  id: "prefer-tag-over-role",
@@ -34308,17 +35934,6 @@ const reactDoctorRules = [
34308
35934
  category: "TanStack Query"
34309
35935
  }
34310
35936
  },
34311
- {
34312
- key: "react-doctor/react-compiler-destructure-method",
34313
- id: "react-compiler-destructure-method",
34314
- source: "react-doctor",
34315
- originallyExternal: false,
34316
- rule: {
34317
- ...reactCompilerDestructureMethod,
34318
- framework: "global",
34319
- category: "Architecture"
34320
- }
34321
- },
34322
35937
  {
34323
35938
  key: "react-doctor/react-compiler-no-manual-memoization",
34324
35939
  id: "react-compiler-no-manual-memoization",
@@ -34341,6 +35956,28 @@ const reactDoctorRules = [
34341
35956
  category: "Correctness"
34342
35957
  }
34343
35958
  },
35959
+ {
35960
+ key: "react-doctor/redux-useselector-inline-derivation",
35961
+ id: "redux-useselector-inline-derivation",
35962
+ source: "react-doctor",
35963
+ originallyExternal: false,
35964
+ rule: {
35965
+ ...reduxUseselectorInlineDerivation,
35966
+ framework: "global",
35967
+ category: "Performance"
35968
+ }
35969
+ },
35970
+ {
35971
+ key: "react-doctor/redux-useselector-returns-new-collection",
35972
+ id: "redux-useselector-returns-new-collection",
35973
+ source: "react-doctor",
35974
+ originallyExternal: false,
35975
+ rule: {
35976
+ ...reduxUseselectorReturnsNewCollection,
35977
+ framework: "global",
35978
+ category: "Performance"
35979
+ }
35980
+ },
34344
35981
  {
34345
35982
  key: "react-doctor/rendering-animate-svg-wrapper",
34346
35983
  id: "rendering-animate-svg-wrapper",
@@ -34484,6 +36121,17 @@ const reactDoctorRules = [
34484
36121
  category: "Performance"
34485
36122
  }
34486
36123
  },
36124
+ {
36125
+ key: "react-doctor/rerender-lazy-ref-init",
36126
+ id: "rerender-lazy-ref-init",
36127
+ source: "react-doctor",
36128
+ originallyExternal: false,
36129
+ rule: {
36130
+ ...rerenderLazyRefInit,
36131
+ framework: "global",
36132
+ category: "Performance"
36133
+ }
36134
+ },
34487
36135
  {
34488
36136
  key: "react-doctor/rerender-lazy-state-init",
34489
36137
  id: "rerender-lazy-state-init",
@@ -35254,7 +36902,7 @@ const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id,
35254
36902
  const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
35255
36903
  const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
35256
36904
  const isReactNativeFileActive = (context) => {
35257
- const rawFilename = context.getFilename?.();
36905
+ const rawFilename = context.filename;
35258
36906
  if (!rawFilename) return true;
35259
36907
  const filename = normalizeFilename$1(rawFilename);
35260
36908
  if (NATIVE_FILE_EXTENSION_PATTERN.test(filename)) return true;
@@ -35308,7 +36956,7 @@ const appendNode = (builder, block, node) => {
35308
36956
  };
35309
36957
  const mapDescendantsToBlock = (builder, node, block) => {
35310
36958
  builder.nodeBlock.set(node, block);
35311
- if (isFunctionLike$1(node)) return;
36959
+ if (isFunctionLike$2(node)) return;
35312
36960
  const record = node;
35313
36961
  for (const key of Object.keys(record)) {
35314
36962
  if (key === "parent") continue;
@@ -35646,7 +37294,7 @@ const analyzeControlFlow = (program) => {
35646
37294
  body: program.body
35647
37295
  });
35648
37296
  const visit = (node) => {
35649
- if (isFunctionLike$1(node)) {
37297
+ if (isFunctionLike$2(node)) {
35650
37298
  const body = node.body;
35651
37299
  if (body) buildFor(node, body);
35652
37300
  }
@@ -35663,7 +37311,7 @@ const analyzeControlFlow = (program) => {
35663
37311
  const enclosingFunction = (node) => {
35664
37312
  let current = node;
35665
37313
  while (current) {
35666
- if (isFunctionLike$1(current)) return current;
37314
+ if (isFunctionLike$2(current)) return current;
35667
37315
  if (isNodeOfType(current, "Program")) return current;
35668
37316
  current = current.parent ?? null;
35669
37317
  }
@@ -35742,7 +37390,9 @@ const wrapWithSemanticContext = (rule) => ({
35742
37390
  };
35743
37391
  const enrichedContext = {
35744
37392
  report: baseContext.report,
35745
- getFilename: baseContext.getFilename,
37393
+ get filename() {
37394
+ return baseContext.filename ?? baseContext.getFilename?.();
37395
+ },
35746
37396
  settings: baseContext.settings,
35747
37397
  get scopes() {
35748
37398
  return getScopes();