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

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 +408 -0
  2. package/dist/index.js +2142 -391
  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/",
@@ -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",
@@ -1026,7 +1034,7 @@ const altText = defineRule({
1026
1034
  });
1027
1035
  //#endregion
1028
1036
  //#region src/plugin/rules/a11y/anchor-ambiguous-text.ts
1029
- 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").`;
1030
1038
  const DEFAULT_AMBIGUOUS = [
1031
1039
  "click here",
1032
1040
  "here",
@@ -1083,14 +1091,14 @@ const anchorAmbiguousText = defineRule({
1083
1091
  const normalized = normalizeText(accessibleText);
1084
1092
  if (ambiguousSet.has(normalized)) context.report({
1085
1093
  node: node.openingElement.name,
1086
- message: buildMessage$28(normalized)
1094
+ message: buildMessage$30(normalized)
1087
1095
  });
1088
1096
  } };
1089
1097
  }
1090
1098
  });
1091
1099
  //#endregion
1092
1100
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1093
- 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`.";
1094
1102
  const anchorHasContent = defineRule({
1095
1103
  id: "anchor-has-content",
1096
1104
  tags: ["react-jsx-only"],
@@ -1105,7 +1113,7 @@ const anchorHasContent = defineRule({
1105
1113
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1106
1114
  context.report({
1107
1115
  node: opening.name,
1108
- message: MESSAGE$47
1116
+ message: MESSAGE$49
1109
1117
  });
1110
1118
  } })
1111
1119
  });
@@ -1498,7 +1506,7 @@ const parseJsxValue = (value) => {
1498
1506
  };
1499
1507
  //#endregion
1500
1508
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
1501
- 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.";
1502
1510
  const ariaActivedescendantHasTabindex = defineRule({
1503
1511
  id: "aria-activedescendant-has-tabindex",
1504
1512
  tags: ["react-jsx-only"],
@@ -1515,14 +1523,14 @@ const ariaActivedescendantHasTabindex = defineRule({
1515
1523
  if (tabIndexValue === null || tabIndexValue >= -1) return;
1516
1524
  context.report({
1517
1525
  node: node.name,
1518
- message: MESSAGE$46
1526
+ message: MESSAGE$48
1519
1527
  });
1520
1528
  return;
1521
1529
  }
1522
1530
  if (isInteractiveElement(tag, node)) return;
1523
1531
  context.report({
1524
1532
  node: node.name,
1525
- message: MESSAGE$46
1533
+ message: MESSAGE$48
1526
1534
  });
1527
1535
  } })
1528
1536
  });
@@ -1662,7 +1670,7 @@ const ARIA_PROPERTIES = new Map([
1662
1670
  const isValidAriaProperty = (name) => ARIA_PROPERTIES.has(name);
1663
1671
  //#endregion
1664
1672
  //#region src/plugin/rules/a11y/aria-props.ts
1665
- 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.`;
1666
1674
  const ariaProps = defineRule({
1667
1675
  id: "aria-props",
1668
1676
  tags: ["react-jsx-only"],
@@ -1675,7 +1683,7 @@ const ariaProps = defineRule({
1675
1683
  if (!name || !name.startsWith("aria-")) return;
1676
1684
  if (!isValidAriaProperty(name)) context.report({
1677
1685
  node: node.name,
1678
- message: buildMessage$27(name)
1686
+ message: buildMessage$29(name)
1679
1687
  });
1680
1688
  } })
1681
1689
  });
@@ -1826,7 +1834,7 @@ const buildExpectedDescription = (propType) => {
1826
1834
  case "token-list": return `a space-separated list of: ${propType.tokens.join(", ")}`;
1827
1835
  }
1828
1836
  };
1829
- const buildMessage$26 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1837
+ const buildMessage$28 = (propName, propType) => `\`${propName}\` value must be ${buildExpectedDescription(propType)}.`;
1830
1838
  const allowNoneValue = (propType) => {
1831
1839
  switch (propType.kind) {
1832
1840
  case "boolean":
@@ -1959,13 +1967,13 @@ const ariaProptypes = defineRule({
1959
1967
  if (!node.value) {
1960
1968
  if (!allowNoneValue(propType)) context.report({
1961
1969
  node,
1962
- message: buildMessage$26(propName, propType)
1970
+ message: buildMessage$28(propName, propType)
1963
1971
  });
1964
1972
  return;
1965
1973
  }
1966
1974
  if (!isValidValueForType(propType, node.value)) context.report({
1967
1975
  node,
1968
- message: buildMessage$26(propName, propType)
1976
+ message: buildMessage$28(propName, propType)
1969
1977
  });
1970
1978
  } })
1971
1979
  });
@@ -2277,7 +2285,7 @@ const ariaRole = defineRule({
2277
2285
  });
2278
2286
  //#endregion
2279
2287
  //#region src/plugin/rules/a11y/aria-unsupported-elements.ts
2280
- 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.`;
2281
2289
  const ariaUnsupportedElements = defineRule({
2282
2290
  id: "aria-unsupported-elements",
2283
2291
  tags: ["react-jsx-only"],
@@ -2294,7 +2302,7 @@ const ariaUnsupportedElements = defineRule({
2294
2302
  if (!attrName) continue;
2295
2303
  if (attrName.startsWith("aria-") || attrName === "role") context.report({
2296
2304
  node: attribute,
2297
- message: buildMessage$25(tag, attrName)
2305
+ message: buildMessage$27(tag, attrName)
2298
2306
  });
2299
2307
  }
2300
2308
  } })
@@ -2327,7 +2335,7 @@ const BUILTIN_GLOBAL_NAMESPACE_NAMES = new Set([
2327
2335
  "BigInt",
2328
2336
  "Reflect"
2329
2337
  ]);
2330
- const MUTATING_ARRAY_METHODS$1 = new Set([
2338
+ const MUTATING_ARRAY_METHODS = new Set([
2331
2339
  "push",
2332
2340
  "pop",
2333
2341
  "shift",
@@ -2338,6 +2346,12 @@ const MUTATING_ARRAY_METHODS$1 = new Set([
2338
2346
  "fill",
2339
2347
  "copyWithin"
2340
2348
  ]);
2349
+ const MUTATING_COLLECTION_METHODS = new Set([
2350
+ "add",
2351
+ "clear",
2352
+ "delete",
2353
+ "set"
2354
+ ]);
2341
2355
  const CHAINABLE_ITERATION_METHODS = new Set([
2342
2356
  "map",
2343
2357
  "filter",
@@ -2549,7 +2563,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
2549
2563
  * (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
2550
2564
  * type-guard form covers both shapes without callers paying a cast.
2551
2565
  */
2552
- 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")));
2553
2567
  //#endregion
2554
2568
  //#region src/plugin/utils/is-inline-function-expression.ts
2555
2569
  /**
@@ -2572,7 +2586,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
2572
2586
  let firstAwait = null;
2573
2587
  walkAst(block, (child) => {
2574
2588
  if (firstAwait) return false;
2575
- if (child !== block && isFunctionLike$1(child)) return false;
2589
+ if (child !== block && isFunctionLike$2(child)) return false;
2576
2590
  if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
2577
2591
  });
2578
2592
  return firstAwait;
@@ -3027,13 +3041,13 @@ const asyncDeferAwait = defineRule({
3027
3041
  const inspectAllStatementBlocks = (functionBody) => {
3028
3042
  if (!functionBody) return;
3029
3043
  walkAst(functionBody, (descendant) => {
3030
- if (isFunctionLike$1(descendant)) return false;
3044
+ if (isFunctionLike$2(descendant)) return false;
3031
3045
  if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
3032
3046
  else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
3033
3047
  });
3034
3048
  };
3035
3049
  const enterFunction = (node) => {
3036
- if (!isFunctionLike$1(node)) return;
3050
+ if (!isFunctionLike$2(node)) return;
3037
3051
  if (!node.async) return;
3038
3052
  if (!isNodeOfType(node.body, "BlockStatement")) return;
3039
3053
  inspectAllStatementBlocks(node.body);
@@ -3164,7 +3178,7 @@ const asyncParallel = defineRule({
3164
3178
  });
3165
3179
  //#endregion
3166
3180
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
3167
- 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.`;
3168
3182
  const AUTOFILL_TOKENS = new Set([
3169
3183
  "off",
3170
3184
  "on",
@@ -3252,7 +3266,7 @@ const autocompleteValid = defineRule({
3252
3266
  if (!AUTOFILL_TOKENS.has(token)) {
3253
3267
  context.report({
3254
3268
  node: attribute,
3255
- message: buildMessage$24(value)
3269
+ message: buildMessage$26(value)
3256
3270
  });
3257
3271
  return;
3258
3272
  }
@@ -3527,7 +3541,7 @@ const isPureEventBlockerHandler = (attribute) => {
3527
3541
  //#endregion
3528
3542
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
3529
3543
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
3530
- 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`).";
3531
3545
  const KEY_HANDLERS = [
3532
3546
  "onKeyUp",
3533
3547
  "onKeyDown",
@@ -3558,7 +3572,7 @@ const clickEventsHaveKeyEvents = defineRule({
3558
3572
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
3559
3573
  context.report({
3560
3574
  node: node.name,
3561
- message: MESSAGE$45
3575
+ message: MESSAGE$47
3562
3576
  });
3563
3577
  } };
3564
3578
  }
@@ -3669,7 +3683,7 @@ const stripParenExpression = (node) => {
3669
3683
  };
3670
3684
  //#endregion
3671
3685
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
3672
- 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`.";
3673
3687
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
3674
3688
  const DEFAULT_LABELLING_PROPS = [
3675
3689
  "alt",
@@ -3829,7 +3843,7 @@ const controlHasAssociatedLabel = defineRule({
3829
3843
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
3830
3844
  context.report({
3831
3845
  node: opening,
3832
- message: MESSAGE$44
3846
+ message: MESSAGE$46
3833
3847
  });
3834
3848
  } };
3835
3849
  }
@@ -4153,14 +4167,21 @@ const isEs6Component = (node) => {
4153
4167
  };
4154
4168
  //#endregion
4155
4169
  //#region src/plugin/rules/react-builtins/display-name.ts
4156
- 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
+ ];
4157
4176
  const resolveSettings$45 = (settings) => {
4158
4177
  const reactDoctor = settings?.["react-doctor"];
4159
4178
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
4179
+ const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
4160
4180
  return {
4161
4181
  ignoreTranspilerName: ruleSettings.ignoreTranspilerName ?? false,
4162
4182
  checkContextObjects: ruleSettings.checkContextObjects ?? false,
4163
- reactVersion: ruleSettings.reactVersion ?? ""
4183
+ reactVersion: ruleSettings.reactVersion ?? "",
4184
+ additionalHoCs
4164
4185
  };
4165
4186
  };
4166
4187
  const isReactVersionAtLeast$1 = (version, major, minor) => {
@@ -4233,29 +4254,28 @@ const isCreateContextCall = (node) => {
4233
4254
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
4234
4255
  return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "createContext";
4235
4256
  };
4236
- const isObserverCall = (node) => {
4237
- if (!isNodeOfType(node, "CallExpression")) return false;
4238
- const callee = node.callee;
4239
- if (isNodeOfType(callee, "Identifier")) return callee.name === "observer";
4240
- return isNodeOfType(callee, "MemberExpression") && getStaticMemberName(callee) === "observer";
4241
- };
4242
- const getCallName = (node) => {
4243
- if (!isNodeOfType(node, "CallExpression")) return null;
4244
- const callee = node.callee;
4245
- if (isNodeOfType(callee, "Identifier")) return callee.name;
4246
- if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName(callee);
4247
- return null;
4248
- };
4249
4257
  const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
4250
4258
  const firstCallArgument = (node) => {
4251
4259
  if (!isNodeOfType(node, "CallExpression")) return null;
4252
4260
  const first = node.arguments[0];
4253
4261
  return first ? first : null;
4254
4262
  };
4255
- const isDisplayNameHoC = (node) => {
4256
- const callName = getCallName(node);
4257
- 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;
4258
4277
  };
4278
+ const isDisplayNameHoC = (node, additionalHoCs) => resolveHoCCalleeName(node, additionalHoCs) !== null;
4259
4279
  const supportsComposedForwardRefDisplayName = (version) => {
4260
4280
  if (!version) return false;
4261
4281
  if (isReactVersionAtLeast$1(version, 15, 7)) return true;
@@ -4263,17 +4283,17 @@ const supportsComposedForwardRefDisplayName = (version) => {
4263
4283
  return Boolean(match && Number(match[1]) >= 11);
4264
4284
  };
4265
4285
  const shouldReportHoCDisplayName = (node, settings) => {
4266
- if (!isDisplayNameHoC(node)) return false;
4286
+ if (!isDisplayNameHoC(node, settings.additionalHoCs)) return false;
4267
4287
  if (!containsJsx$1(node)) return false;
4268
4288
  const assignedName = getAssignedName(node);
4269
4289
  const programRoot = findProgramRoot(node);
4270
4290
  if (assignedName && programRoot && hasDisplayNameAssignment(assignedName, programRoot)) return false;
4271
- const callName = getCallName(node);
4291
+ const callName = resolveHoCCalleeName(node, settings.additionalHoCs);
4272
4292
  const firstArgument = firstCallArgument(node);
4273
4293
  if (!firstArgument) return false;
4274
- 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;
4275
4295
  if (callName === "memo" && isNodeOfType(firstArgument, "CallExpression")) {
4276
- if (getCallName(firstArgument) !== "forwardRef") return false;
4296
+ if (resolveHoCCalleeName(firstArgument, settings.additionalHoCs) !== "forwardRef") return false;
4277
4297
  return !supportsComposedForwardRefDisplayName(settings.reactVersion);
4278
4298
  }
4279
4299
  if (isNamedFunctionLike(firstArgument)) return false;
@@ -4349,7 +4369,7 @@ const displayName = defineRule({
4349
4369
  const reportAt = (node) => {
4350
4370
  context.report({
4351
4371
  node,
4352
- message: MESSAGE$43
4372
+ message: MESSAGE$45
4353
4373
  });
4354
4374
  };
4355
4375
  return {
@@ -4427,10 +4447,6 @@ const displayName = defineRule({
4427
4447
  reportAt(node);
4428
4448
  return;
4429
4449
  }
4430
- if (isObserverCall(node) && containsJsx$1(node)) {
4431
- reportAt(node);
4432
- return;
4433
- }
4434
4450
  if (shouldReportHoCDisplayName(node, settings)) {
4435
4451
  reportAt(node);
4436
4452
  return;
@@ -4472,7 +4488,7 @@ const displayName = defineRule({
4472
4488
  //#region src/plugin/utils/walk-inside-statement-blocks.ts
4473
4489
  const walkInsideStatementBlocks = (node, visitor) => {
4474
4490
  if (!node || typeof node !== "object") return;
4475
- if (isFunctionLike$1(node)) return;
4491
+ if (isFunctionLike$2(node)) return;
4476
4492
  visitor(node);
4477
4493
  const nodeRecord = node;
4478
4494
  for (const key of Object.keys(nodeRecord)) {
@@ -4560,7 +4576,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4560
4576
  let didFindRelease = false;
4561
4577
  walkAst(node, (child) => {
4562
4578
  if (didFindRelease) return false;
4563
- if (child !== node && isFunctionLike$1(child) && !isIteratorCallbackArgument(child)) return false;
4579
+ if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
4564
4580
  if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
4565
4581
  didFindRelease = true;
4566
4582
  return false;
@@ -4569,7 +4585,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
4569
4585
  return didFindRelease;
4570
4586
  };
4571
4587
  const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
4572
- if (!isFunctionLike$1(node)) return false;
4588
+ if (!isFunctionLike$2(node)) return false;
4573
4589
  return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
4574
4590
  };
4575
4591
  const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
@@ -4811,7 +4827,7 @@ const recordReference = (state, identifier, flag) => {
4811
4827
  };
4812
4828
  const isFunctionBodyBlock = (block) => {
4813
4829
  if (!block.parent) return false;
4814
- return isFunctionLike$1(block.parent);
4830
+ return isFunctionLike$2(block.parent);
4815
4831
  };
4816
4832
  const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
4817
4833
  const handleVariableDeclaration = (declaration, state) => {
@@ -4966,7 +4982,7 @@ const walkParameterReferences = (pattern, state) => {
4966
4982
  if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
4967
4983
  };
4968
4984
  const walk = (node, state) => {
4969
- if (isFunctionLike$1(node)) {
4985
+ if (isFunctionLike$2(node)) {
4970
4986
  if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
4971
4987
  setNodeScope(node, state);
4972
4988
  const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
@@ -5212,7 +5228,7 @@ const closureCaptures = (functionNode, scopes) => {
5212
5228
  const out = [];
5213
5229
  const seen = /* @__PURE__ */ new Set();
5214
5230
  const visit = (node) => {
5215
- if (node !== functionNode && isFunctionLike$1(node)) {
5231
+ if (node !== functionNode && isFunctionLike$2(node)) {
5216
5232
  const innerCaptures = closureCaptures(node, scopes);
5217
5233
  for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
5218
5234
  if (!seen.has(reference.id)) {
@@ -5284,7 +5300,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
5284
5300
  "ParenthesizedExpression",
5285
5301
  "ChainExpression"
5286
5302
  ]);
5287
- const unwrapExpression = (node) => {
5303
+ const unwrapExpression$1 = (node) => {
5288
5304
  let current = node;
5289
5305
  while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
5290
5306
  const inner = current.expression;
@@ -5391,7 +5407,7 @@ const symbolHasStableHookOrigin = (symbol) => {
5391
5407
  if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
5392
5408
  const initializerRaw = declarator.init;
5393
5409
  if (!initializerRaw) return false;
5394
- const initializer = unwrapExpression(initializerRaw);
5410
+ const initializer = unwrapExpression$1(initializerRaw);
5395
5411
  if (symbol.kind === "const") {
5396
5412
  if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
5397
5413
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
@@ -5411,13 +5427,13 @@ const symbolHasStableHookOrigin = (symbol) => {
5411
5427
  return false;
5412
5428
  };
5413
5429
  const symbolHasUseEffectEventOrigin = (symbol) => {
5414
- const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
5430
+ const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
5415
5431
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
5416
5432
  return getHookName(initializer.callee) === "useEffectEvent";
5417
5433
  };
5418
5434
  const getFunctionValueNode = (symbol) => {
5419
5435
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
5420
- const initializer = symbol.initializer ? unwrapExpression(symbol.initializer) : null;
5436
+ const initializer = symbol.initializer ? unwrapExpression$1(symbol.initializer) : null;
5421
5437
  if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
5422
5438
  return null;
5423
5439
  };
@@ -5552,23 +5568,23 @@ const computeDepKey = (reference) => {
5552
5568
  return fullName;
5553
5569
  };
5554
5570
  const computeDeclaredDepKey = (entry) => {
5555
- const stripped = unwrapExpression(entry);
5571
+ const stripped = unwrapExpression$1(entry);
5556
5572
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
5557
5573
  if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
5558
5574
  return null;
5559
5575
  };
5560
5576
  const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
5561
5577
  if (!depsArgument) return false;
5562
- const strippedDepsArgument = unwrapExpression(depsArgument);
5578
+ const strippedDepsArgument = unwrapExpression$1(depsArgument);
5563
5579
  if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
5564
5580
  return strippedDepsArgument.elements.some((element) => {
5565
5581
  if (!element) return false;
5566
- const strippedElement = unwrapExpression(element);
5582
+ const strippedElement = unwrapExpression$1(element);
5567
5583
  return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
5568
5584
  });
5569
5585
  };
5570
5586
  const stringifyMemberChain = (node) => {
5571
- const stripped = unwrapExpression(node);
5587
+ const stripped = unwrapExpression$1(node);
5572
5588
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
5573
5589
  if (isNodeOfType(stripped, "ThisExpression")) return "this";
5574
5590
  if (isNodeOfType(stripped, "MemberExpression")) {
@@ -5610,13 +5626,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
5610
5626
  return false;
5611
5627
  };
5612
5628
  const getMemberRootIdentifier = (node) => {
5613
- const stripped = unwrapExpression(node);
5629
+ const stripped = unwrapExpression$1(node);
5614
5630
  if (isNodeOfType(stripped, "Identifier")) return stripped;
5615
5631
  if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
5616
5632
  return null;
5617
5633
  };
5618
5634
  const hasComputedMemberExpression = (node) => {
5619
- const stripped = unwrapExpression(node);
5635
+ const stripped = unwrapExpression$1(node);
5620
5636
  if (!isNodeOfType(stripped, "MemberExpression")) return false;
5621
5637
  if (stripped.computed) return true;
5622
5638
  return hasComputedMemberExpression(stripped.object);
@@ -5637,7 +5653,7 @@ const isRegExpLiteral = (node) => {
5637
5653
  };
5638
5654
  const isUnstableInitializer = (node) => {
5639
5655
  if (!node) return false;
5640
- const stripped = unwrapExpression(node);
5656
+ const stripped = unwrapExpression$1(node);
5641
5657
  if (isRegExpLiteral(stripped)) return true;
5642
5658
  if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
5643
5659
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
@@ -5793,7 +5809,7 @@ const hasMemberCallForRoot = (node, rootName) => {
5793
5809
  const visit = (current) => {
5794
5810
  if (didFindMemberCall) return;
5795
5811
  if (isNodeOfType(current, "CallExpression")) {
5796
- if (getMemberRootIdentifier(unwrapExpression(current.callee))?.name === rootName) {
5812
+ if (getMemberRootIdentifier(unwrapExpression$1(current.callee))?.name === rootName) {
5797
5813
  didFindMemberCall = true;
5798
5814
  return;
5799
5815
  }
@@ -5855,7 +5871,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5855
5871
  return;
5856
5872
  }
5857
5873
  const depsArgumentRaw = node.arguments[depsArgumentIndex];
5858
- const callbackExpression = unwrapExpression(callbackArgument);
5874
+ const callbackExpression = unwrapExpression$1(callbackArgument);
5859
5875
  let callbackToAnalyze = null;
5860
5876
  const forcedCaptureKeys = /* @__PURE__ */ new Set();
5861
5877
  if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
@@ -5863,7 +5879,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5863
5879
  const callbackSymbol = context.scopes.symbolFor(callbackExpression);
5864
5880
  const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
5865
5881
  if (functionValueNode) callbackToAnalyze = functionValueNode;
5866
- 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);
5867
5883
  else if (depsArgumentRaw) {
5868
5884
  if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
5869
5885
  context.report({
@@ -5920,7 +5936,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5920
5936
  });
5921
5937
  return;
5922
5938
  }
5923
- const depsArgument = unwrapExpression(depsArgumentRaw);
5939
+ const depsArgument = unwrapExpression$1(depsArgumentRaw);
5924
5940
  if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
5925
5941
  if (isAutoDependenciesHook(hookName)) return;
5926
5942
  if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
@@ -5961,11 +5977,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
5961
5977
  for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
5962
5978
  const hasLiteralDepElement = depsArgument.elements.some((element) => {
5963
5979
  if (!element) return false;
5964
- return isLiteralOrEmptyTemplate(unwrapExpression(element));
5980
+ return isLiteralOrEmptyTemplate(unwrapExpression$1(element));
5965
5981
  });
5966
5982
  const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
5967
5983
  if (!element) return false;
5968
- return isNonStringLiteral(unwrapExpression(element));
5984
+ return isNonStringLiteral(unwrapExpression$1(element));
5969
5985
  });
5970
5986
  if (hasNonStringLiteralDep) context.report({
5971
5987
  node: depsArgument,
@@ -5986,7 +6002,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
5986
6002
  });
5987
6003
  continue;
5988
6004
  }
5989
- const stripped = unwrapExpression(elementNode);
6005
+ const stripped = unwrapExpression$1(elementNode);
5990
6006
  if (isLiteralOrEmptyTemplate(stripped)) continue;
5991
6007
  if (isNodeOfType(stripped, "Identifier")) {
5992
6008
  const depSymbol = context.scopes.symbolFor(stripped);
@@ -6206,7 +6222,7 @@ const flattenJsxName$1 = (name) => {
6206
6222
  return "";
6207
6223
  };
6208
6224
  const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
6209
- 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.`;
6210
6226
  const forbidComponentProps = defineRule({
6211
6227
  id: "forbid-component-props",
6212
6228
  severity: "warn",
@@ -6232,7 +6248,7 @@ const forbidComponentProps = defineRule({
6232
6248
  if (!isForbiddenForTag(entry, tag)) continue;
6233
6249
  context.report({
6234
6250
  node: attribute,
6235
- message: buildMessage$23(propName, entry.message)
6251
+ message: buildMessage$25(propName, entry.message)
6236
6252
  });
6237
6253
  break;
6238
6254
  }
@@ -6242,7 +6258,7 @@ const forbidComponentProps = defineRule({
6242
6258
  });
6243
6259
  //#endregion
6244
6260
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6245
- 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.`;
6246
6262
  const resolveSettings$43 = (settings) => {
6247
6263
  const reactDoctor = settings?.["react-doctor"];
6248
6264
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
@@ -6280,7 +6296,7 @@ const forbidDomProps = defineRule({
6280
6296
  if (disallowedFor && disallowedFor.size > 0 && !disallowedFor.has(elementName)) continue;
6281
6297
  context.report({
6282
6298
  node: attribute.name,
6283
- message: buildMessage$22(propName, descriptor.message)
6299
+ message: buildMessage$24(propName, descriptor.message)
6284
6300
  });
6285
6301
  }
6286
6302
  } };
@@ -6350,7 +6366,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6350
6366
  };
6351
6367
  //#endregion
6352
6368
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6353
- 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.`;
6354
6370
  const resolveSettings$42 = (settings) => {
6355
6371
  const reactDoctor = settings?.["react-doctor"];
6356
6372
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
@@ -6374,7 +6390,7 @@ const forbidElements = defineRule({
6374
6390
  if (!fullName || !forbidMap.has(fullName)) return;
6375
6391
  context.report({
6376
6392
  node: node.name,
6377
- message: buildMessage$21(fullName, forbidMap.get(fullName))
6393
+ message: buildMessage$23(fullName, forbidMap.get(fullName))
6378
6394
  });
6379
6395
  },
6380
6396
  CallExpression(node) {
@@ -6394,7 +6410,7 @@ const forbidElements = defineRule({
6394
6410
  if (!elementName || !forbidMap.has(elementName)) return;
6395
6411
  context.report({
6396
6412
  node: firstArgument,
6397
- message: buildMessage$21(elementName, forbidMap.get(elementName))
6413
+ message: buildMessage$23(elementName, forbidMap.get(elementName))
6398
6414
  });
6399
6415
  }
6400
6416
  };
@@ -6402,7 +6418,7 @@ const forbidElements = defineRule({
6402
6418
  });
6403
6419
  //#endregion
6404
6420
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
6405
- 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.";
6406
6422
  const forwardRefUsesRef = defineRule({
6407
6423
  id: "forward-ref-uses-ref",
6408
6424
  severity: "warn",
@@ -6421,13 +6437,13 @@ const forwardRefUsesRef = defineRule({
6421
6437
  if (isNodeOfType(onlyParam, "RestElement")) return;
6422
6438
  context.report({
6423
6439
  node: inner,
6424
- message: MESSAGE$42
6440
+ message: MESSAGE$44
6425
6441
  });
6426
6442
  } })
6427
6443
  });
6428
6444
  //#endregion
6429
6445
  //#region src/plugin/rules/a11y/heading-has-content.ts
6430
- 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`).";
6431
6447
  const DEFAULT_HEADING_TAGS = [
6432
6448
  "h1",
6433
6449
  "h2",
@@ -6459,7 +6475,7 @@ const headingHasContent = defineRule({
6459
6475
  if (isHiddenFromScreenReader(node, context.settings)) return;
6460
6476
  context.report({
6461
6477
  node,
6462
- message: MESSAGE$41
6478
+ message: MESSAGE$43
6463
6479
  });
6464
6480
  } };
6465
6481
  }
@@ -6595,7 +6611,7 @@ const hooksNoNanInDeps = defineRule({
6595
6611
  });
6596
6612
  //#endregion
6597
6613
  //#region src/plugin/rules/a11y/html-has-lang.ts
6598
- 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.";
6599
6615
  const resolveSettings$39 = (settings) => {
6600
6616
  const reactDoctor = settings?.["react-doctor"];
6601
6617
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -6642,7 +6658,7 @@ const htmlHasLang = defineRule({
6642
6658
  if (!lang) {
6643
6659
  context.report({
6644
6660
  node: node.name,
6645
- message: MESSAGE$40
6661
+ message: MESSAGE$42
6646
6662
  });
6647
6663
  return;
6648
6664
  }
@@ -6650,13 +6666,13 @@ const htmlHasLang = defineRule({
6650
6666
  if (verdict === "missing" || verdict === "empty") {
6651
6667
  context.report({
6652
6668
  node: lang,
6653
- message: MESSAGE$40
6669
+ message: MESSAGE$42
6654
6670
  });
6655
6671
  return;
6656
6672
  }
6657
6673
  if (hasSpread && !lang) context.report({
6658
6674
  node: node.name,
6659
- message: MESSAGE$40
6675
+ message: MESSAGE$42
6660
6676
  });
6661
6677
  } };
6662
6678
  }
@@ -6696,7 +6712,7 @@ const BLOCK_LEVEL_ELEMENTS = new Set([
6696
6712
  "table",
6697
6713
  "ul"
6698
6714
  ]);
6699
- const buildMessage$20 = (childTagName) => `Block-level \`<${childTagName}>\` cannot appear inside a \`<p>\` — the HTML parser auto-closes the paragraph at the start of \`<${childTagName}>\`, splitting your DOM in ways the renderer never expressed and triggering hydration mismatches.`;
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.`;
6700
6716
  const isParagraphElement = (candidate) => {
6701
6717
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6702
6718
  const opening = candidate.openingElement;
@@ -6724,7 +6740,7 @@ const htmlNoInvalidParagraphChild = defineRule({
6724
6740
  if (!findEnclosingParagraph(node)) return;
6725
6741
  context.report({
6726
6742
  node: node.name,
6727
- message: buildMessage$20(childTagName)
6743
+ message: buildMessage$22(childTagName)
6728
6744
  });
6729
6745
  } })
6730
6746
  });
@@ -6744,7 +6760,7 @@ const ROW_GROUPS = new Set([
6744
6760
  "tbody",
6745
6761
  "tfoot"
6746
6762
  ]);
6747
- const buildMessage$19 = (childTag, expectedParent, actualParent) => `Improper table nesting — \`<${childTag}>\` must be a direct child of ${expectedParent}, but its nearest host ancestor is \`<${actualParent}>\`. Browsers auto-rewrite invalid table structure, producing a DOM that doesn't match the JSX (broken hydration, broken \`>\` selectors, broken accessibility tree).`;
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).`;
6748
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.";
6749
6765
  const getHostTagName = (jsxElement) => {
6750
6766
  if (!isNodeOfType(jsxElement, "JSXElement")) return null;
@@ -6812,28 +6828,28 @@ const htmlNoInvalidTableNesting = defineRule({
6812
6828
  if (ROW_GROUPS.has(tagName)) {
6813
6829
  if (actualParent !== "table") context.report({
6814
6830
  node: node.openingElement.name,
6815
- message: buildMessage$19(tagName, "`<table>`", actualParent)
6831
+ message: buildMessage$21(tagName, "`<table>`", actualParent)
6816
6832
  });
6817
6833
  return;
6818
6834
  }
6819
6835
  if (tagName === "tr") {
6820
6836
  if (!ROW_GROUPS.has(actualParent) && actualParent !== "table") context.report({
6821
6837
  node: node.openingElement.name,
6822
- message: buildMessage$19(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6838
+ message: buildMessage$21(tagName, "`<thead>`, `<tbody>`, or `<tfoot>`", actualParent)
6823
6839
  });
6824
6840
  return;
6825
6841
  }
6826
6842
  if (tagName === "td" || tagName === "th") {
6827
6843
  if (actualParent !== "tr") context.report({
6828
6844
  node: node.openingElement.name,
6829
- message: buildMessage$19(tagName, "`<tr>`", actualParent)
6845
+ message: buildMessage$21(tagName, "`<tr>`", actualParent)
6830
6846
  });
6831
6847
  }
6832
6848
  } })
6833
6849
  });
6834
6850
  //#endregion
6835
6851
  //#region src/plugin/rules/correctness/html-no-nested-interactive.ts
6836
- const buildMessage$18 = (tagName) => `Improper nesting of \`<${tagName}>\` inside another \`<${tagName}>\` — the HTML parser auto-closes the outer element, splitting your DOM in ways the renderer never expressed and breaking event delegation, focus, and accessibility.`;
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.`;
6837
6853
  const isJsxElementWithTagName = (candidate, tagName) => {
6838
6854
  if (!isNodeOfType(candidate, "JSXElement")) return false;
6839
6855
  const opening = candidate.openingElement;
@@ -6861,13 +6877,13 @@ const htmlNoNestedInteractive = defineRule({
6861
6877
  if (!findEnclosingSameTag(node, tagName)) return;
6862
6878
  context.report({
6863
6879
  node: node.name,
6864
- message: buildMessage$18(tagName)
6880
+ message: buildMessage$20(tagName)
6865
6881
  });
6866
6882
  } })
6867
6883
  });
6868
6884
  //#endregion
6869
6885
  //#region src/plugin/rules/a11y/iframe-has-title.ts
6870
- 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.";
6871
6887
  const evaluateTitleValue = (value) => {
6872
6888
  if (!value) return "missing";
6873
6889
  if (isNodeOfType(value, "Literal")) {
@@ -6906,14 +6922,14 @@ const iframeHasTitle = defineRule({
6906
6922
  if (!titleAttr) {
6907
6923
  if (hasSpread || tag === "iframe") context.report({
6908
6924
  node: node.name,
6909
- message: MESSAGE$39
6925
+ message: MESSAGE$41
6910
6926
  });
6911
6927
  return;
6912
6928
  }
6913
6929
  const verdict = evaluateTitleValue(titleAttr.value);
6914
6930
  if (verdict === "missing" || verdict === "empty") context.report({
6915
6931
  node: titleAttr,
6916
- message: MESSAGE$39
6932
+ message: MESSAGE$41
6917
6933
  });
6918
6934
  } })
6919
6935
  });
@@ -7016,7 +7032,7 @@ const iframeMissingSandbox = defineRule({
7016
7032
  });
7017
7033
  //#endregion
7018
7034
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
7019
- 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.";
7020
7036
  const DEFAULT_COMPONENTS = ["img"];
7021
7037
  const DEFAULT_REDUNDANT_WORDS = [
7022
7038
  "image",
@@ -7078,7 +7094,7 @@ const imgRedundantAlt = defineRule({
7078
7094
  if (!altAttribute) return;
7079
7095
  if (altValueRedundant(altAttribute, settings.words)) context.report({
7080
7096
  node: altAttribute,
7081
- message: MESSAGE$38
7097
+ message: MESSAGE$40
7082
7098
  });
7083
7099
  } };
7084
7100
  }
@@ -7273,7 +7289,7 @@ const isAtomFromJotai = (callExpression) => {
7273
7289
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
7274
7290
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
7275
7291
  };
7276
- const isFunctionLike = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7292
+ const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
7277
7293
  const getFirstParameterName = (fn) => {
7278
7294
  const parameters = fn.params ?? [];
7279
7295
  if (parameters.length !== 1) return null;
@@ -7391,7 +7407,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7391
7407
  const args = node.arguments ?? [];
7392
7408
  if (args.length === 0) return;
7393
7409
  const reader = args[0];
7394
- if (!isFunctionLike(reader)) return;
7410
+ if (!isFunctionLike$1(reader)) return;
7395
7411
  const getParameterName = getFirstParameterName(reader);
7396
7412
  if (!getParameterName) return;
7397
7413
  const freshReturn = getFreshReturnForFunction(reader);
@@ -7407,7 +7423,6 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
7407
7423
  //#endregion
7408
7424
  //#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
7409
7425
  const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
7410
- const MEMOIZING_HOOK_NAMES = new Set(["useMemo", "useCallback"]);
7411
7426
  const COMPONENT_NAME_PATTERN = /^[A-Z]/;
7412
7427
  const HOOK_NAME_PATTERN = /^use[A-Z]/;
7413
7428
  const isFunctionLikeNode = (node) => isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
@@ -7645,7 +7660,7 @@ const isInsideLoopContext = (node) => {
7645
7660
  let current = node.parent;
7646
7661
  while (current) {
7647
7662
  if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
7648
- if (isFunctionLike$1(current)) {
7663
+ if (isFunctionLike$2(current)) {
7649
7664
  if (isIteratorCallback(current)) return true;
7650
7665
  return false;
7651
7666
  }
@@ -7999,7 +8014,7 @@ const jsHoistIntl = defineRule({
7999
8014
  let cursor = node.parent ?? null;
8000
8015
  let inFunctionBody = false;
8001
8016
  while (cursor) {
8002
- if (isFunctionLike$1(cursor)) {
8017
+ if (isFunctionLike$2(cursor)) {
8003
8018
  inFunctionBody = true;
8004
8019
  const fnParent = cursor.parent;
8005
8020
  if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
@@ -9344,7 +9359,7 @@ const findVariableInitializer = (referenceNode, bindingName) => {
9344
9359
  };
9345
9360
  //#endregion
9346
9361
  //#region src/plugin/rules/react-builtins/jsx-max-depth.ts
9347
- 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}.`;
9348
9363
  const DEFAULT_MAX_DEPTH = 14;
9349
9364
  const resolveSettings$30 = (settings) => {
9350
9365
  const reactDoctor = settings?.["react-doctor"];
@@ -9411,7 +9426,7 @@ const jsxMaxDepth = defineRule({
9411
9426
  const total = computeJsxAncestorDepth(node) + computeChildrenDepth(node.children ?? [], /* @__PURE__ */ new Set());
9412
9427
  if (total > max) context.report({
9413
9428
  node,
9414
- message: buildMessage$17(total, max)
9429
+ message: buildMessage$19(total, max)
9415
9430
  });
9416
9431
  };
9417
9432
  return {
@@ -9426,7 +9441,7 @@ const jsxMaxDepth = defineRule({
9426
9441
  });
9427
9442
  //#endregion
9428
9443
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
9429
- 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.";
9430
9445
  const LITERAL_TEXT_TAGS = new Set([
9431
9446
  "code",
9432
9447
  "pre",
@@ -9461,11 +9476,20 @@ const jsxNoCommentTextnodes = defineRule({
9461
9476
  if (isInsideLiteralTextTag(node)) return;
9462
9477
  context.report({
9463
9478
  node,
9464
- message: MESSAGE$37
9479
+ message: MESSAGE$39
9465
9480
  });
9466
9481
  } })
9467
9482
  });
9468
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
9469
9493
  //#region src/plugin/utils/is-inside-function-scope.ts
9470
9494
  const FUNCTION_NODE_TYPES$1 = new Set([
9471
9495
  "FunctionDeclaration",
@@ -9483,7 +9507,12 @@ const isInsideFunctionScope = (node) => {
9483
9507
  };
9484
9508
  //#endregion
9485
9509
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
9486
- 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
+ ];
9487
9516
  const isConstructedValue = (expression) => {
9488
9517
  const stripped = stripParenExpression(expression);
9489
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;
@@ -9491,10 +9520,57 @@ const isConstructedValue = (expression) => {
9491
9520
  if (isNodeOfType(stripped, "LogicalExpression")) return isConstructedValue(stripped.left) || isConstructedValue(stripped.right);
9492
9521
  return false;
9493
9522
  };
9494
- const isProviderName = (node) => {
9523
+ const isProviderMemberName = (node) => {
9495
9524
  if (!isNodeOfType(node, "JSXMemberExpression")) return false;
9496
9525
  return node.property.name === "Provider";
9497
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
+ };
9498
9574
  const jsxNoConstructedContextValues = defineRule({
9499
9575
  id: "jsx-no-constructed-context-values",
9500
9576
  tags: ["react-jsx-only"],
@@ -9503,26 +9579,35 @@ const jsxNoConstructedContextValues = defineRule({
9503
9579
  category: "Performance",
9504
9580
  create: (context) => {
9505
9581
  const isTestlikeFile = isTestlikeFilename(context.filename);
9506
- return { JSXOpeningElement(node) {
9507
- if (isTestlikeFile) return;
9508
- if (!isProviderName(node.name)) return;
9509
- if (!isInsideFunctionScope(node)) return;
9510
- for (const attribute of node.attributes) {
9511
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9512
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
9513
- if (attribute.name.name !== "value") continue;
9514
- const attributeValue = attribute.value;
9515
- if (!attributeValue) continue;
9516
- if (!isNodeOfType(attributeValue, "JSXExpressionContainer")) continue;
9517
- const innerExpression = attributeValue.expression;
9518
- if (!innerExpression || innerExpression.type === "JSXEmptyExpression") continue;
9519
- if (!isConstructedValue(innerExpression)) continue;
9520
- context.report({
9521
- node: attribute,
9522
- message: MESSAGE$36
9523
- });
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
+ }
9524
9609
  }
9525
- } };
9610
+ };
9526
9611
  }
9527
9612
  });
9528
9613
  //#endregion
@@ -9548,7 +9633,8 @@ const jsxNoDuplicateProps = defineRule({
9548
9633
  //#endregion
9549
9634
  //#region src/plugin/utils/build-same-file-memo-registry.ts
9550
9635
  const HOC_NAMES_FOR_MEMOISATION = new Set([
9551
- ...REACT_HOC_NAMES,
9636
+ "memo",
9637
+ "React.memo",
9552
9638
  "observer",
9553
9639
  "observable",
9554
9640
  "lazy",
@@ -9602,7 +9688,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
9602
9688
  };
9603
9689
  //#endregion
9604
9690
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
9605
- 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.";
9606
9692
  const KNOWN_SLOT_PROP_NAMES = new Set([
9607
9693
  "icon",
9608
9694
  "Icon",
@@ -9870,7 +9956,7 @@ const jsxNoJsxAsProp = defineRule({
9870
9956
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
9871
9957
  context.report({
9872
9958
  node,
9873
- message: MESSAGE$35
9959
+ message: MESSAGE$37
9874
9960
  });
9875
9961
  }
9876
9962
  };
@@ -10158,7 +10244,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
10158
10244
  ];
10159
10245
  //#endregion
10160
10246
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
10161
- 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.";
10162
10248
  const isDataArrayPropName = (propName) => {
10163
10249
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
10164
10250
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -10241,7 +10327,7 @@ const jsxNoNewArrayAsProp = defineRule({
10241
10327
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
10242
10328
  context.report({
10243
10329
  node,
10244
- message: MESSAGE$34
10330
+ message: MESSAGE$36
10245
10331
  });
10246
10332
  }
10247
10333
  };
@@ -10499,7 +10585,7 @@ const SAFE_RECEIVER_NAMES = new Set([
10499
10585
  ]);
10500
10586
  //#endregion
10501
10587
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
10502
- 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.";
10503
10589
  const isAccessorPredicateName = (propName) => {
10504
10590
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
10505
10591
  if (propName.length <= prefix.length) continue;
@@ -10704,7 +10790,7 @@ const jsxNoNewFunctionAsProp = defineRule({
10704
10790
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
10705
10791
  context.report({
10706
10792
  node,
10707
- message: MESSAGE$33
10793
+ message: MESSAGE$35
10708
10794
  });
10709
10795
  }
10710
10796
  };
@@ -10924,7 +11010,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
10924
11010
  ];
10925
11011
  //#endregion
10926
11012
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
10927
- 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.";
10928
11014
  const isConfigObjectPropName = (propName) => {
10929
11015
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
10930
11016
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11011,7 +11097,7 @@ const jsxNoNewObjectAsProp = defineRule({
11011
11097
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
11012
11098
  context.report({
11013
11099
  node,
11014
- message: MESSAGE$32
11100
+ message: MESSAGE$34
11015
11101
  });
11016
11102
  }
11017
11103
  };
@@ -11019,7 +11105,7 @@ const jsxNoNewObjectAsProp = defineRule({
11019
11105
  });
11020
11106
  //#endregion
11021
11107
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
11022
- 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.";
11023
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;
11024
11110
  const resolveSettings$29 = (settings) => {
11025
11111
  const reactDoctor = settings?.["react-doctor"];
@@ -11059,7 +11145,7 @@ const jsxNoScriptUrl = defineRule({
11059
11145
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
11060
11146
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
11061
11147
  node: attribute,
11062
- message: MESSAGE$31
11148
+ message: MESSAGE$33
11063
11149
  });
11064
11150
  }
11065
11151
  } };
@@ -11347,7 +11433,7 @@ const jsxNoTargetBlank = defineRule({
11347
11433
  });
11348
11434
  //#endregion
11349
11435
  //#region src/plugin/rules/react-builtins/jsx-no-undef.ts
11350
- const buildMessage$16 = (name) => `\`${name}\` is not defined in this scope.`;
11436
+ const buildMessage$18 = (name) => `\`${name}\` is not defined in this scope.`;
11351
11437
  const KNOWN_GLOBALS = new Set([
11352
11438
  "globalThis",
11353
11439
  "window",
@@ -11382,7 +11468,7 @@ const jsxNoUndef = defineRule({
11382
11468
  if (findVariableInitializer(node, rootIdentifier)) return;
11383
11469
  context.report({
11384
11470
  node: node.name,
11385
- message: buildMessage$16(rootIdentifier)
11471
+ message: buildMessage$18(rootIdentifier)
11386
11472
  });
11387
11473
  } })
11388
11474
  });
@@ -11481,7 +11567,7 @@ const jsxNoUselessFragment = defineRule({
11481
11567
  });
11482
11568
  //#endregion
11483
11569
  //#region src/plugin/rules/react-builtins/jsx-pascal-case.ts
11484
- const buildMessage$15 = (componentName, allowAllCaps) => allowAllCaps ? `JSX component \`${componentName}\` must be in PascalCase or SCREAMING_SNAKE_CASE.` : `JSX component \`${componentName}\` must be in PascalCase.`;
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.`;
11485
11571
  const resolveSettings$26 = (settings) => {
11486
11572
  const reactDoctor = settings?.["react-doctor"];
11487
11573
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPascalCase ?? {} : {};
@@ -11597,7 +11683,7 @@ const jsxPascalCase = defineRule({
11597
11683
  if (!isPascal && !isAllCaps) {
11598
11684
  context.report({
11599
11685
  node,
11600
- message: buildMessage$15(segment, settings.allowAllCaps)
11686
+ message: buildMessage$17(segment, settings.allowAllCaps)
11601
11687
  });
11602
11688
  return;
11603
11689
  }
@@ -11649,7 +11735,7 @@ const jsxPropsNoSpreadMulti = defineRule({
11649
11735
  });
11650
11736
  //#endregion
11651
11737
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
11652
- 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.";
11653
11739
  const resolveSettings$25 = (settings) => {
11654
11740
  const reactDoctor = settings?.["react-doctor"];
11655
11741
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -11689,7 +11775,7 @@ const jsxPropsNoSpreading = defineRule({
11689
11775
  }
11690
11776
  context.report({
11691
11777
  node: attribute,
11692
- message: MESSAGE$30
11778
+ message: MESSAGE$32
11693
11779
  });
11694
11780
  }
11695
11781
  } };
@@ -11844,7 +11930,7 @@ const labelHasAssociatedControl = defineRule({
11844
11930
  });
11845
11931
  //#endregion
11846
11932
  //#region src/plugin/rules/a11y/lang.ts
11847
- 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`).";
11848
11934
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
11849
11935
  "aa",
11850
11936
  "ab",
@@ -12055,7 +12141,7 @@ const lang = defineRule({
12055
12141
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
12056
12142
  context.report({
12057
12143
  node: langAttr,
12058
- message: MESSAGE$29
12144
+ message: MESSAGE$31
12059
12145
  });
12060
12146
  return;
12061
12147
  }
@@ -12064,13 +12150,13 @@ const lang = defineRule({
12064
12150
  if (value === null) return;
12065
12151
  if (!isValidLangTag(value)) context.report({
12066
12152
  node: langAttr,
12067
- message: MESSAGE$29
12153
+ message: MESSAGE$31
12068
12154
  });
12069
12155
  } })
12070
12156
  });
12071
12157
  //#endregion
12072
12158
  //#region src/plugin/rules/a11y/media-has-caption.ts
12073
- 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.";
12074
12160
  const DEFAULT_AUDIO = ["audio"];
12075
12161
  const DEFAULT_VIDEO = ["video"];
12076
12162
  const DEFAULT_TRACK = ["track"];
@@ -12110,7 +12196,7 @@ const mediaHasCaption = defineRule({
12110
12196
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
12111
12197
  context.report({
12112
12198
  node: node.name,
12113
- message: MESSAGE$28
12199
+ message: MESSAGE$30
12114
12200
  });
12115
12201
  return;
12116
12202
  }
@@ -12127,7 +12213,7 @@ const mediaHasCaption = defineRule({
12127
12213
  return kindValue.value.toLowerCase() === "captions";
12128
12214
  })) context.report({
12129
12215
  node: node.name,
12130
- message: MESSAGE$28
12216
+ message: MESSAGE$30
12131
12217
  });
12132
12218
  } };
12133
12219
  }
@@ -12818,7 +12904,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
12818
12904
  };
12819
12905
  const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
12820
12906
  if (remainingDepth <= 0) return [];
12821
- if (isFunctionLike$1(expression)) return expression.body ? [expression.body] : [];
12907
+ if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
12822
12908
  if (isNodeOfType(expression, "CallExpression")) {
12823
12909
  for (const callArgument of expression.arguments ?? []) {
12824
12910
  if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
@@ -12941,7 +13027,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
12941
13027
  });
12942
13028
  //#endregion
12943
13029
  //#region src/plugin/rules/a11y/no-access-key.ts
12944
- 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.";
12945
13031
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
12946
13032
  const noAccessKey = defineRule({
12947
13033
  id: "no-access-key",
@@ -12957,7 +13043,7 @@ const noAccessKey = defineRule({
12957
13043
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
12958
13044
  context.report({
12959
13045
  node: accessKey,
12960
- message: MESSAGE$27
13046
+ message: MESSAGE$29
12961
13047
  });
12962
13048
  return;
12963
13049
  }
@@ -12967,7 +13053,7 @@ const noAccessKey = defineRule({
12967
13053
  if (isUndefinedIdentifier(expression)) return;
12968
13054
  context.report({
12969
13055
  node: accessKey,
12970
- message: MESSAGE$27
13056
+ message: MESSAGE$29
12971
13057
  });
12972
13058
  }
12973
13059
  } })
@@ -13276,7 +13362,7 @@ const getEffectFn = (analysis, node) => {
13276
13362
  if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
13277
13363
  if (isNodeOfType(fn, "Identifier")) {
13278
13364
  const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
13279
- if (definitionNode && isFunctionLike$1(definitionNode)) return definitionNode;
13365
+ if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
13280
13366
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13281
13367
  const initializer = definitionNode.init;
13282
13368
  if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
@@ -13369,14 +13455,14 @@ const getUseStateDecl = (analysis, ref) => {
13369
13455
  return node ?? null;
13370
13456
  };
13371
13457
  const isCleanupReturnArgument = (analysis, node) => {
13372
- if (isFunctionLike$1(node)) return true;
13458
+ if (isFunctionLike$2(node)) return true;
13373
13459
  if (isNodeOfType(node, "MemberExpression")) return true;
13374
13460
  if (isNodeOfType(node, "Identifier")) {
13375
13461
  const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
13376
- if (definitionNode && isFunctionLike$1(definitionNode)) return true;
13462
+ if (definitionNode && isFunctionLike$2(definitionNode)) return true;
13377
13463
  if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
13378
13464
  const initializer = definitionNode.init;
13379
- return isFunctionLike$1(initializer);
13465
+ return isFunctionLike$2(initializer);
13380
13466
  }
13381
13467
  }
13382
13468
  if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
@@ -13386,7 +13472,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13386
13472
  if (visited.has(node)) return false;
13387
13473
  visited.add(node);
13388
13474
  if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
13389
- if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
13475
+ if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
13390
13476
  const record = node;
13391
13477
  for (const [key, value] of Object.entries(record)) {
13392
13478
  if (key === "parent") continue;
@@ -13398,7 +13484,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
13398
13484
  };
13399
13485
  const hasCleanup = (analysis, node) => {
13400
13486
  const fn = getEffectFn(analysis, node);
13401
- if (!isFunctionLike$1(fn)) return false;
13487
+ if (!isFunctionLike$2(fn)) return false;
13402
13488
  if (!isNodeOfType(fn.body, "BlockStatement")) return false;
13403
13489
  return hasCleanupReturn(analysis, fn.body);
13404
13490
  };
@@ -13440,7 +13526,7 @@ const noAdjustStateOnPropChange = defineRule({
13440
13526
  });
13441
13527
  //#endregion
13442
13528
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
13443
- 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.";
13444
13530
  const noAriaHiddenOnFocusable = defineRule({
13445
13531
  id: "no-aria-hidden-on-focusable",
13446
13532
  tags: ["react-jsx-only"],
@@ -13466,7 +13552,7 @@ const noAriaHiddenOnFocusable = defineRule({
13466
13552
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
13467
13553
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
13468
13554
  node: ariaHidden,
13469
- message: MESSAGE$26
13555
+ message: MESSAGE$28
13470
13556
  });
13471
13557
  } })
13472
13558
  });
@@ -13746,7 +13832,7 @@ const isInsideStaticPlaceholderMap = (node) => {
13746
13832
  let current = node;
13747
13833
  while (current.parent) {
13748
13834
  const parent = current.parent;
13749
- if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13835
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13750
13836
  const callee = parent.callee;
13751
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);
13752
13838
  if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
@@ -13765,7 +13851,7 @@ const findIteratorItemName$1 = (node) => {
13765
13851
  let current = node;
13766
13852
  while (current.parent) {
13767
13853
  const parent = current.parent;
13768
- if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13854
+ if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
13769
13855
  const callee = parent.callee;
13770
13856
  const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
13771
13857
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
@@ -13833,7 +13919,7 @@ const noArrayIndexAsKey = defineRule({
13833
13919
  });
13834
13920
  //#endregion
13835
13921
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
13836
- 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.";
13837
13923
  const SECOND_INDEX_METHODS = new Set([
13838
13924
  "every",
13839
13925
  "filter",
@@ -14035,7 +14121,7 @@ const noArrayIndexKey = defineRule({
14035
14121
  }
14036
14122
  context.report({
14037
14123
  node: keyAttribute,
14038
- message: MESSAGE$25
14124
+ message: MESSAGE$27
14039
14125
  });
14040
14126
  },
14041
14127
  CallExpression(node) {
@@ -14055,7 +14141,7 @@ const noArrayIndexKey = defineRule({
14055
14141
  if (propName !== "key") continue;
14056
14142
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
14057
14143
  node: property,
14058
- message: MESSAGE$25
14144
+ message: MESSAGE$27
14059
14145
  });
14060
14146
  }
14061
14147
  }
@@ -14063,7 +14149,7 @@ const noArrayIndexKey = defineRule({
14063
14149
  });
14064
14150
  //#endregion
14065
14151
  //#region src/plugin/rules/a11y/no-autofocus.ts
14066
- const MESSAGE$24 = "`autoFocus` should not be used — it disrupts users who expect the page focus to remain at the top of the document on load.";
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.";
14067
14153
  const resolveSettings$21 = (settings) => {
14068
14154
  const reactDoctor = settings?.["react-doctor"];
14069
14155
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -14118,7 +14204,7 @@ const noAutofocus = defineRule({
14118
14204
  }
14119
14205
  context.report({
14120
14206
  node: autoFocusAttribute,
14121
- message: MESSAGE$24
14207
+ message: MESSAGE$26
14122
14208
  });
14123
14209
  } };
14124
14210
  }
@@ -14609,7 +14695,7 @@ const noChainStateUpdates = defineRule({
14609
14695
  });
14610
14696
  //#endregion
14611
14697
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
14612
- const MESSAGE$23 = "Avoid passing children using a `children` prop — nest them between the JSX tags or pass them as additional `React.createElement` arguments instead.";
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.";
14613
14699
  const noChildrenProp = defineRule({
14614
14700
  id: "no-children-prop",
14615
14701
  severity: "warn",
@@ -14620,7 +14706,7 @@ const noChildrenProp = defineRule({
14620
14706
  if (node.name.name !== "children") return;
14621
14707
  context.report({
14622
14708
  node: node.name,
14623
- message: MESSAGE$23
14709
+ message: MESSAGE$25
14624
14710
  });
14625
14711
  },
14626
14712
  CallExpression(node) {
@@ -14633,7 +14719,7 @@ const noChildrenProp = defineRule({
14633
14719
  const propertyKey = property.key;
14634
14720
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
14635
14721
  node: propertyKey,
14636
- message: MESSAGE$23
14722
+ message: MESSAGE$25
14637
14723
  });
14638
14724
  }
14639
14725
  }
@@ -14641,7 +14727,7 @@ const noChildrenProp = defineRule({
14641
14727
  });
14642
14728
  //#endregion
14643
14729
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
14644
- 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.";
14645
14731
  const noCloneElement = defineRule({
14646
14732
  id: "no-clone-element",
14647
14733
  severity: "warn",
@@ -14653,7 +14739,7 @@ const noCloneElement = defineRule({
14653
14739
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
14654
14740
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
14655
14741
  node: callee,
14656
- message: MESSAGE$22
14742
+ message: MESSAGE$24
14657
14743
  });
14658
14744
  return;
14659
14745
  }
@@ -14666,14 +14752,231 @@ const noCloneElement = defineRule({
14666
14752
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
14667
14753
  context.report({
14668
14754
  node: callee,
14669
- message: MESSAGE$22
14755
+ message: MESSAGE$24
14670
14756
  });
14671
14757
  }
14672
14758
  } })
14673
14759
  });
14674
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
14675
14978
  //#region src/plugin/rules/react-builtins/no-danger.ts
14676
- 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.";
14677
14980
  const noDanger = defineRule({
14678
14981
  id: "no-danger",
14679
14982
  severity: "warn",
@@ -14684,7 +14987,7 @@ const noDanger = defineRule({
14684
14987
  if (!propAttribute) return;
14685
14988
  context.report({
14686
14989
  node: propAttribute.name,
14687
- message: MESSAGE$21
14990
+ message: MESSAGE$22
14688
14991
  });
14689
14992
  },
14690
14993
  CallExpression(node) {
@@ -14696,7 +14999,7 @@ const noDanger = defineRule({
14696
14999
  const propertyKey = property.key;
14697
15000
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
14698
15001
  node: propertyKey,
14699
- message: MESSAGE$21
15002
+ message: MESSAGE$22
14700
15003
  });
14701
15004
  }
14702
15005
  }
@@ -14704,7 +15007,7 @@ const noDanger = defineRule({
14704
15007
  });
14705
15008
  //#endregion
14706
15009
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
14707
- 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.";
14708
15011
  const isLineBreak = (child) => {
14709
15012
  if (!isNodeOfType(child, "JSXText")) return false;
14710
15013
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -14773,7 +15076,7 @@ const noDangerWithChildren = defineRule({
14773
15076
  if (!hasChildrenProp && !hasNestedChildren) return;
14774
15077
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
14775
15078
  node: opening,
14776
- message: MESSAGE$20
15079
+ message: MESSAGE$21
14777
15080
  });
14778
15081
  },
14779
15082
  CallExpression(node) {
@@ -14785,7 +15088,7 @@ const noDangerWithChildren = defineRule({
14785
15088
  if (!propsShape.hasDangerously) return;
14786
15089
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
14787
15090
  node,
14788
- message: MESSAGE$20
15091
+ message: MESSAGE$21
14789
15092
  });
14790
15093
  }
14791
15094
  })
@@ -15160,7 +15463,7 @@ const extractDestructuredPropNames = (params) => {
15160
15463
  };
15161
15464
  const getInlineFunctionNode = (node) => {
15162
15465
  if (!node) return null;
15163
- if (isFunctionLike$1(node)) return node;
15466
+ if (isFunctionLike$2(node)) return node;
15164
15467
  if (!isNodeOfType(node, "CallExpression")) return null;
15165
15468
  for (const argument of node.arguments ?? []) {
15166
15469
  const inlineFunctionNode = getInlineFunctionNode(argument);
@@ -15171,7 +15474,7 @@ const getInlineFunctionNode = (node) => {
15171
15474
  const getNearestComponentFunction = (node) => {
15172
15475
  let cursor = node.parent ?? null;
15173
15476
  while (cursor) {
15174
- if (isFunctionLike$1(cursor)) return cursor;
15477
+ if (isFunctionLike$2(cursor)) return cursor;
15175
15478
  cursor = cursor.parent ?? null;
15176
15479
  }
15177
15480
  return null;
@@ -15352,7 +15655,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
15352
15655
  //#endregion
15353
15656
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
15354
15657
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
15355
- const MESSAGE$19 = "Do not use `this.setState` in `componentDidMount`.";
15658
+ const MESSAGE$20 = "Do not use `this.setState` in `componentDidMount`.";
15356
15659
  const resolveSettings$20 = (settings) => {
15357
15660
  const reactDoctor = settings?.["react-doctor"];
15358
15661
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -15370,7 +15673,7 @@ const noDidMountSetState = defineRule({
15370
15673
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15371
15674
  context.report({
15372
15675
  node: node.callee,
15373
- message: MESSAGE$19
15676
+ message: MESSAGE$20
15374
15677
  });
15375
15678
  } };
15376
15679
  }
@@ -15378,7 +15681,7 @@ const noDidMountSetState = defineRule({
15378
15681
  //#endregion
15379
15682
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
15380
15683
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
15381
- 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.";
15382
15685
  const resolveSettings$19 = (settings) => {
15383
15686
  const reactDoctor = settings?.["react-doctor"];
15384
15687
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -15396,7 +15699,7 @@ const noDidUpdateSetState = defineRule({
15396
15699
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
15397
15700
  context.report({
15398
15701
  node: node.callee,
15399
- message: MESSAGE$18
15702
+ message: MESSAGE$19
15400
15703
  });
15401
15704
  } };
15402
15705
  }
@@ -15419,7 +15722,7 @@ const isStateMemberExpression = (node) => {
15419
15722
  };
15420
15723
  //#endregion
15421
15724
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
15422
- const MESSAGE$17 = "Never mutate `this.state` directly.";
15725
+ const MESSAGE$18 = "Never mutate `this.state` directly.";
15423
15726
  const shouldIgnoreMutation = (node) => {
15424
15727
  let isConstructor = false;
15425
15728
  let isInsideCallExpression = false;
@@ -15441,7 +15744,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
15441
15744
  if (shouldIgnoreMutation(reportNode)) return;
15442
15745
  context.report({
15443
15746
  node: reportNode,
15444
- message: MESSAGE$17
15747
+ message: MESSAGE$18
15445
15748
  });
15446
15749
  };
15447
15750
  const noDirectMutationState = defineRule({
@@ -15497,7 +15800,7 @@ const collectFunctionLocalBindings = (functionNode) => {
15497
15800
  const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
15498
15801
  if (!node || typeof node !== "object") return;
15499
15802
  let nextShadowedStateNames = shadowedStateNames;
15500
- if (isFunctionLike$1(node)) {
15803
+ if (isFunctionLike$2(node)) {
15501
15804
  const localBindings = collectFunctionLocalBindings(node);
15502
15805
  if (localBindings.size > 0) {
15503
15806
  const merged = new Set(shadowedStateNames);
@@ -15543,7 +15846,7 @@ const noDirectStateMutation = defineRule({
15543
15846
  if (!isNodeOfType(callee, "MemberExpression")) return;
15544
15847
  if (!isNodeOfType(callee.property, "Identifier")) return;
15545
15848
  const methodName = callee.property.name;
15546
- if (!MUTATING_ARRAY_METHODS$1.has(methodName)) return;
15849
+ if (!MUTATING_ARRAY_METHODS.has(methodName)) return;
15547
15850
  const rootName = getRootIdentifierName(callee.object);
15548
15851
  if (!rootName || !stateValueToSetter.has(rootName)) return;
15549
15852
  if (currentlyShadowed.has(rootName)) return;
@@ -15604,7 +15907,7 @@ const noDisabledZoom = defineRule({
15604
15907
  });
15605
15908
  //#endregion
15606
15909
  //#region src/plugin/rules/a11y/no-distracting-elements.ts
15607
- 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.`;
15608
15911
  const DEFAULT_DISTRACTING = ["marquee", "blink"];
15609
15912
  const resolveSettings$18 = (settings) => {
15610
15913
  const reactDoctor = settings?.["react-doctor"];
@@ -15624,7 +15927,7 @@ const noDistractingElements = defineRule({
15624
15927
  const tag = getElementType(node, context.settings);
15625
15928
  if (distractingTags.has(tag)) context.report({
15626
15929
  node: node.name,
15627
- message: buildMessage$14(tag)
15930
+ message: buildMessage$16(tag)
15628
15931
  });
15629
15932
  } };
15630
15933
  }
@@ -16064,6 +16367,69 @@ const noEffectEventInDeps = defineRule({
16064
16367
  }
16065
16368
  });
16066
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
16067
16433
  //#region src/plugin/rules/security/no-eval.ts
16068
16434
  const noEval = defineRule({
16069
16435
  id: "no-eval",
@@ -16954,7 +17320,7 @@ const ALLOWED_NAMESPACES = new Set([
16954
17320
  "ReactDOM",
16955
17321
  "ReactDom"
16956
17322
  ]);
16957
- const MESSAGE$16 = "Unexpected call to `findDOMNode` — removed in React 19.";
17323
+ const MESSAGE$17 = "Unexpected call to `findDOMNode` — removed in React 19.";
16958
17324
  const noFindDomNode = defineRule({
16959
17325
  id: "no-find-dom-node",
16960
17326
  severity: "warn",
@@ -16964,7 +17330,7 @@ const noFindDomNode = defineRule({
16964
17330
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
16965
17331
  context.report({
16966
17332
  node: callee,
16967
- message: MESSAGE$16
17333
+ message: MESSAGE$17
16968
17334
  });
16969
17335
  return;
16970
17336
  }
@@ -16975,7 +17341,7 @@ const noFindDomNode = defineRule({
16975
17341
  if (callee.property.name !== "findDOMNode") return;
16976
17342
  context.report({
16977
17343
  node: callee.property,
16978
- message: MESSAGE$16
17344
+ message: MESSAGE$17
16979
17345
  });
16980
17346
  }
16981
17347
  } })
@@ -17465,7 +17831,7 @@ const noInlinePropOnMemoComponent = defineRule({
17465
17831
  });
17466
17832
  //#endregion
17467
17833
  //#region src/plugin/rules/a11y/no-interactive-element-to-noninteractive-role.ts
17468
- 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}\`.`;
17469
17835
  const PRESENTATION_ROLES = ["presentation", "none"];
17470
17836
  const DEFAULT_ALLOWED_ROLES$1 = {
17471
17837
  tr: ["none", "presentation"],
@@ -17509,7 +17875,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
17509
17875
  if (!isNonInteractiveRole(firstRole) && !PRESENTATION_ROLES.includes(firstRole)) return;
17510
17876
  context.report({
17511
17877
  node: roleAttribute,
17512
- message: buildMessage$13(elementType, firstRole)
17878
+ message: buildMessage$15(elementType, firstRole)
17513
17879
  });
17514
17880
  } };
17515
17881
  }
@@ -17710,7 +18076,7 @@ const isInsideClassBody = (node) => {
17710
18076
  let current = node.parent;
17711
18077
  while (current) {
17712
18078
  if (isNodeOfType(current, "ClassBody")) return true;
17713
- if (isFunctionLike$1(current)) return false;
18079
+ if (isFunctionLike$2(current)) return false;
17714
18080
  current = current.parent;
17715
18081
  }
17716
18082
  return false;
@@ -17953,7 +18319,7 @@ const noMoment = defineRule({
17953
18319
  });
17954
18320
  //#endregion
17955
18321
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
17956
- 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}.`;
17957
18323
  const resolveSettings$16 = (settings) => {
17958
18324
  const reactDoctor = settings?.["react-doctor"];
17959
18325
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -18274,7 +18640,7 @@ const noMultiComp = defineRule({
18274
18640
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
18275
18641
  for (const component of flagged.slice(1)) context.report({
18276
18642
  node: component.reportNode,
18277
- message: buildMessage$12(component.name)
18643
+ message: buildMessage$14(component.name)
18278
18644
  });
18279
18645
  } };
18280
18646
  }
@@ -18351,25 +18717,315 @@ const noMutableInDeps = defineRule({
18351
18717
  }
18352
18718
  });
18353
18719
  //#endregion
18354
- //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
18355
- const MESSAGE$15 = "Reducer mutates its current state and returns the same reference. Return a copied object or array so React can observe the update.";
18356
- const MUTATING_ARRAY_METHODS = new Set([
18357
- "copyWithin",
18358
- "fill",
18359
- "pop",
18360
- "push",
18361
- "reverse",
18362
- "shift",
18363
- "sort",
18364
- "splice",
18365
- "unshift"
18366
- ]);
18367
- const MUTATING_COLLECTION_METHODS = new Set([
18368
- "add",
18369
- "clear",
18370
- "delete",
18371
- "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"
18372
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.";
18373
19029
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
18374
19030
  "copyWithin",
18375
19031
  "fill",
@@ -18388,7 +19044,6 @@ const cloneReducerPathState = (state) => ({
18388
19044
  mutableStateSourceNames: new Set(state.mutableStateSourceNames),
18389
19045
  mutations: [...state.mutations]
18390
19046
  });
18391
- const isFunctionLikeAstNode = (node) => Boolean(node && (isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression")));
18392
19047
  const isSpecifierImportedFromReact = (node) => {
18393
19048
  const parent = node.parent ?? null;
18394
19049
  return parent !== null && isNodeOfType(parent, "ImportDeclaration") && parent.source.value === "react";
@@ -18415,19 +19070,16 @@ const isCallToImportedReactUseReducer = (node) => {
18415
19070
  const binding = findVariableInitializer(callee.object, callee.object.name);
18416
19071
  return Boolean(binding?.initializer && isReactNamespaceOrDefaultImportSpecifier(binding.initializer));
18417
19072
  };
18418
- const resolveSameFileReducerFunction = (node) => {
18419
- if (!node) return null;
18420
- const unwrappedNode = stripParenExpression(node);
18421
- if (isFunctionLikeAstNode(unwrappedNode)) return unwrappedNode;
18422
- if (!isNodeOfType(unwrappedNode, "Identifier")) return null;
18423
- const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
18424
- if (!initializer) return null;
18425
- const unwrappedInitializer = stripParenExpression(initializer);
18426
- return isFunctionLikeAstNode(unwrappedInitializer) ? unwrappedInitializer : null;
18427
- };
18428
19073
  const isStaticMethodCallOnNamedObject = (node, objectName, methodNames) => {
18429
19074
  const unwrappedNode = stripParenExpression(node);
18430
- 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;
18431
19083
  };
18432
19084
  const isExpressionRootedInMutableReducerStateSource = (node, state) => {
18433
19085
  let current = stripParenExpression(node);
@@ -18463,11 +19115,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
18463
19115
  return false;
18464
19116
  };
18465
19117
  const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
18466
- if (isFunctionLikeAstNode(node)) return [];
19118
+ if (isFunctionLike$2(node)) return [];
18467
19119
  const mutations = [];
18468
19120
  walkAst(node, (child) => {
18469
19121
  const unwrappedChild = stripParenExpression(child);
18470
- if (child !== node && isFunctionLikeAstNode(unwrappedChild)) return false;
19122
+ if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
18471
19123
  if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
18472
19124
  if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
18473
19125
  return;
@@ -18486,6 +19138,10 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
18486
19138
  mutations.push({ node: unwrappedChild });
18487
19139
  return;
18488
19140
  }
19141
+ if (firstArgument && isExpressionRootedInMutableReducerStateSource(firstArgument, state) && isLodashMutatorCall(unwrappedChild)) {
19142
+ mutations.push({ node: unwrappedChild });
19143
+ return;
19144
+ }
18489
19145
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
18490
19146
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
18491
19147
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
@@ -18512,18 +19168,47 @@ const restoreOuterIdentityForBlockScopedNames = (blockState, outerState, blockSc
18512
19168
  }
18513
19169
  return nextState;
18514
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
+ };
18515
19197
  const updateReducerStateIdentityForVariableDeclaration = (declaration, state) => {
18516
19198
  for (const declarator of declaration.declarations ?? []) {
18517
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
18518
- const name = declarator.id.name;
18519
- state.originalStateReferenceNames.delete(name);
18520
- state.mutableStateSourceNames.delete(name);
18521
- if (isExpressionOriginalReducerStateReference(declarator.init, state)) {
18522
- state.originalStateReferenceNames.add(name);
18523
- 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);
18524
19209
  continue;
18525
19210
  }
18526
- 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);
18527
19212
  }
18528
19213
  };
18529
19214
  const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) => {
@@ -18538,24 +19223,40 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
18538
19223
  }
18539
19224
  if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
18540
19225
  };
18541
- const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes) => {
18542
- 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;
18543
19228
  const firstParam = functionNode.params?.[0];
18544
19229
  const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
18545
19230
  if (!stateName) return;
18546
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
+ }
18547
19242
  for (const mutation of mutations) {
18548
19243
  if (reportedNodes.has(mutation.node)) continue;
18549
19244
  reportedNodes.add(mutation.node);
18550
19245
  context.report({
18551
19246
  node: mutation.node,
18552
- message: MESSAGE$15
19247
+ message: MESSAGE$16
18553
19248
  });
18554
19249
  }
18555
19250
  };
19251
+ let pathBudgetExceeded = false;
18556
19252
  const analyzeReducerStatementListByPath = (statements, initialState) => {
19253
+ if (pathBudgetExceeded) return [cloneReducerPathState(initialState)];
18557
19254
  let activeStates = [cloneReducerPathState(initialState)];
18558
19255
  for (const statement of statements) {
19256
+ if (activeStates.length > 1e3) {
19257
+ pathBudgetExceeded = true;
19258
+ break;
19259
+ }
18559
19260
  const nextStates = [];
18560
19261
  for (const activeState of activeStates) {
18561
19262
  if (isNodeOfType(statement, "ReturnStatement")) {
@@ -18624,18 +19325,23 @@ const noMutatingReducerState = defineRule({
18624
19325
  create: (context) => {
18625
19326
  const analyzedReducers = /* @__PURE__ */ new WeakSet();
18626
19327
  const reportedNodes = /* @__PURE__ */ new WeakSet();
19328
+ const currentFilename = context.filename;
18627
19329
  return { CallExpression(node) {
18628
19330
  if (!isCallToImportedReactUseReducer(node)) return;
18629
- const reducerFunction = resolveSameFileReducerFunction(node.arguments?.[0]);
18630
- if (!reducerFunction || analyzedReducers.has(reducerFunction)) return;
18631
- analyzedReducers.add(reducerFunction);
18632
- 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
+ });
18633
19339
  } };
18634
19340
  }
18635
19341
  });
18636
19342
  //#endregion
18637
19343
  //#region src/plugin/rules/react-builtins/no-namespace.ts
18638
- 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.`;
18639
19345
  const noNamespace = defineRule({
18640
19346
  id: "no-namespace",
18641
19347
  severity: "warn",
@@ -18647,7 +19353,7 @@ const noNamespace = defineRule({
18647
19353
  const fullName = `${namespaced.namespace.name}:${namespaced.name.name}`;
18648
19354
  context.report({
18649
19355
  node: namespaced,
18650
- message: buildMessage$11(fullName)
19356
+ message: buildMessage$13(fullName)
18651
19357
  });
18652
19358
  },
18653
19359
  CallExpression(node) {
@@ -18658,7 +19364,7 @@ const noNamespace = defineRule({
18658
19364
  if (!firstArgument.value.includes(":")) return;
18659
19365
  context.report({
18660
19366
  node: firstArgument,
18661
- message: buildMessage$11(firstArgument.value)
19367
+ message: buildMessage$13(firstArgument.value)
18662
19368
  });
18663
19369
  }
18664
19370
  })
@@ -18702,7 +19408,7 @@ const noNestedComponentDefinition = defineRule({
18702
19408
  });
18703
19409
  //#endregion
18704
19410
  //#region src/plugin/rules/a11y/no-noninteractive-element-interactions.ts
18705
- const buildMessage$10 = (tag) => `Non-interactive element \`<${tag}>\` should not have interactive event handlers — convert to a semantic interactive element or add an interactive role.`;
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.`;
18706
19412
  const INTERACTIVE_HANDLERS = [
18707
19413
  "onClick",
18708
19414
  "onMouseDown",
@@ -18728,13 +19434,13 @@ const noNoninteractiveElementInteractions = defineRule({
18728
19434
  }
18729
19435
  context.report({
18730
19436
  node: node.name,
18731
- message: buildMessage$10(tag)
19437
+ message: buildMessage$12(tag)
18732
19438
  });
18733
19439
  } })
18734
19440
  });
18735
19441
  //#endregion
18736
19442
  //#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
18737
- 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.`;
18738
19444
  const DEFAULT_ALLOWED_ROLES = {
18739
19445
  ul: [
18740
19446
  "menu",
@@ -18798,14 +19504,14 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
18798
19504
  if (!isInteractiveRole(firstRole)) return;
18799
19505
  context.report({
18800
19506
  node: roleAttribute,
18801
- message: buildMessage$9(elementType, firstRole)
19507
+ message: buildMessage$11(elementType, firstRole)
18802
19508
  });
18803
19509
  } };
18804
19510
  }
18805
19511
  });
18806
19512
  //#endregion
18807
19513
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
18808
- 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.";
18809
19515
  const resolveSettings$14 = (settings) => {
18810
19516
  const reactDoctor = settings?.["react-doctor"];
18811
19517
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -18832,7 +19538,7 @@ const noNoninteractiveTabindex = defineRule({
18832
19538
  if (numeric === null) {
18833
19539
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
18834
19540
  node: tabIndex,
18835
- message: MESSAGE$14
19541
+ message: MESSAGE$15
18836
19542
  });
18837
19543
  return;
18838
19544
  }
@@ -18845,7 +19551,7 @@ const noNoninteractiveTabindex = defineRule({
18845
19551
  if (!roleAttribute) {
18846
19552
  context.report({
18847
19553
  node: tabIndex,
18848
- message: MESSAGE$14
19554
+ message: MESSAGE$15
18849
19555
  });
18850
19556
  return;
18851
19557
  }
@@ -18859,7 +19565,7 @@ const noNoninteractiveTabindex = defineRule({
18859
19565
  }
18860
19566
  context.report({
18861
19567
  node: tabIndex,
18862
- message: MESSAGE$14
19568
+ message: MESSAGE$15
18863
19569
  });
18864
19570
  } };
18865
19571
  }
@@ -19388,6 +20094,63 @@ const noPropCallbackInEffect = defineRule({
19388
20094
  }
19389
20095
  });
19390
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
19391
20154
  //#region src/plugin/rules/design/no-pure-black-background.ts
19392
20155
  const noPureBlackBackground = defineRule({
19393
20156
  id: "no-pure-black-background",
@@ -19419,8 +20182,94 @@ const noPureBlackBackground = defineRule({
19419
20182
  })
19420
20183
  });
19421
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
19422
20271
  //#region src/plugin/rules/react-builtins/no-react-children.ts
19423
- 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.";
19424
20273
  const isChildrenIdentifier = (node, contextNode) => {
19425
20274
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
19426
20275
  return isImportedFromModule(contextNode, "Children", "react");
@@ -19445,13 +20294,13 @@ const noReactChildren = defineRule({
19445
20294
  if (isChildrenIdentifier(memberObject, node)) {
19446
20295
  context.report({
19447
20296
  node: calleeOuter,
19448
- message: MESSAGE$13
20297
+ message: MESSAGE$14
19449
20298
  });
19450
20299
  return;
19451
20300
  }
19452
20301
  if (isReactNamespaceMember(memberObject, node)) context.report({
19453
20302
  node: calleeOuter,
19454
- message: MESSAGE$13
20303
+ message: MESSAGE$14
19455
20304
  });
19456
20305
  } })
19457
20306
  });
@@ -19647,7 +20496,7 @@ const getTagsForRole = (role) => {
19647
20496
  };
19648
20497
  //#endregion
19649
20498
  //#region src/plugin/rules/a11y/no-redundant-roles.ts
19650
- 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.`;
19651
20500
  const resolveSettings$13 = (settings) => {
19652
20501
  const reactDoctor = settings?.["react-doctor"];
19653
20502
  return { exceptions: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noRedundantRoles ?? {} : {}).exceptions ?? {} };
@@ -19670,14 +20519,14 @@ const noRedundantRoles = defineRule({
19670
20519
  const allowedHere = settings.exceptions[tag] ?? [];
19671
20520
  if (implicitRoles.includes(role) && !allowedHere.includes(role)) context.report({
19672
20521
  node: roleAttr,
19673
- message: buildMessage$8(tag, role)
20522
+ message: buildMessage$9(tag, role)
19674
20523
  });
19675
20524
  } };
19676
20525
  }
19677
20526
  });
19678
20527
  //#endregion
19679
20528
  //#region src/plugin/rules/react-builtins/no-redundant-should-component-update.ts
19680
- 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\`.`;
19681
20530
  const isPureComponentSuper = (superClass) => {
19682
20531
  if (!superClass) return false;
19683
20532
  if (isNodeOfType(superClass, "Identifier")) return superClass.name === "PureComponent";
@@ -19709,7 +20558,7 @@ const noRedundantShouldComponentUpdate = defineRule({
19709
20558
  const className = classNode.id?.name ?? "<anonymous class>";
19710
20559
  context.report({
19711
20560
  node: reportNode,
19712
- message: buildMessage$7(className)
20561
+ message: buildMessage$8(className)
19713
20562
  });
19714
20563
  };
19715
20564
  return {
@@ -19768,7 +20617,7 @@ const noRenderPropChildren = defineRule({
19768
20617
  });
19769
20618
  //#endregion
19770
20619
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
19771
- 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`.";
19772
20621
  const isReactDomRenderCall = (node) => {
19773
20622
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
19774
20623
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -19791,7 +20640,7 @@ const noRenderReturnValue = defineRule({
19791
20640
  if (!isUsedAsReturnValue(node.parent)) return;
19792
20641
  context.report({
19793
20642
  node: node.callee,
19794
- message: MESSAGE$12
20643
+ message: MESSAGE$13
19795
20644
  });
19796
20645
  } })
19797
20646
  });
@@ -20186,7 +21035,7 @@ const isTanStackServerFnHandler = (node) => {
20186
21035
  const isInsideServerOnlyScope = (node) => {
20187
21036
  let currentNode = node.parent ?? null;
20188
21037
  while (currentNode) {
20189
- if (isFunctionLike$1(currentNode)) {
21038
+ if (isFunctionLike$2(currentNode)) {
20190
21039
  if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
20191
21040
  }
20192
21041
  currentNode = currentNode.parent ?? null;
@@ -20240,6 +21089,363 @@ const noSecretsInClientCode = defineRule({
20240
21089
  }
20241
21090
  });
20242
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
20243
21449
  //#region src/plugin/utils/get-parent-component.ts
20244
21450
  const getParentComponent = (node) => {
20245
21451
  let ancestor = node.parent;
@@ -20251,7 +21457,7 @@ const getParentComponent = (node) => {
20251
21457
  };
20252
21458
  //#endregion
20253
21459
  //#region src/plugin/rules/react-builtins/no-set-state.ts
20254
- const MESSAGE$11 = "Do not use `this.setState` in components.";
21460
+ const MESSAGE$12 = "Do not use `this.setState` in components.";
20255
21461
  const noSetState = defineRule({
20256
21462
  id: "no-set-state",
20257
21463
  severity: "warn",
@@ -20265,7 +21471,7 @@ const noSetState = defineRule({
20265
21471
  if (!getParentComponent(node)) return;
20266
21472
  context.report({
20267
21473
  node: node.callee,
20268
- message: MESSAGE$11
21474
+ message: MESSAGE$12
20269
21475
  });
20270
21476
  } })
20271
21477
  });
@@ -20424,7 +21630,7 @@ const isAbstractRole = (openingElement, settings) => {
20424
21630
  };
20425
21631
  //#endregion
20426
21632
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
20427
- 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.";
20428
21634
  const DEFAULT_HANDLERS = [
20429
21635
  "onClick",
20430
21636
  "onMouseDown",
@@ -20483,7 +21689,7 @@ const noStaticElementInteractions = defineRule({
20483
21689
  if (!roleAttribute || !roleAttribute.value) {
20484
21690
  context.report({
20485
21691
  node: node.name,
20486
- message: MESSAGE$10
21692
+ message: MESSAGE$11
20487
21693
  });
20488
21694
  return;
20489
21695
  }
@@ -20493,14 +21699,14 @@ const noStaticElementInteractions = defineRule({
20493
21699
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
20494
21700
  context.report({
20495
21701
  node: node.name,
20496
- message: MESSAGE$10
21702
+ message: MESSAGE$11
20497
21703
  });
20498
21704
  return;
20499
21705
  }
20500
21706
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
20501
21707
  context.report({
20502
21708
  node: node.name,
20503
- message: MESSAGE$10
21709
+ message: MESSAGE$11
20504
21710
  });
20505
21711
  } };
20506
21712
  }
@@ -20556,7 +21762,7 @@ const noStringRefs = defineRule({
20556
21762
  });
20557
21763
  //#endregion
20558
21764
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
20559
- 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.";
20560
21766
  const isInsideClassMethod = (node, customClassFactoryNames) => {
20561
21767
  let ancestor = node.parent;
20562
21768
  while (ancestor) {
@@ -20624,7 +21830,7 @@ const noThisInSfc = defineRule({
20624
21830
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
20625
21831
  context.report({
20626
21832
  node,
20627
- message: MESSAGE$9
21833
+ message: MESSAGE$10
20628
21834
  });
20629
21835
  } };
20630
21836
  }
@@ -20806,7 +22012,7 @@ const ESCAPED_VERSIONS = {
20806
22012
  ">": "`&gt;` / `&#62;`",
20807
22013
  "}": "`&#125;` (or wrap the literal in `{'}'}`)"
20808
22014
  };
20809
- 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]}.`;
20810
22016
  const noUnescapedEntities = defineRule({
20811
22017
  id: "no-unescaped-entities",
20812
22018
  severity: "warn",
@@ -20817,7 +22023,7 @@ const noUnescapedEntities = defineRule({
20817
22023
  for (const character of value) if (character in ESCAPED_VERSIONS) {
20818
22024
  context.report({
20819
22025
  node,
20820
- message: buildMessage$6(character)
22026
+ message: buildMessage$7(character)
20821
22027
  });
20822
22028
  return;
20823
22029
  }
@@ -21838,7 +23044,7 @@ const SAFER_REPLACEMENT = {
21838
23044
  componentWillUpdate: "componentDidUpdate",
21839
23045
  UNSAFE_componentWillUpdate: "componentDidUpdate"
21840
23046
  };
21841
- 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.`;
21842
23048
  const resolveSettings$9 = (settings) => {
21843
23049
  const reactDoctor = settings?.["react-doctor"];
21844
23050
  return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
@@ -21886,7 +23092,7 @@ const noUnsafe = defineRule({
21886
23092
  if (!getParentComponent(node)) return;
21887
23093
  context.report({
21888
23094
  node: node.key,
21889
- message: buildMessage$5(name)
23095
+ message: buildMessage$6(name)
21890
23096
  });
21891
23097
  },
21892
23098
  Property(node) {
@@ -21897,7 +23103,7 @@ const noUnsafe = defineRule({
21897
23103
  if (isEs5Component(ancestor)) {
21898
23104
  context.report({
21899
23105
  node: node.key,
21900
- message: buildMessage$5(name)
23106
+ message: buildMessage$6(name)
21901
23107
  });
21902
23108
  return;
21903
23109
  }
@@ -21909,7 +23115,7 @@ const noUnsafe = defineRule({
21909
23115
  });
21910
23116
  //#endregion
21911
23117
  //#region src/plugin/rules/react-builtins/no-unstable-nested-components.ts
21912
- const buildMessage$4 = (parentName, isInProp, allowAsProps) => {
23118
+ const buildMessage$5 = (parentName, isInProp, allowAsProps) => {
21913
23119
  let message = "Don't define components inside another component";
21914
23120
  if (parentName) message += ` (\`${parentName}\`)`;
21915
23121
  message += " — extract it to module scope.";
@@ -21978,7 +23184,7 @@ const isReactClassComponent = (classNode) => {
21978
23184
  const findEnclosingComponent = (node) => {
21979
23185
  let walker = node.parent;
21980
23186
  while (walker) {
21981
- if (isFunctionLike$1(walker)) {
23187
+ if (isFunctionLike$2(walker)) {
21982
23188
  const componentName = inferFunctionLikeName(walker);
21983
23189
  if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
21984
23190
  component: walker,
@@ -22144,7 +23350,7 @@ const noUnstableNestedComponents = defineRule({
22144
23350
  if (!enclosing) return;
22145
23351
  context.report({
22146
23352
  node: reportNode,
22147
- message: buildMessage$4(enclosing.name, propInfo !== null, settings.allowAsProps)
23353
+ message: buildMessage$5(enclosing.name, propInfo !== null, settings.allowAsProps)
22148
23354
  });
22149
23355
  };
22150
23356
  const checkFunctionLike = (node) => {
@@ -22179,15 +23385,6 @@ const noUnstableNestedComponents = defineRule({
22179
23385
  }
22180
23386
  });
22181
23387
  //#endregion
22182
- //#region src/plugin/utils/is-canonical-react-namespace-name.ts
22183
- const isCanonicalReactNamespaceName = (namespaceName) => {
22184
- if (namespaceName === "React") return true;
22185
- if (namespaceName === "react") return true;
22186
- if (namespaceName.startsWith("_react")) return true;
22187
- if (namespaceName.startsWith("_React")) return true;
22188
- return false;
22189
- };
22190
- //#endregion
22191
23388
  //#region src/plugin/rules/performance/no-usememo-simple-expression.ts
22192
23389
  const isSimpleExpression = (node) => {
22193
23390
  if (!node) return false;
@@ -22276,7 +23473,7 @@ const noWideLetterSpacing = defineRule({
22276
23473
  //#endregion
22277
23474
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
22278
23475
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
22279
- 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.";
22280
23477
  const resolveSettings$7 = (settings) => {
22281
23478
  const reactDoctor = settings?.["react-doctor"];
22282
23479
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -22309,7 +23506,7 @@ const noWillUpdateSetState = defineRule({
22309
23506
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
22310
23507
  context.report({
22311
23508
  node: node.callee,
22312
- message: MESSAGE$8
23509
+ message: MESSAGE$9
22313
23510
  });
22314
23511
  } };
22315
23512
  }
@@ -22922,7 +24119,7 @@ const REACT_HOOK_NAMES = new Set([
22922
24119
  "useSyncExternalStore",
22923
24120
  "useTransition"
22924
24121
  ]);
22925
- const buildMessage$3 = (importedNames) => `Import ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`preact/hooks\` (or \`preact/compat\`) — importing hooks from \`react\` in a pure-Preact project loads a second copy of Preact's hook state and triggers \`__H\` undefined errors.`;
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.`;
22926
24123
  const preactNoReactHooksImport = defineRule({
22927
24124
  id: "preact-no-react-hooks-import",
22928
24125
  requires: ["pure-preact"],
@@ -22945,7 +24142,7 @@ const preactNoReactHooksImport = defineRule({
22945
24142
  });
22946
24143
  context.report({
22947
24144
  node,
22948
- message: buildMessage$3(importedNames)
24145
+ message: buildMessage$4(importedNames)
22949
24146
  });
22950
24147
  } })
22951
24148
  });
@@ -23002,7 +24199,7 @@ const preactNoRenderArguments = defineRule({
23002
24199
  });
23003
24200
  //#endregion
23004
24201
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
23005
- const MESSAGE$7 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
24202
+ const MESSAGE$8 = "Preact follows DOM event naming — use `onDblClick` (lowercase second word). React's `onDoubleClick` handler never fires in Preact core.";
23006
24203
  const preactPreferOndblclick = defineRule({
23007
24204
  id: "preact-prefer-ondblclick",
23008
24205
  requires: ["pure-preact"],
@@ -23016,7 +24213,7 @@ const preactPreferOndblclick = defineRule({
23016
24213
  if (!onDoubleClickAttribute) return;
23017
24214
  context.report({
23018
24215
  node: onDoubleClickAttribute,
23019
- message: MESSAGE$7
24216
+ message: MESSAGE$8
23020
24217
  });
23021
24218
  } })
23022
24219
  });
@@ -23124,7 +24321,7 @@ const preferEs6Class = defineRule({
23124
24321
  });
23125
24322
  //#endregion
23126
24323
  //#region src/plugin/rules/react-builtins/prefer-function-component.ts
23127
- 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.";
23128
24325
  const resolveSettings$4 = (settings) => {
23129
24326
  const reactDoctor = settings?.["react-doctor"];
23130
24327
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
@@ -23162,7 +24359,7 @@ const preferFunctionComponent = defineRule({
23162
24359
  const reportNode = node.id ?? node;
23163
24360
  context.report({
23164
24361
  node: reportNode,
23165
- message: MESSAGE$6
24362
+ message: MESSAGE$7
23166
24363
  });
23167
24364
  };
23168
24365
  return {
@@ -23219,6 +24416,276 @@ const preferHtmlDialog = defineRule({
23219
24416
  } })
23220
24417
  });
23221
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
23222
24689
  //#region src/plugin/rules/a11y/prefer-tag-over-role.ts
23223
24690
  const buildMessage$2 = (role, tag) => `Prefer the semantic \`<${tag}>\` element over \`role="${role}"\` on a generic tag.`;
23224
24691
  const preferTagOverRole = defineRule({
@@ -23769,91 +25236,216 @@ const reactCompilerNoManualMemoization = defineRule({
23769
25236
  } })
23770
25237
  });
23771
25238
  //#endregion
23772
- //#region src/plugin/utils/has-binding-named.ts
23773
- const hasBindingNamed = (root, bindingName) => {
23774
- const collected = /* @__PURE__ */ new Set();
23775
- const visit = (node) => {
23776
- switch (node.type) {
23777
- case "VariableDeclarator":
23778
- if ("id" in node && node.id) collectPatternNames(node.id, collected);
23779
- break;
23780
- case "FunctionDeclaration":
23781
- case "FunctionExpression":
23782
- case "ClassDeclaration":
23783
- case "ClassExpression":
23784
- if ("id" in node && node.id && node.id.type === "Identifier") {
23785
- const idNode = node.id;
23786
- if (typeof idNode.name === "string") collected.add(idNode.name);
23787
- }
23788
- break;
23789
- case "ArrowFunctionExpression": break;
23790
- case "ImportDefaultSpecifier":
23791
- case "ImportNamespaceSpecifier":
23792
- case "ImportSpecifier":
23793
- if ("local" in node && node.local && node.local.type === "Identifier") {
23794
- const local = node.local;
23795
- if (typeof local.name === "string") collected.add(local.name);
23796
- }
23797
- break;
23798
- case "TSImportEqualsDeclaration":
23799
- case "TSEnumDeclaration":
23800
- case "TSTypeAliasDeclaration":
23801
- case "TSInterfaceDeclaration":
23802
- case "TSModuleDeclaration": {
23803
- const idNode = node.id;
23804
- if (idNode && idNode.type === "Identifier") {
23805
- const idObject = idNode;
23806
- if (typeof idObject.name === "string") collected.add(idObject.name);
23807
- }
23808
- break;
23809
- }
23810
- default: break;
23811
- }
23812
- if ("params" in node && Array.isArray(node.params)) for (const param of node.params) collectPatternNames(param, collected);
23813
- if (collected.has(bindingName)) return;
23814
- const nodeRecord = node;
23815
- for (const key of Object.keys(nodeRecord)) {
23816
- if (key === "parent") continue;
23817
- const child = nodeRecord[key];
23818
- if (Array.isArray(child)) {
23819
- for (const item of child) if (isAstNode(item)) visit(item);
23820
- } else if (isAstNode(child)) visit(child);
23821
- if (collected.has(bindingName)) return;
23822
- }
23823
- };
23824
- visit(root);
23825
- return collected.has(bindingName);
23826
- };
23827
- //#endregion
23828
25239
  //#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
23829
- 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')`).";
23830
25241
  const reactInJsxScope = defineRule({
23831
25242
  id: "react-in-jsx-scope",
23832
25243
  severity: "warn",
23833
25244
  defaultEnabled: false,
23834
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.",
23835
- create: (context) => {
23836
- let didCheckBindingForFile = false;
23837
- let isReactBound = false;
23838
- const ensureBindingChecked = (jsxNode) => {
23839
- if (didCheckBindingForFile) return isReactBound;
23840
- didCheckBindingForFile = true;
23841
- const programRoot = findProgramRoot(jsxNode);
23842
- isReactBound = programRoot ? hasBindingNamed(programRoot, "React") : false;
23843
- 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
23844
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();
23845
25380
  return {
23846
- JSXOpeningElement(node) {
23847
- if (ensureBindingChecked(node)) return;
23848
- context.report({
23849
- node: node.name,
23850
- 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
+ }
23851
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);
23852
25442
  },
23853
- JSXFragment(node) {
23854
- if (ensureBindingChecked(node)) return;
25443
+ CallExpression(node) {
25444
+ const selectorArgument = inlineUseSelectorFunction(node, aliases);
25445
+ if (!selectorArgument) return;
25446
+ if (!isConciseBodyReturningCollection(selectorArgument)) return;
23855
25447
  context.report({
23856
- node: node.openingFragment,
25448
+ node,
23857
25449
  message: MESSAGE$5
23858
25450
  });
23859
25451
  }
@@ -24131,7 +25723,7 @@ const hasOwnAwait = (functionBody) => {
24131
25723
  let found = false;
24132
25724
  walkAst(functionBody, (child) => {
24133
25725
  if (found) return;
24134
- if (child !== functionBody && isFunctionLike$1(child)) return false;
25726
+ if (child !== functionBody && isFunctionLike$2(child)) return false;
24135
25727
  if (isNodeOfType(child, "AwaitExpression")) found = true;
24136
25728
  });
24137
25729
  return found;
@@ -24150,7 +25742,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
24150
25742
  let found = false;
24151
25743
  walkAst(componentBody, (child) => {
24152
25744
  if (found) return;
24153
- if (!isFunctionLike$1(child)) return;
25745
+ if (!isFunctionLike$2(child)) return;
24154
25746
  const functionBody = child.body;
24155
25747
  if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
24156
25748
  if (callsIdentifier(functionBody, setterName)) found = true;
@@ -24604,6 +26196,33 @@ const rerenderFunctionalSetstate = defineRule({
24604
26196
  } })
24605
26197
  });
24606
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
24607
26226
  //#region src/plugin/rules/state-and-effects/rerender-lazy-state-init.ts
24608
26227
  const rerenderLazyStateInit = defineRule({
24609
26228
  id: "rerender-lazy-state-init",
@@ -30053,7 +31672,7 @@ const isUseEffectEventSymbol = (symbol) => {
30053
31672
  const findEnclosingComponentOrHookFunction = (node) => {
30054
31673
  let current = node.parent;
30055
31674
  while (current) {
30056
- if (isFunctionLike$1(current)) {
31675
+ if (isFunctionLike$2(current)) {
30057
31676
  const resolvedName = inferFunctionName(current);
30058
31677
  if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
30059
31678
  }
@@ -30074,7 +31693,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
30074
31693
  const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
30075
31694
  let current = node.parent;
30076
31695
  while (current) {
30077
- if (isFunctionLike$1(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
31696
+ if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
30078
31697
  current = current.parent ?? null;
30079
31698
  }
30080
31699
  return false;
@@ -30408,7 +32027,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
30408
32027
  let foundAuthCall = false;
30409
32028
  for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
30410
32029
  if (foundAuthCall) return;
30411
- if (isFunctionLike$1(child)) return false;
32030
+ if (isFunctionLike$2(child)) return false;
30412
32031
  if (!isNodeOfType(child, "CallExpression")) return;
30413
32032
  if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
30414
32033
  });
@@ -32995,6 +34614,28 @@ const reactDoctorRules = [
32995
34614
  category: "Architecture"
32996
34615
  }
32997
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
+ },
32998
34639
  {
32999
34640
  key: "react-doctor/no-danger",
33000
34641
  id: "no-danger",
@@ -33193,6 +34834,17 @@ const reactDoctorRules = [
33193
34834
  category: "State & Effects"
33194
34835
  }
33195
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
+ },
33196
34848
  {
33197
34849
  key: "react-doctor/no-eval",
33198
34850
  id: "no-eval",
@@ -33666,6 +35318,17 @@ const reactDoctorRules = [
33666
35318
  category: "State & Effects"
33667
35319
  }
33668
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
+ },
33669
35332
  {
33670
35333
  key: "react-doctor/no-pure-black-background",
33671
35334
  id: "no-pure-black-background",
@@ -33677,6 +35340,17 @@ const reactDoctorRules = [
33677
35340
  category: "Architecture"
33678
35341
  }
33679
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
+ },
33680
35354
  {
33681
35355
  key: "react-doctor/no-react-children",
33682
35356
  id: "no-react-children",
@@ -33798,6 +35472,17 @@ const reactDoctorRules = [
33798
35472
  category: "Security"
33799
35473
  }
33800
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
+ },
33801
35486
  {
33802
35487
  key: "react-doctor/no-set-state",
33803
35488
  id: "no-set-state",
@@ -34106,6 +35791,39 @@ const reactDoctorRules = [
34106
35791
  category: "Accessibility"
34107
35792
  }
34108
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
+ },
34109
35827
  {
34110
35828
  key: "react-doctor/prefer-tag-over-role",
34111
35829
  id: "prefer-tag-over-role",
@@ -34238,6 +35956,28 @@ const reactDoctorRules = [
34238
35956
  category: "Correctness"
34239
35957
  }
34240
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
+ },
34241
35981
  {
34242
35982
  key: "react-doctor/rendering-animate-svg-wrapper",
34243
35983
  id: "rendering-animate-svg-wrapper",
@@ -34381,6 +36121,17 @@ const reactDoctorRules = [
34381
36121
  category: "Performance"
34382
36122
  }
34383
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
+ },
34384
36135
  {
34385
36136
  key: "react-doctor/rerender-lazy-state-init",
34386
36137
  id: "rerender-lazy-state-init",
@@ -35205,7 +36956,7 @@ const appendNode = (builder, block, node) => {
35205
36956
  };
35206
36957
  const mapDescendantsToBlock = (builder, node, block) => {
35207
36958
  builder.nodeBlock.set(node, block);
35208
- if (isFunctionLike$1(node)) return;
36959
+ if (isFunctionLike$2(node)) return;
35209
36960
  const record = node;
35210
36961
  for (const key of Object.keys(record)) {
35211
36962
  if (key === "parent") continue;
@@ -35543,7 +37294,7 @@ const analyzeControlFlow = (program) => {
35543
37294
  body: program.body
35544
37295
  });
35545
37296
  const visit = (node) => {
35546
- if (isFunctionLike$1(node)) {
37297
+ if (isFunctionLike$2(node)) {
35547
37298
  const body = node.body;
35548
37299
  if (body) buildFor(node, body);
35549
37300
  }
@@ -35560,7 +37311,7 @@ const analyzeControlFlow = (program) => {
35560
37311
  const enclosingFunction = (node) => {
35561
37312
  let current = node;
35562
37313
  while (current) {
35563
- if (isFunctionLike$1(current)) return current;
37314
+ if (isFunctionLike$2(current)) return current;
35564
37315
  if (isNodeOfType(current, "Program")) return current;
35565
37316
  current = current.parent ?? null;
35566
37317
  }