oxlint-plugin-react-doctor 0.4.2-dev.547e1e4 → 0.4.2-dev.571ca9c

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 (2) hide show
  1. package/dist/index.js +310 -308
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -719,7 +719,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
719
719
  if (totalEffects === 0) return;
720
720
  context.report({
721
721
  node: openingElement,
722
- message: `Every hide & show rebuilds ${effectfulChildren.join(", ")} from scratch because <Activity> wraps them & they use ${totalEffects} effect hook${totalEffects === 1 ? "" : "s"}.`
722
+ message: `Every hide and show rebuilds ${effectfulChildren.join(", ")} from scratch because <Activity> wraps components with ${totalEffects} effect hook${totalEffects === 1 ? "" : "s"}.`
723
723
  });
724
724
  }
725
725
  };
@@ -1127,10 +1127,55 @@ const flattenJsxName$1 = (node) => {
1127
1127
  //#region src/plugin/utils/is-member-property.ts
1128
1128
  const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
1129
1129
  //#endregion
1130
+ //#region src/plugin/constants/nextjs.ts
1131
+ const NEXTJS_SOURCE_FILE_EXTENSION_GROUP = "(?:tsx?|jsx?|mts|mjs)";
1132
+ const PAGE_FILE_PATTERN = new RegExp(`/page\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1133
+ const PAGE_OR_LAYOUT_FILE_PATTERN = new RegExp(`/(page|layout)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1134
+ const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
1135
+ const PAGES_DIRECTORY_PATTERN = /\/pages\//;
1136
+ const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
1137
+ "redirect",
1138
+ "permanentRedirect",
1139
+ "notFound",
1140
+ "forbidden",
1141
+ "unauthorized"
1142
+ ]);
1143
+ const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
1144
+ const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
1145
+ const APP_DIRECTORY_PATTERN = /\/app\//;
1146
+ const ROUTE_HANDLER_FILE_PATTERN = new RegExp(`/route\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1147
+ const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
1148
+ const MUTATING_ROUTE_SEGMENTS = new Set([
1149
+ "logout",
1150
+ "log-out",
1151
+ "signout",
1152
+ "sign-out",
1153
+ "unsubscribe",
1154
+ "delete",
1155
+ "remove",
1156
+ "revoke",
1157
+ "cancel",
1158
+ "deactivate"
1159
+ ]);
1160
+ const ERROR_BOUNDARY_FILE_PATTERN = new RegExp(`/(error|global-error)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1161
+ const GLOBAL_ERROR_FILE_PATTERN = new RegExp(`/global-error\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1162
+ const ROUTE_HANDLER_HTTP_METHODS = new Set([
1163
+ "GET",
1164
+ "POST",
1165
+ "PUT",
1166
+ "PATCH",
1167
+ "DELETE",
1168
+ "OPTIONS",
1169
+ "HEAD"
1170
+ ]);
1171
+ const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
1172
+ const OG_IMAGE_FILE_PATTERN = new RegExp(`/(opengraph-image|twitter-image)\\d*\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1173
+ //#endregion
1130
1174
  //#region src/plugin/utils/is-nextjs-metadata-image-route-filename.ts
1175
+ const METADATA_IMAGE_ROUTE_FILE_PATTERN = new RegExp(`^(opengraph-image|twitter-image|icon|apple-icon)\\d*\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
1131
1176
  const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1132
1177
  if (!rawFilename) return false;
1133
- return /^(opengraph-image|twitter-image|icon|apple-icon)\d*\.(jsx?|tsx?)$/.test(path.basename(rawFilename));
1178
+ return METADATA_IMAGE_ROUTE_FILE_PATTERN.test(path.basename(rawFilename));
1134
1179
  };
1135
1180
  //#endregion
1136
1181
  //#region src/plugin/utils/normalize-filename.ts
@@ -1369,12 +1414,12 @@ const objectHasAccessibleChild = (jsxElement, settings) => {
1369
1414
  };
1370
1415
  //#endregion
1371
1416
  //#region src/plugin/rules/a11y/alt-text.ts
1372
- const MISSING_ALT_PROP = "Blind users can't use this image because screen readers skip it without `alt`, so add `alt=\"...\"` (or `alt=\"\"` if decorative).";
1373
- const MISSING_ALT_VALUE = "Blind users can't use this image because its `alt` is empty or invalid, so add a short description (or `alt=\"\"` if decorative).";
1374
- const ARIA_LABEL_VALUE = "Blind users hear nothing here because `aria-label` has no value, so give it a short description.";
1375
- const ARIA_LABELLEDBY_VALUE = "Blind users hear nothing here because `aria-labelledby` has no value, so point it at the id of the text that labels this.";
1417
+ const MISSING_ALT_PROP = "Screen reader users cannot access this image without `alt`. Add `alt=\"image_description\"`, or `alt=\"\"` if it is decorative.";
1418
+ const MISSING_ALT_VALUE = "Screen reader users cannot access this image because its `alt` is empty or invalid. Add a short description, or `alt=\"\"` if it is decorative.";
1419
+ const ARIA_LABEL_VALUE = "Screen reader users hear nothing here because `aria-label` has no value, so give it a short description.";
1420
+ const ARIA_LABELLEDBY_VALUE = "Screen reader users hear nothing here because `aria-labelledby` has no value, so point it at the id of the text that labels this.";
1376
1421
  const PREFER_ALT = "Screen readers skip a decorative image more reliably with `alt=\"\"` than `role=\"presentation\"`, so use `alt=\"\"` instead.";
1377
- const MESSAGE_OBJECT = "Blind users can't use this `<object>` because screen readers can't describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
1422
+ const MESSAGE_OBJECT = "Screen reader users cannot use this `<object>` because assistive tech cannot describe it, so add `alt`, `aria-label`, `aria-labelledby`, `title`, or inner fallback text.";
1378
1423
  const MESSAGE_AREA = "Blind users can't use this `<area>` of the image map because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
1379
1424
  const MESSAGE_INPUT_IMAGE = "Blind users can't use this image button because screen readers can't describe it, so add `alt`, `aria-label`, or `aria-labelledby`.";
1380
1425
  const resolveSettings$52 = (settings) => {
@@ -2703,7 +2748,7 @@ const ABSTRACT_ROLES = new Set([
2703
2748
  const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
2704
2749
  //#endregion
2705
2750
  //#region src/plugin/rules/a11y/aria-role.ts
2706
- const buildBaseMessage = (suffix) => `Screen reader users get no help from this \`role\` because it isn't a valid ARIA role, so use a real, non-abstract role.${suffix}`;
2751
+ const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
2707
2752
  const resolveSettings$49 = (settings) => {
2708
2753
  const reactDoctor = settings?.["react-doctor"];
2709
2754
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
@@ -2717,7 +2762,7 @@ const ariaRole = defineRule({
2717
2762
  title: "Invalid ARIA role",
2718
2763
  tags: ["react-jsx-only"],
2719
2764
  severity: "error",
2720
- recommendation: "Use a real, non-abstract ARIA role.",
2765
+ recommendation: "Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.",
2721
2766
  category: "Accessibility",
2722
2767
  create: (context) => {
2723
2768
  const settings = resolveSettings$49(context.settings);
@@ -3512,7 +3557,7 @@ const asyncDeferAwait = defineRule({
3512
3557
  title: "await before an early-return guard",
3513
3558
  severity: "warn",
3514
3559
  tags: ["test-noise"],
3515
- recommendation: "Move the `await` below the early-return guard so the skip path stays fast",
3560
+ recommendation: "Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",
3516
3561
  create: (context) => {
3517
3562
  const inspectStatements = (statements) => {
3518
3563
  for (let statementIndex = 0; statementIndex < statements.length - 1; statementIndex++) {
@@ -3755,7 +3800,7 @@ const autocompleteValid = defineRule({
3755
3800
  title: "Invalid autocomplete value",
3756
3801
  tags: ["react-jsx-only"],
3757
3802
  severity: "warn",
3758
- recommendation: "Use a valid autofill token in `autoComplete`.",
3803
+ recommendation: "Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",
3759
3804
  category: "Accessibility",
3760
3805
  create: (context) => {
3761
3806
  const settings = resolveSettings$48(context.settings);
@@ -3810,7 +3855,7 @@ const isCreateElementCall = (node) => {
3810
3855
  //#endregion
3811
3856
  //#region src/plugin/rules/react-builtins/button-has-type.ts
3812
3857
  const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
3813
- const INVALID_MESSAGE = "This button's `type` is invalid.";
3858
+ const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
3814
3859
  const resolveSettings$47 = (settings) => {
3815
3860
  const reactDoctor = settings?.["react-doctor"];
3816
3861
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
@@ -3851,7 +3896,7 @@ const buttonHasType = defineRule({
3851
3896
  id: "button-has-type",
3852
3897
  title: "Button missing explicit type",
3853
3898
  severity: "warn",
3854
- recommendation: "Always set a `type` on a `<button>`: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
3899
+ recommendation: "Set an explicit button `type` so plain buttons do not submit forms by accident: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
3855
3900
  create: (context) => {
3856
3901
  const settings = resolveSettings$47(context.settings);
3857
3902
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -3921,7 +3966,7 @@ const buttonHasType = defineRule({
3921
3966
  //#endregion
3922
3967
  //#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
3923
3968
  const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
3924
- const EXCLUSIVE_MESSAGE = "This input behaves unpredictably with both `checked` & `defaultChecked` set.";
3969
+ const EXCLUSIVE_MESSAGE = "This input mixes `checked` with `defaultChecked`, so React can't tell whether it is controlled or uncontrolled.";
3925
3970
  const resolveSettings$46 = (settings) => {
3926
3971
  const reactDoctor = settings?.["react-doctor"];
3927
3972
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
@@ -3972,7 +4017,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
3972
4017
  id: "checked-requires-onchange-or-readonly",
3973
4018
  title: "Checked input without onChange",
3974
4019
  severity: "warn",
3975
- recommendation: "Add `onChange` (controlled) or `readOnly` (display-only), or use `defaultChecked` for an uncontrolled checkbox.",
4020
+ recommendation: "Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",
3976
4021
  category: "Correctness",
3977
4022
  create: (context) => {
3978
4023
  const settings = resolveSettings$46(context.settings);
@@ -4466,7 +4511,7 @@ const noRedundantPaddingAxes = defineRule({
4466
4511
  severity: "warn",
4467
4512
  defaultEnabled: false,
4468
4513
  category: "Architecture",
4469
- recommendation: "Collapse `px-N py-N` to `p-N` when both sides match. Keep them split only when one side changes at a breakpoint (`py-2 md:py-3`).",
4514
+ recommendation: "Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",
4470
4515
  create: (context) => ({ JSXAttribute(jsxAttribute) {
4471
4516
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
4472
4517
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
@@ -4476,7 +4521,7 @@ const noRedundantPaddingAxes = defineRule({
4476
4521
  if (matchedPairs.length === 0) return;
4477
4522
  for (const matchedPair of matchedPairs) context.report({
4478
4523
  node: jsxAttribute,
4479
- message: `px-${matchedPair.value} & py-${matchedPair.value} are the same.`
4524
+ message: `px-${matchedPair.value} and py-${matchedPair.value} duplicate p-${matchedPair.value}, so the class list is noisier without changing spacing.`
4480
4525
  });
4481
4526
  } })
4482
4527
  });
@@ -4490,7 +4535,7 @@ const noRedundantSizeAxes = defineRule({
4490
4535
  severity: "warn",
4491
4536
  defaultEnabled: false,
4492
4537
  category: "Architecture",
4493
- recommendation: "Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both sides match.",
4538
+ recommendation: "Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",
4494
4539
  create: (context) => ({ JSXAttribute(jsxAttribute) {
4495
4540
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
4496
4541
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
@@ -4500,7 +4545,7 @@ const noRedundantSizeAxes = defineRule({
4500
4545
  if (matchedPairs.length === 0) return;
4501
4546
  for (const matchedPair of matchedPairs) context.report({
4502
4547
  node: jsxAttribute,
4503
- message: `w-${matchedPair.value} & h-${matchedPair.value} are the same.`
4548
+ message: `w-${matchedPair.value} and h-${matchedPair.value} duplicate size-${matchedPair.value}, so the class list is noisier without changing layout.`
4504
4549
  });
4505
4550
  } })
4506
4551
  });
@@ -4535,7 +4580,7 @@ const noSpaceOnFlexChildren = defineRule({
4535
4580
  const spaceValue = spaceMatch[2];
4536
4581
  context.report({
4537
4582
  node: jsxAttribute,
4538
- message: `space-${spaceAxis}-${spaceValue} on a flex or grid parent breaks spacing for your users.`
4583
+ message: `space-${spaceAxis}-${spaceValue} on a flex or grid parent can leave uneven gaps when children hide, wrap, or render in RTL layouts.`
4539
4584
  });
4540
4585
  } })
4541
4586
  });
@@ -4548,14 +4593,14 @@ const noThreePeriodEllipsis = defineRule({
4548
4593
  severity: "warn",
4549
4594
  defaultEnabled: false,
4550
4595
  category: "Architecture",
4551
- recommendation: "Use the real ellipsis \"…\" (or `&hellip;`) instead of three dots. Good for labels like \"Rename…\" and \"Loading…\".",
4596
+ recommendation: "Use the real ellipsis character (\"…\") so UI labels look polished and consistent instead of like three separate periods.",
4552
4597
  create: (context) => ({ JSXText(jsxTextNode) {
4553
4598
  const textValue = typeof jsxTextNode.value === "string" ? jsxTextNode.value : "";
4554
4599
  if (!TRAILING_THREE_PERIOD_ELLIPSIS_PATTERN.test(textValue)) return;
4555
4600
  if (isInsideExcludedTypographyAncestor(jsxTextNode)) return;
4556
4601
  context.report({
4557
4602
  node: jsxTextNode,
4558
- message: "Three dots (\"...\") look unpolished to your users."
4603
+ message: "Use the real ellipsis character (\"…\") instead of three period characters."
4559
4604
  });
4560
4605
  } })
4561
4606
  });
@@ -4617,7 +4662,7 @@ const noVagueButtonLabel = defineRule({
4617
4662
  if (!VAGUE_BUTTON_LABELS.has(normalizedLabel)) return;
4618
4663
  context.report({
4619
4664
  node: jsxElementNode.openingElement ?? jsxElementNode,
4620
- message: `Screen reader & unsure users can't tell what "${labelText}" does.`
4665
+ message: `Screen reader users may not know what "${labelText}" does. Use a specific action label.`
4621
4666
  });
4622
4667
  } })
4623
4668
  });
@@ -5203,10 +5248,10 @@ const effectNeedsCleanup = defineRule({
5203
5248
  if (usages.length === 0) return;
5204
5249
  if (effectHasCleanupReturn(callback, usages)) return;
5205
5250
  const firstUsage = usages[0];
5206
- const verb = firstUsage.kind === "timer" ? "schedules" : "subscribes via";
5251
+ const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
5207
5252
  context.report({
5208
5253
  node,
5209
- message: `\`${firstUsage.resourceName}(...)\` leaks memory because useEffect ${verb} it but never cleans it up.`
5254
+ message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
5210
5255
  });
5211
5256
  } })
5212
5257
  });
@@ -5854,21 +5899,21 @@ const isOutsideAllFunctions = (symbol) => {
5854
5899
  //#region src/plugin/rules/react-builtins/exhaustive-deps-messages.ts
5855
5900
  const buildMissingDepMessage = (hookName, depName) => `\`${hookName}\` can run with a stale \`${depName}\` & show your users old data.`;
5856
5901
  const buildUnnecessaryDepMessage = (hookName, depName) => `\`${hookName}\` re-runs whenever \`${depName}\` changes even though it never uses it.`;
5857
- const buildDuplicateDepMessage = (hookName, depName) => `\`${hookName}\` lists \`${depName}\` twice in its dependency array.`;
5858
- const buildLiteralDepMessage = (hookName) => `A literal in \`${hookName}\`'s dependency array never changes & does nothing.`;
5902
+ const buildDuplicateDepMessage = (hookName, depName) => `\`${hookName}\` lists \`${depName}\` twice, adding dependency-array noise without changing when it runs.`;
5903
+ const buildLiteralDepMessage = (hookName) => `A literal in \`${hookName}\`'s dependency array never changes, so it adds noise without protecting against stale values.`;
5859
5904
  const buildRefCurrentDepMessage = (hookName, depName) => `\`${hookName}\` won't re-run when \`${depName}\` changes, since a ref never triggers a redraw.`;
5860
- const buildNonArrayDepsMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because its second argument isn't an inline array.`;
5905
+ const buildNonArrayDepsMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because its second argument isn't an inline array, so stale values can slip through.`;
5861
5906
  const buildMissingDepArrayMessage = (hookName) => `\`${hookName}\` re-runs on every render with no dependency array.`;
5862
5907
  const buildMissingCallbackMessage = (hookName) => `\`${hookName}\` crashes without a function as its first argument.`;
5863
- const buildEffectEventDepMessage = () => `A function from \`useEffectEvent\` is stable & shouldn't sit in the dependency array.`;
5864
- const buildSpreadDepMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because of a spread in the array.`;
5865
- const buildComplexDepMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because of a complex expression in the array.`;
5908
+ const buildEffectEventDepMessage = () => `A function from \`useEffectEvent\` is stable, so listing it adds noise and defeats the event/dependency split.`;
5909
+ const buildSpreadDepMessage = (hookName) => `A spread in \`${hookName}\`'s dependency array hides the actual deps, so stale values can slip through.`;
5910
+ const buildComplexDepMessage = (hookName) => `A complex expression in \`${hookName}\`'s dependency array hides the real value, so stale values can slip through.`;
5866
5911
  const buildAsyncEffectMessage = (hookName) => `\`${hookName}\` was given an async function, so its cleanup breaks.`;
5867
- const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s dependencies can't be checked because its function is defined elsewhere.`;
5912
+ const buildUnknownCallbackMessage = (hookName) => `\`${hookName}\`'s callback is defined elsewhere, so dependencies can't be checked and stale values can slip through.`;
5868
5913
  const buildUnstableDepMessage = (hookName, depName) => `\`${depName}\` is rebuilt every render, so \`${hookName}\` runs every time.`;
5869
5914
  const buildSetStateWithoutDepsMessage = (hookName, setterName) => `\`${hookName}\` calls \`${setterName}\` with no dependency array, so it can loop forever & freeze the component.`;
5870
5915
  const buildRefCleanupMessage = (depName) => `Your cleanup may read the wrong node since the ref \`${depName}\` can change before it runs.`;
5871
- const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render.`;
5916
+ const buildAssignmentMessage = (name) => `Assigning to \`${name}\` inside a hook is thrown away after each render, so the next render reads the old value.`;
5872
5917
  //#endregion
5873
5918
  //#region src/plugin/rules/react-builtins/exhaustive-deps-settings.ts
5874
5919
  const resolveExhaustiveDepsSettings = (settings) => {
@@ -6771,13 +6816,13 @@ const flattenJsxName = (name) => {
6771
6816
  return "";
6772
6817
  };
6773
6818
  const isSupportedJsxName = (name) => isNodeOfType(name, "JSXIdentifier") || isNodeOfType(name, "JSXMemberExpression");
6774
- const buildMessage$24 = (propName, message) => message ?? `Your project blocks the \`${propName}\` prop on this component.`;
6819
+ const buildMessage$24 = (propName, message) => message ?? `Your project blocks the \`${propName}\` prop on this component, so this bypasses the component API contract.`;
6775
6820
  const forbidComponentProps = defineRule({
6776
6821
  id: "forbid-component-props",
6777
- title: "Forbidden prop on component",
6822
+ title: "Blocked component prop bypasses API contract",
6778
6823
  severity: "warn",
6779
6824
  defaultEnabled: false,
6780
- recommendation: "List the props you want to block per component in the `forbidComponentProps.forbid` setting.",
6825
+ recommendation: "Configure blocked component props so callers cannot bypass the component API contract.",
6781
6826
  category: "Architecture",
6782
6827
  create: (context) => {
6783
6828
  const entries = resolveSettings$43(context.settings).forbid.map(normalizeEntry);
@@ -6808,7 +6853,7 @@ const forbidComponentProps = defineRule({
6808
6853
  });
6809
6854
  //#endregion
6810
6855
  //#region src/plugin/rules/react-builtins/forbid-dom-props.ts
6811
- const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags.`;
6856
+ const buildMessage$23 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags, so this bypasses the agreed DOM API contract.`;
6812
6857
  const resolveSettings$42 = (settings) => {
6813
6858
  const reactDoctor = settings?.["react-doctor"];
6814
6859
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
@@ -6826,9 +6871,9 @@ const resolveSettings$42 = (settings) => {
6826
6871
  };
6827
6872
  const forbidDomProps = defineRule({
6828
6873
  id: "forbid-dom-props",
6829
- title: "Forbidden DOM prop used",
6874
+ title: "Blocked DOM prop bypasses project contract",
6830
6875
  severity: "warn",
6831
- recommendation: "List the HTML props you want to block in the `forbidDomProps.forbid` setting.",
6876
+ recommendation: "Configure blocked DOM props so plain HTML tags stay on the agreed DOM API surface.",
6832
6877
  category: "Architecture",
6833
6878
  create: (context) => {
6834
6879
  const forbidMap = resolveSettings$42(context.settings);
@@ -6889,7 +6934,7 @@ const isReactFunctionCall = (node, expectedCall) => {
6889
6934
  };
6890
6935
  //#endregion
6891
6936
  //#region src/plugin/rules/react-builtins/forbid-elements.ts
6892
- const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here.`;
6937
+ const buildMessage$22 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here, so code stays on the approved UI surface.`;
6893
6938
  const resolveSettings$41 = (settings) => {
6894
6939
  const reactDoctor = settings?.["react-doctor"];
6895
6940
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
@@ -6901,9 +6946,9 @@ const resolveSettings$41 = (settings) => {
6901
6946
  const flattenMemberName$1 = flattenCalleeName;
6902
6947
  const forbidElements = defineRule({
6903
6948
  id: "forbid-elements",
6904
- title: "Forbidden element used",
6949
+ title: "Blocked element bypasses approved UI primitives",
6905
6950
  severity: "warn",
6906
- recommendation: "List the element names you want to block in the `forbidElements.forbid` setting.",
6951
+ recommendation: "Configure blocked elements so code stays on the approved UI primitives.",
6907
6952
  category: "Architecture",
6908
6953
  create: (context) => {
6909
6954
  const forbidMap = resolveSettings$41(context.settings);
@@ -6947,7 +6992,7 @@ const forwardRefUsesRef = defineRule({
6947
6992
  id: "forward-ref-uses-ref",
6948
6993
  title: "forwardRef without ref parameter",
6949
6994
  severity: "warn",
6950
- recommendation: "Either accept a `ref` parameter, or drop the `forwardRef` wrapper.",
6995
+ recommendation: "Accept the `ref` parameter or drop `forwardRef` so parents are not promised a ref that never reaches the node.",
6951
6996
  category: "Architecture",
6952
6997
  create: (context) => ({ CallExpression(node) {
6953
6998
  if (getCalleeName$1(node) !== "forwardRef") return;
@@ -7008,8 +7053,8 @@ const headingHasContent = defineRule({
7008
7053
  });
7009
7054
  //#endregion
7010
7055
  //#region src/plugin/rules/react-builtins/hook-use-state.ts
7011
- const REQUIRE_DESTRUCTURE_MESSAGE = "This `useState` result is hard to follow.";
7012
- const NAMING_CONVENTION_MESSAGE = "The setter here is hard to spot.";
7056
+ const REQUIRE_DESTRUCTURE_MESSAGE = "`useState` should be destructured as `[value, setValue]` so readers can see the state value and setter together.";
7057
+ const NAMING_CONVENTION_MESSAGE = "This `useState` setter does not match its value name, so updates are harder to trace.";
7013
7058
  const resolveSettings$39 = (settings) => {
7014
7059
  const reactDoctor = settings?.["react-doctor"];
7015
7060
  return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
@@ -7034,7 +7079,7 @@ const hookUseState = defineRule({
7034
7079
  title: "useState not destructured",
7035
7080
  severity: "warn",
7036
7081
  defaultEnabled: false,
7037
- recommendation: "Destructure useState as `const [thing, setThing] = useState(…)`.",
7082
+ recommendation: "Destructure `useState` as `const [thing, setThing] = useState(…)` so state reads and writes stay visible together.",
7038
7083
  category: "Architecture",
7039
7084
  create: (context) => {
7040
7085
  const { allowDestructuredState } = resolveSettings$39(context.settings);
@@ -7112,7 +7157,7 @@ const HOOKS_WITH_DEP_ARRAY = new Set([
7112
7157
  "useMemo",
7113
7158
  "useImperativeHandle"
7114
7159
  ]);
7115
- const NAN_MESSAGE = "`NaN` in a dependency array silently breaks your hook, since React never sees it change.";
7160
+ const NAN_MESSAGE = "`NaN` in a dependency array never compares as changed with `Object.is`, so normalize the value before passing it as a dependency.";
7116
7161
  const isNanLiteral = (node) => {
7117
7162
  if (isNodeOfType(node, "Identifier") && node.name === "NaN") return true;
7118
7163
  if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "Number" && isNodeOfType(node.property, "Identifier") && node.property.name === "NaN") return true;
@@ -7415,7 +7460,7 @@ const htmlNoNestedInteractive = defineRule({
7415
7460
  });
7416
7461
  //#endregion
7417
7462
  //#region src/plugin/rules/a11y/iframe-has-title.ts
7418
- const MESSAGE$43 = "Blind users can't tell what this `<iframe>` holds because screen readers have no title to read, so add a `title` describing its content.";
7463
+ const MESSAGE$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
7419
7464
  const evaluateTitleValue = (value) => {
7420
7465
  if (!value) return "missing";
7421
7466
  if (isNodeOfType(value, "Literal")) {
@@ -7445,7 +7490,7 @@ const iframeHasTitle = defineRule({
7445
7490
  title: "iframe missing title",
7446
7491
  tags: ["react-jsx-only"],
7447
7492
  severity: "warn",
7448
- recommendation: "Add a descriptive `title` to every `<iframe>`.",
7493
+ recommendation: "Add a descriptive `title` so screen reader users know what the embedded frame contains.",
7449
7494
  category: "Accessibility",
7450
7495
  create: (context) => ({ JSXOpeningElement(node) {
7451
7496
  const tag = getElementType(node, context.settings);
@@ -7513,7 +7558,7 @@ const iframeMissingSandbox = defineRule({
7513
7558
  id: "iframe-missing-sandbox",
7514
7559
  title: "iframe missing sandbox attribute",
7515
7560
  severity: "warn",
7516
- recommendation: "Add `sandbox=\"\"` (or a curated value) to your iframe.",
7561
+ recommendation: "Add `sandbox=\"\"` or a curated value so embedded pages cannot get full access to your site by default.",
7517
7562
  category: "Security",
7518
7563
  create: (context) => ({
7519
7564
  JSXOpeningElement(node) {
@@ -7721,7 +7766,7 @@ const interactiveSupportsFocus = defineRule({
7721
7766
  title: "Interactive element not focusable",
7722
7767
  tags: ["react-jsx-only"],
7723
7768
  severity: "warn",
7724
- recommendation: "Add `tabIndex` to elements with interactive roles and handlers.",
7769
+ recommendation: "Add keyboard focus support so users can reach interactive elements without a pointer.",
7725
7770
  category: "Accessibility",
7726
7771
  create: (context) => {
7727
7772
  const settings = resolveSettings$36(context.settings);
@@ -8539,7 +8584,7 @@ const jsHoistRegexp = defineRule({
8539
8584
  create: (context) => createLoopAwareVisitors({ NewExpression(node) {
8540
8585
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "RegExp") context.report({
8541
8586
  node,
8542
- message: "This slows the loop because new RegExp() rebuilds the pattern every pass, so move it to a constant outside the loop"
8587
+ message: "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop."
8543
8588
  });
8544
8589
  } })
8545
8590
  });
@@ -8913,7 +8958,7 @@ const jsSetMapLookups = defineRule({
8913
8958
  if (isIndexedArrayElementWithStringArgument(node.callee.object, node.arguments?.[0])) return;
8914
8959
  context.report({
8915
8960
  node,
8916
- message: `This gets slow because array.${methodName}() inside a loop scans the whole list every time, so use a Set for instant lookups`
8961
+ message: `This scales poorly because \`array.${methodName}()\` inside a loop scans the whole list every time. Use a Set for constant-time lookups.`
8917
8962
  });
8918
8963
  } })
8919
8964
  });
@@ -8937,9 +8982,9 @@ const jsTosortedImmutable = defineRule({
8937
8982
  });
8938
8983
  //#endregion
8939
8984
  //#region src/plugin/rules/react-builtins/jsx-boolean-value.ts
8940
- const NEVER_MESSAGE$2 = () => `This prop is written inconsistently.`;
8941
- const ALWAYS_MESSAGE$2 = () => `This prop is written inconsistently.`;
8942
- const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing.`;
8985
+ const NEVER_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
8986
+ const ALWAYS_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
8987
+ const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing, so the explicit false value adds noise without changing output.`;
8943
8988
  const resolveSettings$35 = (settings) => {
8944
8989
  const reactDoctor = settings?.["react-doctor"];
8945
8990
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
@@ -8955,7 +9000,7 @@ const jsxBooleanValue = defineRule({
8955
9000
  title: "Inconsistent boolean prop notation",
8956
9001
  severity: "warn",
8957
9002
  defaultEnabled: false,
8958
- recommendation: "Pick a single boolean-attribute style across the codebase (default: omit `={true}`).",
9003
+ recommendation: "Use one boolean-attribute style so equivalent true props scan the same across the codebase.",
8959
9004
  category: "Architecture",
8960
9005
  create: (context) => {
8961
9006
  const settings = resolveSettings$35(context.settings);
@@ -9009,8 +9054,8 @@ const jsxBooleanValue = defineRule({
9009
9054
  });
9010
9055
  //#endregion
9011
9056
  //#region src/plugin/rules/react-builtins/jsx-curly-brace-presence.ts
9012
- const UNNECESSARY_BRACES_MESSAGE = "These curly braces do nothing here.";
9013
- const REQUIRED_BRACES_MESSAGE = "This value needs curly braces `{ }` to read as an expression.";
9057
+ const UNNECESSARY_BRACES_MESSAGE = "These curly braces wrap a literal value, so they add JSX noise without changing output.";
9058
+ const REQUIRED_BRACES_MESSAGE = "This JSX value needs `{ }` so React reads it as an expression instead of text.";
9014
9059
  const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
9015
9060
  const resolveSettings$34 = (settings) => {
9016
9061
  const reactDoctor = settings?.["react-doctor"];
@@ -9105,7 +9150,7 @@ const jsxCurlyBracePresence = defineRule({
9105
9150
  title: "Unnecessary curly braces in JSX",
9106
9151
  severity: "warn",
9107
9152
  defaultEnabled: false,
9108
- recommendation: "Pick a consistent quoting style for JSX literal values.",
9153
+ recommendation: "Use one JSX literal style so equivalent markup scans the same without extra JSX noise.",
9109
9154
  category: "Architecture",
9110
9155
  create: (context) => {
9111
9156
  const settings = resolveSettings$34(context.settings);
@@ -9189,8 +9234,8 @@ const containsJsxElement = (root) => {
9189
9234
  };
9190
9235
  //#endregion
9191
9236
  //#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
9192
- const JSX_NOT_ALLOWED = (extension) => `This file has JSX but a \`${extension}\` name.`;
9193
- const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are meant for JSX, but this one has none.`;
9237
+ const JSX_NOT_ALLOWED = (extension) => `This file contains JSX but uses a \`${extension}\` name, so the filename no longer signals JSX to readers or tooling conventions.`;
9238
+ const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are reserved for JSX here, so using that extension without JSX makes file-type conventions less useful.`;
9194
9239
  const resolveSettings$33 = (settings) => {
9195
9240
  const reactDoctor = settings?.["react-doctor"];
9196
9241
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
@@ -9210,7 +9255,7 @@ const jsxFilenameExtension = defineRule({
9210
9255
  title: "JSX in disallowed file extension",
9211
9256
  severity: "warn",
9212
9257
  defaultEnabled: false,
9213
- recommendation: "Name files with JSX `.jsx` or `.tsx`, or whatever extension your project uses.",
9258
+ recommendation: "Name JSX files with the configured extension so file names accurately signal which files contain JSX.",
9214
9259
  category: "Architecture",
9215
9260
  create: (context) => {
9216
9261
  const settings = resolveSettings$33(context.settings);
@@ -9265,8 +9310,8 @@ const isJsxFragmentElement = (node) => {
9265
9310
  };
9266
9311
  //#endregion
9267
9312
  //#region src/plugin/rules/react-builtins/jsx-fragments.ts
9268
- const SYNTAX_MESSAGE = "This fragment is written inconsistently.";
9269
- const ELEMENT_MESSAGE = "This fragment is written inconsistently.";
9313
+ const SYNTAX_MESSAGE = "`<React.Fragment>` is used where shorthand fragments are configured, so similar wrappers look different across the codebase.";
9314
+ const ELEMENT_MESSAGE = "Fragment shorthand is used where explicit fragments are configured, so similar wrappers look different across the codebase.";
9270
9315
  const resolveSettings$32 = (settings) => {
9271
9316
  const reactDoctor = settings?.["react-doctor"];
9272
9317
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
@@ -9276,7 +9321,7 @@ const jsxFragments = defineRule({
9276
9321
  title: "Inconsistent fragment syntax",
9277
9322
  severity: "warn",
9278
9323
  defaultEnabled: false,
9279
- recommendation: "Pick one fragment style across the codebase.",
9324
+ recommendation: "Use one fragment style so identical wrappers do not look different across files.",
9280
9325
  category: "Architecture",
9281
9326
  create: (context) => {
9282
9327
  const { mode } = resolveSettings$32(context.settings);
@@ -9304,8 +9349,8 @@ const jsxFragments = defineRule({
9304
9349
  });
9305
9350
  //#endregion
9306
9351
  //#region src/plugin/rules/react-builtins/jsx-handler-names.ts
9307
- const buildHandlerNameMessage = (handlerName, propKey) => `The handler "${handlerName}" for "${propKey}" is hard to recognize.`;
9308
- const buildHandlerPropMessage = (propKey, propValue) => `The prop "${propKey}" passes a handler "${propValue}".`;
9352
+ const buildHandlerNameMessage = (handlerName, propKey) => `The handler "${handlerName}" does not match the "${propKey}" event prop convention, so readers cannot trace event flow quickly.`;
9353
+ const buildHandlerPropMessage = (propKey, propValue) => `The prop "${propKey}" passes handler "${propValue}" but is not named like an event prop, so callers cannot tell it fires an event.`;
9309
9354
  const DEFAULT_HANDLER_PREFIX = "handle";
9310
9355
  const DEFAULT_HANDLER_PROP_PREFIX = "on";
9311
9356
  const splitPrefixes = (prefixes) => prefixes.split("|").map((token) => token.trim()).filter((token) => token.length > 0);
@@ -9396,7 +9441,7 @@ const jsxHandlerNames = defineRule({
9396
9441
  title: "Inconsistent event handler names",
9397
9442
  severity: "warn",
9398
9443
  defaultEnabled: false,
9399
- recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers.",
9444
+ recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers so readers can trace event flow.",
9400
9445
  category: "Architecture",
9401
9446
  create: (context) => {
9402
9447
  const settings = resolveSettings$31(context.settings);
@@ -9630,7 +9675,7 @@ const jsxKey = defineRule({
9630
9675
  id: "jsx-key",
9631
9676
  title: "Missing key in list",
9632
9677
  severity: "error",
9633
- recommendation: "Add a `key={...}` prop to each element produced inside `.map` / array literal.",
9678
+ recommendation: "Add a stable `key` prop so React can keep list items matched to the right data when the list changes.",
9634
9679
  create: (context) => {
9635
9680
  const settings = resolveSettings$30(context.settings);
9636
9681
  return {
@@ -9796,7 +9841,7 @@ const jsxNoCommentTextnodes = defineRule({
9796
9841
  id: "jsx-no-comment-textnodes",
9797
9842
  title: "Comment rendered as JSX text",
9798
9843
  severity: "warn",
9799
- recommendation: "Wrap JSX comments in `{/* … */}` so they're parsed as comments, not children.",
9844
+ recommendation: "Wrap JSX comments in `{/* … */}` so users do not see comment text rendered as children.",
9800
9845
  create: (context) => ({ JSXText(node) {
9801
9846
  if (!hasCommentLikePattern(node.value)) return;
9802
9847
  if (isInsideLiteralTextTag(node)) return;
@@ -9903,7 +9948,7 @@ const jsxNoConstructedContextValues = defineRule({
9903
9948
  tags: ["react-jsx-only"],
9904
9949
  severity: "warn",
9905
9950
  disabledBy: ["react-compiler"],
9906
- recommendation: "Wrap the context value in `useMemo`, or move it outside the component.",
9951
+ recommendation: "Wrap the context value in `useMemo` or move it outside the component so consumers do not redraw every render.",
9907
9952
  category: "Performance",
9908
9953
  create: (context) => {
9909
9954
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -9944,7 +9989,7 @@ const jsxNoDuplicateProps = defineRule({
9944
9989
  id: "jsx-no-duplicate-props",
9945
9990
  title: "Duplicate props on element",
9946
9991
  severity: "error",
9947
- recommendation: "Remove or rename one of the duplicate props.",
9992
+ recommendation: "Remove or rename one of the duplicate props so the later value does not silently override the earlier one.",
9948
9993
  create: (context) => ({ JSXOpeningElement(node) {
9949
9994
  const seenPropNames = /* @__PURE__ */ new Set();
9950
9995
  for (const attribute of node.attributes) {
@@ -10261,7 +10306,7 @@ const jsxNoJsxAsProp = defineRule({
10261
10306
  tags: ["react-jsx-only"],
10262
10307
  severity: "warn",
10263
10308
  disabledBy: ["react-compiler"],
10264
- recommendation: "Move the JSX outside the component, or wrap it in `useMemo`.",
10309
+ recommendation: "Move the JSX outside the component or wrap it in `useMemo` so memoized children do not redraw every render.",
10265
10310
  category: "Performance",
10266
10311
  create: (context) => {
10267
10312
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -10633,7 +10678,7 @@ const jsxNoNewArrayAsProp = defineRule({
10633
10678
  tags: ["react-jsx-only"],
10634
10679
  severity: "warn",
10635
10680
  disabledBy: ["react-compiler"],
10636
- recommendation: "Wrap the array in `useMemo`, or move it outside the component.",
10681
+ recommendation: "Wrap the array in `useMemo` or move it outside the component so memoized children do not redraw every render.",
10637
10682
  category: "Performance",
10638
10683
  create: (context) => {
10639
10684
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -11096,7 +11141,7 @@ const jsxNoNewFunctionAsProp = defineRule({
11096
11141
  tags: ["react-jsx-only"],
11097
11142
  severity: "warn",
11098
11143
  disabledBy: ["react-compiler"],
11099
- recommendation: "Wrap the callback in `useCallback`, or move it outside the component.",
11144
+ recommendation: "Wrap the callback in `useCallback` or move it outside the component so memoized children do not redraw every render.",
11100
11145
  category: "Performance",
11101
11146
  create: (context) => {
11102
11147
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -11403,7 +11448,7 @@ const jsxNoNewObjectAsProp = defineRule({
11403
11448
  tags: ["react-jsx-only"],
11404
11449
  severity: "warn",
11405
11450
  disabledBy: ["react-compiler"],
11406
- recommendation: "Wrap the object in `useMemo`, or move it outside the component.",
11451
+ recommendation: "Wrap the object in `useMemo` or move it outside the component so memoized children do not redraw every render.",
11407
11452
  category: "Performance",
11408
11453
  create: (context) => {
11409
11454
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -11463,7 +11508,7 @@ const jsxNoScriptUrl = defineRule({
11463
11508
  id: "jsx-no-script-url",
11464
11509
  title: "javascript: URL in JSX",
11465
11510
  severity: "error",
11466
- recommendation: "Replace `javascript:` URLs with `onClick` or `onSubmit` handlers. React 19 blocks them anyway.",
11511
+ recommendation: "Use real event handlers instead of `javascript:` URLs so injected URL text cannot execute as code.",
11467
11512
  category: "Security",
11468
11513
  create: (context) => {
11469
11514
  const options = resolveSettings$28(context.settings);
@@ -11514,7 +11559,7 @@ const jsxNoUndef = defineRule({
11514
11559
  id: "jsx-no-undef",
11515
11560
  title: "Undefined JSX component",
11516
11561
  severity: "error",
11517
- recommendation: "Import the component or check for typos.",
11562
+ recommendation: "Import the component or fix the typo so React can resolve the JSX identifier at runtime.",
11518
11563
  create: (context) => ({ JSXOpeningElement(node) {
11519
11564
  const rootIdentifier = getRootIdentifier(node.name);
11520
11565
  if (!rootIdentifier) return;
@@ -11706,7 +11751,7 @@ const jsxPascalCase = defineRule({
11706
11751
  severity: "warn",
11707
11752
  defaultEnabled: false,
11708
11753
  tags: ["test-noise"],
11709
- recommendation: "Rename custom JSX components to PascalCase.",
11754
+ recommendation: "Rename custom JSX components to PascalCase so React treats them as components, not intrinsic DOM tags.",
11710
11755
  category: "Architecture",
11711
11756
  create: (context) => {
11712
11757
  const settings = resolveSettings$26(context.settings);
@@ -11770,7 +11815,7 @@ const jsxPropsNoSpreadMulti = defineRule({
11770
11815
  id: "jsx-props-no-spread-multi",
11771
11816
  title: "Same prop spread multiple times",
11772
11817
  severity: "warn",
11773
- recommendation: "Spread the same value at most once per element.",
11818
+ recommendation: "Spread each value at most once so later props cannot silently override earlier props from the same object.",
11774
11819
  create: (context) => ({ JSXOpeningElement(node) {
11775
11820
  const seenNames = /* @__PURE__ */ new Map();
11776
11821
  const reportedNames = /* @__PURE__ */ new Set();
@@ -11810,7 +11855,7 @@ const jsxPropsNoSpreading = defineRule({
11810
11855
  title: "Props spread onto element",
11811
11856
  severity: "warn",
11812
11857
  defaultEnabled: false,
11813
- recommendation: "List each prop explicitly so consumers can see what's being passed.",
11858
+ recommendation: "List each prop explicitly so consumers can see the component API instead of receiving hidden spread props.",
11814
11859
  category: "Architecture",
11815
11860
  create: (context) => {
11816
11861
  const settings = resolveSettings$25(context.settings);
@@ -12191,7 +12236,7 @@ const lang = defineRule({
12191
12236
  title: "Invalid lang attribute value",
12192
12237
  tags: ["react-jsx-only"],
12193
12238
  severity: "warn",
12194
- recommendation: "Use a valid language code, like `en` or `en-US`.",
12239
+ recommendation: "Use a valid language code like `en` or `en-US` so screen readers choose the right pronunciation rules.",
12195
12240
  category: "Accessibility",
12196
12241
  create: (context) => ({ JSXOpeningElement(node) {
12197
12242
  if (getElementType(node, context.settings) !== "html") return;
@@ -12218,7 +12263,7 @@ const lang = defineRule({
12218
12263
  });
12219
12264
  //#endregion
12220
12265
  //#region src/plugin/rules/a11y/media-has-caption.ts
12221
- const MESSAGE$32 = "Deaf & hard-of-hearing users miss this media without captions, so add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12266
+ const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
12222
12267
  const DEFAULT_AUDIO = ["audio"];
12223
12268
  const DEFAULT_VIDEO = ["video"];
12224
12269
  const DEFAULT_TRACK = ["track"];
@@ -12401,49 +12446,6 @@ const nextjsAsyncClientComponent = defineRule({
12401
12446
  }
12402
12447
  });
12403
12448
  //#endregion
12404
- //#region src/plugin/constants/nextjs.ts
12405
- const PAGE_FILE_PATTERN = /\/page\.(tsx?|jsx?)$/;
12406
- const PAGE_OR_LAYOUT_FILE_PATTERN = /\/(page|layout)\.(tsx?|jsx?)$/;
12407
- const INTERNAL_PAGE_PATH_PATTERN = /\/(?:(?:\((?:dashboard|admin|settings|account|internal|manage|console|portal|auth|onboarding|app|ee|protected)\))|(?:dashboard|admin|settings|account|internal|manage|console|portal))\//i;
12408
- const PAGES_DIRECTORY_PATTERN = /\/pages\//;
12409
- const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
12410
- "redirect",
12411
- "permanentRedirect",
12412
- "notFound",
12413
- "forbidden",
12414
- "unauthorized"
12415
- ]);
12416
- const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
12417
- const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
12418
- const APP_DIRECTORY_PATTERN = /\/app\//;
12419
- const ROUTE_HANDLER_FILE_PATTERN = /\/route\.(tsx?|jsx?)$/;
12420
- const CRON_ROUTE_PATTERN = /\/(?:cron|jobs\/cron)(?:\/|$)/i;
12421
- const MUTATING_ROUTE_SEGMENTS = new Set([
12422
- "logout",
12423
- "log-out",
12424
- "signout",
12425
- "sign-out",
12426
- "unsubscribe",
12427
- "delete",
12428
- "remove",
12429
- "revoke",
12430
- "cancel",
12431
- "deactivate"
12432
- ]);
12433
- const ERROR_BOUNDARY_FILE_PATTERN = /\/(error|global-error)\.(tsx?|jsx?)$/;
12434
- const GLOBAL_ERROR_FILE_PATTERN = /\/global-error\.(tsx?|jsx?)$/;
12435
- const ROUTE_HANDLER_HTTP_METHODS = new Set([
12436
- "GET",
12437
- "POST",
12438
- "PUT",
12439
- "PATCH",
12440
- "DELETE",
12441
- "OPTIONS",
12442
- "HEAD"
12443
- ]);
12444
- const GOOGLE_ANALYTICS_SCRIPT_PATTERN = /google-analytics\.com|googletagmanager\.com\/gtag/;
12445
- const OG_IMAGE_FILE_PATTERN = /\/(opengraph-image|twitter-image)\d*\.(tsx?|jsx?)$/;
12446
- //#endregion
12447
12449
  //#region src/plugin/rules/nextjs/nextjs-error-boundary-missing-use-client.ts
12448
12450
  const nextjsErrorBoundaryMissingUseClient = defineRule({
12449
12451
  id: "nextjs-error-boundary-missing-use-client",
@@ -12503,11 +12505,11 @@ const hasJsxAttribute = (attributes, attributeName) => Boolean(findJsxAttribute(
12503
12505
  //#region src/plugin/rules/nextjs/nextjs-image-missing-sizes.ts
12504
12506
  const nextjsImageMissingSizes = defineRule({
12505
12507
  id: "nextjs-image-missing-sizes",
12506
- title: "Image fill missing sizes",
12508
+ title: "next/image fill image is missing sizes",
12507
12509
  tags: ["test-noise"],
12508
12510
  requires: ["nextjs"],
12509
12511
  severity: "warn",
12510
- recommendation: "Add sizes for responsive behavior: `sizes=\"(max-width: 768px) 100vw, 50vw\"` matching your layout breakpoints",
12512
+ recommendation: "Add `sizes` matching your layout so `next/image` does not assume the largest candidate and make users download oversized images.",
12511
12513
  create: (context) => ({ JSXOpeningElement(node) {
12512
12514
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "Image") return;
12513
12515
  const attributes = node.attributes ?? [];
@@ -12543,11 +12545,11 @@ const nextjsInlineScriptMissingId = defineRule({
12543
12545
  //#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
12544
12546
  const nextjsMissingMetadata = defineRule({
12545
12547
  id: "nextjs-missing-metadata",
12546
- title: "Page missing metadata",
12548
+ title: "Page missing metadata for search previews",
12547
12549
  tags: ["test-noise"],
12548
12550
  requires: ["nextjs"],
12549
12551
  severity: "warn",
12550
- recommendation: "Add `export const metadata = { title: '...', description: '...' }` or `export async function generateMetadata()`",
12552
+ recommendation: "Add metadata or `generateMetadata()` so search engines and social previews get a title and description.",
12551
12553
  create: (context) => ({ Program(programNode) {
12552
12554
  const filename = normalizeFilename$1(context.filename ?? "");
12553
12555
  if (!PAGE_FILE_PATTERN.test(filename)) return;
@@ -12560,7 +12562,7 @@ const nextjsMissingMetadata = defineRule({
12560
12562
  return false;
12561
12563
  })) context.report({
12562
12564
  node: programNode,
12563
- message: "This page has no metadata, so search engines & social previews get no title or description."
12565
+ message: "This page has no metadata, so search engines and social previews get no title or description."
12564
12566
  });
12565
12567
  } })
12566
12568
  });
@@ -12568,7 +12570,7 @@ const nextjsMissingMetadata = defineRule({
12568
12570
  //#region src/plugin/rules/nextjs/nextjs-no-a-element.ts
12569
12571
  const nextjsNoAElement = defineRule({
12570
12572
  id: "nextjs-no-a-element",
12571
- title: "Plain anchor for internal link",
12573
+ title: "Plain anchor reloads internal Next.js links",
12572
12574
  tags: ["test-noise"],
12573
12575
  requires: ["nextjs"],
12574
12576
  severity: "warn",
@@ -12582,7 +12584,7 @@ const nextjsNoAElement = defineRule({
12582
12584
  else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
12583
12585
  if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
12584
12586
  node,
12585
- message: "Plain <a> reloads the whole page on internal links."
12587
+ message: "Plain <a> reloads the whole page for internal links, so Next.js loses client-side navigation and prefetching."
12586
12588
  });
12587
12589
  } })
12588
12590
  });
@@ -12667,11 +12669,11 @@ const nextjsNoClientSideRedirect = defineRule({
12667
12669
  //#region src/plugin/rules/nextjs/nextjs-no-css-link.ts
12668
12670
  const nextjsNoCssLink = defineRule({
12669
12671
  id: "nextjs-no-css-link",
12670
- title: "Stylesheet loaded via link",
12672
+ title: "Linked stylesheet bypasses Next.js CSS optimization",
12671
12673
  tags: ["test-noise"],
12672
12674
  requires: ["nextjs"],
12673
12675
  severity: "warn",
12674
- recommendation: "Import CSS directly: `import './styles.css'` or use CSS Modules: `import styles from './Button.module.css'`",
12676
+ recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
12675
12677
  create: (context) => ({ JSXOpeningElement(node) {
12676
12678
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
12677
12679
  const attributes = node.attributes ?? [];
@@ -12684,7 +12686,7 @@ const nextjsNoCssLink = defineRule({
12684
12686
  if (typeof hrefValue === "string" && GOOGLE_FONTS_PATTERN.test(hrefValue)) return;
12685
12687
  context.report({
12686
12688
  node,
12687
- message: "This <link rel=\"stylesheet\"> loads unbundled, unoptimized CSS."
12689
+ message: "This <link rel=\"stylesheet\"> bypasses Next.js CSS handling, so the CSS loads unbundled and unoptimized."
12688
12690
  });
12689
12691
  } })
12690
12692
  });
@@ -12708,7 +12710,7 @@ const nextjsNoDefaultExportInRouteHandler = defineRule({
12708
12710
  tags: ["test-noise"],
12709
12711
  requires: ["nextjs"],
12710
12712
  severity: "error",
12711
- recommendation: "Replace `export default` with named HTTP method exports: `export async function GET(request) { }`",
12713
+ recommendation: "Replace `export default` with named HTTP method exports because Next.js ignores default exports in `route.ts`.",
12712
12714
  create: (context) => {
12713
12715
  let isAppRouteHandler = false;
12714
12716
  let programNode = null;
@@ -12795,11 +12797,11 @@ const nextjsNoFontLink = defineRule({
12795
12797
  //#region src/plugin/rules/nextjs/nextjs-no-google-analytics-script.ts
12796
12798
  const nextjsNoGoogleAnalyticsScript = defineRule({
12797
12799
  id: "nextjs-no-google-analytics-script",
12798
- title: "Manual Google Analytics script",
12800
+ title: "Manual Google Analytics script blocks optimized loading",
12799
12801
  tags: ["test-noise"],
12800
12802
  requires: ["nextjs"],
12801
12803
  severity: "warn",
12802
- recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization & smaller bundles",
12804
+ recommendation: "Use `import { GoogleAnalytics } from '@next/third-parties/google'` for automatic optimization and smaller bundles.",
12803
12805
  create: (context) => ({ JSXOpeningElement(node) {
12804
12806
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
12805
12807
  if (node.name.name !== "script" && node.name.name !== "Script") return;
@@ -12808,7 +12810,7 @@ const nextjsNoGoogleAnalyticsScript = defineRule({
12808
12810
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
12809
12811
  if (typeof srcValue === "string" && GOOGLE_ANALYTICS_SCRIPT_PATTERN.test(srcValue)) context.report({
12810
12812
  node,
12811
- message: "Manual Google Analytics script blocks rendering. Use @next/third-parties for optimal loading strategy."
12813
+ message: "Manual Google Analytics scripts block rendering without Next.js' optimized loading strategy."
12812
12814
  });
12813
12815
  } })
12814
12816
  });
@@ -12820,7 +12822,7 @@ const nextjsNoHeadImport = defineRule({
12820
12822
  tags: ["test-noise"],
12821
12823
  requires: ["nextjs"],
12822
12824
  severity: "error",
12823
- recommendation: "Use the Metadata API instead. Add `export const metadata = { title: '...' }` or `export async function generateMetadata()`",
12825
+ recommendation: "Use the Metadata API because `next/head` is ignored in the App Router and meta tags will not render.",
12824
12826
  create: (context) => ({ ImportDeclaration(node) {
12825
12827
  if (node.source?.value !== "next/head") return;
12826
12828
  const filename = normalizeFilename$1(context.filename ?? "");
@@ -12835,18 +12837,18 @@ const nextjsNoHeadImport = defineRule({
12835
12837
  //#region src/plugin/rules/nextjs/nextjs-no-img-element.ts
12836
12838
  const nextjsNoImgElement = defineRule({
12837
12839
  id: "nextjs-no-img-element",
12838
- title: "Plain img element",
12840
+ title: "Plain img ships unoptimized images",
12839
12841
  tags: ["test-noise"],
12840
12842
  requires: ["nextjs"],
12841
12843
  severity: "warn",
12842
- recommendation: "`import Image from 'next/image'` for automatic WebP/AVIF, lazy loading, and responsive srcset",
12844
+ recommendation: "Use `next/image` so users get optimized formats, responsive srcsets, and lazy loading instead of oversized image downloads.",
12843
12845
  create: (context) => {
12844
12846
  if (isGeneratedImageRenderContext(context)) return {};
12845
12847
  return { JSXOpeningElement(node) {
12846
12848
  if (isGeneratedImageRenderContext(context, node)) return;
12847
12849
  if (isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "img") context.report({
12848
12850
  node,
12849
- message: "Plain <img> ships unoptimized, oversized images to your users."
12851
+ message: "Plain <img> ships unoptimized, oversized images."
12850
12852
  });
12851
12853
  } };
12852
12854
  }
@@ -12855,11 +12857,11 @@ const nextjsNoImgElement = defineRule({
12855
12857
  //#region src/plugin/rules/nextjs/nextjs-no-native-script.ts
12856
12858
  const nextjsNoNativeScript = defineRule({
12857
12859
  id: "nextjs-no-native-script",
12858
- title: "Plain script tag",
12860
+ title: "Plain script can block Next.js rendering",
12859
12861
  tags: ["test-noise"],
12860
12862
  requires: ["nextjs"],
12861
12863
  severity: "warn",
12862
- recommendation: "`import Script from \"next/script\"`. Use `strategy=\"afterInteractive\"` for analytics or `\"lazyOnload\"` for widgets",
12864
+ recommendation: "Use `next/script` with `strategy=\"afterInteractive\"` or `\"lazyOnload\"` so third-party scripts do not block rendering.",
12863
12865
  create: (context) => ({ JSXOpeningElement(node) {
12864
12866
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
12865
12867
  const typeAttribute = findJsxAttribute(node.attributes ?? [], "type");
@@ -12867,7 +12869,7 @@ const nextjsNoNativeScript = defineRule({
12867
12869
  if (typeof typeValue === "string" && !EXECUTABLE_SCRIPT_TYPES.has(typeValue)) return;
12868
12870
  context.report({
12869
12871
  node,
12870
- message: "Plain <script> blocks rendering with no loading strategy."
12872
+ message: "Plain <script> has no Next.js loading strategy, so it can block rendering."
12871
12873
  });
12872
12874
  } })
12873
12875
  });
@@ -12900,7 +12902,7 @@ const nextjsNoRedirectInTryCatch = defineRule({
12900
12902
  tags: ["test-noise"],
12901
12903
  requires: ["nextjs"],
12902
12904
  severity: "warn",
12903
- recommendation: "Move the redirect/notFound call outside the try block, or add `unstable_rethrow(error)` in the catch",
12905
+ recommendation: "Move `redirect()` or `notFound()` outside the try block, or rethrow in `catch`, because these APIs throw control-flow errors that catch blocks swallow.",
12904
12906
  create: (context) => {
12905
12907
  let tryCatchDepth = 0;
12906
12908
  return {
@@ -14051,7 +14053,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
14051
14053
  tags: ["test-noise"],
14052
14054
  requires: ["nextjs"],
14053
14055
  severity: "warn",
14054
- recommendation: "Wrap the component using useSearchParams: `<Suspense fallback={<Skeleton />}><SearchComponent /></Suspense>`",
14056
+ recommendation: "Wrap the component using `useSearchParams` in `<Suspense>` so the rest of the page can stay statically rendered.",
14055
14057
  create: (context) => {
14056
14058
  let isPageOrLayoutFile = false;
14057
14059
  let hasAncestorLayoutSuspense = false;
@@ -14089,7 +14091,7 @@ const nextjsNoUseSearchParamsWithoutSuspense = defineRule({
14089
14091
  if (!componentBody || !astContainsUseSearchParams(componentBody)) return;
14090
14092
  context.report({
14091
14093
  node,
14092
- message: `<${node.name.name}> uses useSearchParams() but is not wrapped in a <Suspense> boundary.`
14094
+ message: `<${node.name.name}> uses useSearchParams() outside <Suspense>, so this page falls back to client-side rendering.`
14093
14095
  });
14094
14096
  }
14095
14097
  };
@@ -14103,7 +14105,7 @@ const nextjsNoVercelOgImport = defineRule({
14103
14105
  tags: ["test-noise"],
14104
14106
  requires: ["nextjs"],
14105
14107
  severity: "warn",
14106
- recommendation: "Use `import { ImageResponse } from \"next/og\"`. The `@vercel/og` package is built into Next.js and should not be imported directly",
14108
+ recommendation: "Use `import { ImageResponse } from \"next/og\"`; do not import `@vercel/og` directly because Next.js already bundles it.",
14107
14109
  create: (context) => ({ ImportDeclaration(node) {
14108
14110
  if (node.source?.value !== "@vercel/og") return;
14109
14111
  context.report({
@@ -14617,7 +14619,7 @@ const noAdjustStateOnPropChange = defineRule({
14617
14619
  if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isProp(analysis, argRef))) continue;
14618
14620
  context.report({
14619
14621
  node: callExpr,
14620
- message: "Your users briefly see the wrong value when the prop changes."
14622
+ message: "This effect adjusts state after a prop changes, so users briefly see the stale value."
14621
14623
  });
14622
14624
  }
14623
14625
  } })
@@ -14630,7 +14632,7 @@ const noAriaHiddenOnFocusable = defineRule({
14630
14632
  title: "aria-hidden on focusable element",
14631
14633
  tags: ["react-jsx-only"],
14632
14634
  severity: "warn",
14633
- recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable.",
14635
+ recommendation: "Remove `aria-hidden` from focusable elements, or stop them being focusable, so keyboard users do not land on content screen readers hide.",
14634
14636
  category: "Accessibility",
14635
14637
  create: (context) => ({ JSXOpeningElement(node) {
14636
14638
  const ariaHidden = hasJsxPropIgnoreCase(node.attributes, "aria-hidden");
@@ -15174,7 +15176,7 @@ const noArrayIndexKey = defineRule({
15174
15176
  title: "Array index used as a key",
15175
15177
  severity: "warn",
15176
15178
  defaultEnabled: false,
15177
- recommendation: "Use a stable `key` from your data instead of the array index.",
15179
+ recommendation: "Use a stable `key` from your data so reordered items keep the right state and DOM.",
15178
15180
  category: "Performance",
15179
15181
  create: (context) => ({
15180
15182
  JSXOpeningElement(node) {
@@ -15251,7 +15253,7 @@ const noArrayIndexKey = defineRule({
15251
15253
  });
15252
15254
  //#endregion
15253
15255
  //#region src/plugin/rules/a11y/no-autofocus.ts
15254
- const MESSAGE$28 = "Screen reader & keyboard users get disoriented because `autoFocus` jumps focus on load, so remove it and let people choose where to focus.";
15256
+ const MESSAGE$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
15255
15257
  const resolveSettings$21 = (settings) => {
15256
15258
  const reactDoctor = settings?.["react-doctor"];
15257
15259
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -15485,7 +15487,7 @@ const noCascadingSetState = defineRule({
15485
15487
  title: "Multiple setState calls in one effect",
15486
15488
  severity: "warn",
15487
15489
  tags: ["test-noise"],
15488
- recommendation: "Combine into useReducer: `const [state, dispatch] = useReducer(reducer, initialState)`",
15490
+ recommendation: "Combine related updates in `useReducer` so one effect does not redraw the screen once per `setState` call.",
15489
15491
  create: (context) => ({ CallExpression(node) {
15490
15492
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15491
15493
  if (isInitOnlyEffect(node)) return;
@@ -15531,12 +15533,12 @@ const noChainStateUpdates = defineRule({
15531
15533
  });
15532
15534
  //#endregion
15533
15535
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
15534
- const MESSAGE$27 = "Your component can render the wrong children when you pass them through a `children` prop.";
15536
+ const MESSAGE$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
15535
15537
  const noChildrenProp = defineRule({
15536
15538
  id: "no-children-prop",
15537
15539
  title: "Children passed as a prop",
15538
15540
  severity: "warn",
15539
- recommendation: "Nest children between the tags instead of passing a `children` prop.",
15541
+ recommendation: "Nest children between the tags so the rendered content is visible in JSX and cannot be hidden inside a props object.",
15540
15542
  create: (context) => ({
15541
15543
  JSXAttribute(node) {
15542
15544
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -15564,13 +15566,13 @@ const noChildrenProp = defineRule({
15564
15566
  });
15565
15567
  //#endregion
15566
15568
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
15567
- const MESSAGE$26 = "`React.cloneElement` breaks easily when the cloned element's props change.";
15569
+ const MESSAGE$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
15568
15570
  const noCloneElement = defineRule({
15569
15571
  id: "no-clone-element",
15570
- title: "Use of cloneElement",
15572
+ title: "cloneElement makes child props fragile",
15571
15573
  severity: "warn",
15572
15574
  defaultEnabled: false,
15573
- recommendation: "Pass children, render props, or use `Children.map` instead of cloning elements.",
15575
+ recommendation: "Pass children or render props instead so parent code does not depend on fragile cloned child props.",
15574
15576
  category: "Architecture",
15575
15577
  create: (context) => ({ CallExpression(node) {
15576
15578
  const callee = stripParenExpression(node.callee);
@@ -15802,7 +15804,7 @@ const noCreateStoreInRender = defineRule({
15802
15804
  title: "Store created during render",
15803
15805
  severity: "error",
15804
15806
  category: "Correctness",
15805
- recommendation: "Create the store, atom, or observable at the top level of the file, not inside a component or hook.",
15807
+ recommendation: "Create stores at module scope so subscribers are not cut off and saved state does not reset every render.",
15806
15808
  create: (context) => ({ CallExpression(node) {
15807
15809
  const factory = resolveStoreFactoryForCallee(node.callee);
15808
15810
  if (!factory) return;
@@ -15818,10 +15820,10 @@ const noCreateStoreInRender = defineRule({
15818
15820
  const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
15819
15821
  const noDanger = defineRule({
15820
15822
  id: "no-danger",
15821
- title: "Use of dangerouslySetInnerHTML",
15823
+ title: "Raw HTML injection can run unsafe markup",
15822
15824
  severity: "warn",
15823
15825
  category: "Security",
15824
- recommendation: "Render trusted content as React children rather than injecting raw HTML.",
15826
+ recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
15825
15827
  create: (context) => ({
15826
15828
  JSXOpeningElement(node) {
15827
15829
  const propAttribute = hasJsxProp(node.attributes, "dangerouslySetInnerHTML");
@@ -15907,7 +15909,7 @@ const noDangerWithChildren = defineRule({
15907
15909
  id: "no-danger-with-children",
15908
15910
  title: "dangerouslySetInnerHTML with children",
15909
15911
  severity: "error",
15910
- recommendation: "Use either `children` or `dangerouslySetInnerHTML`, never both.",
15912
+ recommendation: "Use either `children` or `dangerouslySetInnerHTML` so React does not ignore one source of content.",
15911
15913
  category: "Correctness",
15912
15914
  create: (context) => ({
15913
15915
  JSXElement(node) {
@@ -16066,7 +16068,7 @@ const noDarkModeGlow = defineRule({
16066
16068
  if (!hasDarkBackground || !shadowValue || !shadowProperty) return;
16067
16069
  if (hasColoredGlowShadow(shadowValue)) context.report({
16068
16070
  node: shadowProperty,
16069
- message: "Your users see a cheap, overdone colored glow on the dark background, so use a subtle, neutral shadow instead."
16071
+ message: "A strong colored glow on a dark background can feel heavy. Use a subtle, neutral shadow instead."
16070
16072
  });
16071
16073
  } })
16072
16074
  });
@@ -16442,7 +16444,7 @@ const noDerivedUseState = defineRule({
16442
16444
  title: "Prop derived into useState",
16443
16445
  tags: ["test-noise"],
16444
16446
  severity: "warn",
16445
- recommendation: "Remove useState and compute the value inline: `const value = transform(propName)`",
16447
+ recommendation: "Compute the value inline so prop changes do not leave `useState` holding a stale copy.",
16446
16448
  create: (context) => {
16447
16449
  const propStackTracker = createComponentPropStackTracker();
16448
16450
  return {
@@ -16534,7 +16536,7 @@ const noDidMountSetState = defineRule({
16534
16536
  //#endregion
16535
16537
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
16536
16538
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
16537
- const MESSAGE$21 = "This can loop forever & freeze the component.";
16539
+ const MESSAGE$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
16538
16540
  const resolveSettings$19 = (settings) => {
16539
16541
  const reactDoctor = settings?.["react-doctor"];
16540
16542
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -16774,7 +16776,7 @@ const noDistractingElements = defineRule({
16774
16776
  title: "Distracting marquee or blink element",
16775
16777
  tags: ["react-jsx-only"],
16776
16778
  severity: "error",
16777
- recommendation: "Replace `<marquee>` and `<blink>` with normal, accessible markup.",
16779
+ recommendation: "Replace `<marquee>` and `<blink>` with normal markup so motion does not distract or disorient users.",
16778
16780
  category: "Accessibility",
16779
16781
  create: (context) => {
16780
16782
  const { distractingTags } = resolveSettings$18(context.settings);
@@ -16803,7 +16805,7 @@ const noDocumentStartViewTransition = defineRule({
16803
16805
  if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
16804
16806
  context.report({
16805
16807
  node,
16806
- message: "Your users lose React's <ViewTransition> animations when document.startViewTransition() runs directly."
16808
+ message: "Calling `document.startViewTransition()` directly can bypass React's `<ViewTransition>` animation lifecycle."
16807
16809
  });
16808
16810
  } })
16809
16811
  });
@@ -16821,13 +16823,13 @@ const noDynamicImportPath = defineRule({
16821
16823
  if (source && !isNodeOfType(source, "Literal") && !isNodeOfType(source, "TemplateLiteral")) {
16822
16824
  context.report({
16823
16825
  node,
16824
- message: "This ships in the main bundle & slows page load, since the bundler can't code-split a dynamic import path. Use a plain string path instead."
16826
+ message: "This can stay in the main bundle because the bundler cannot code-split a dynamic import path. Use a plain string path instead."
16825
16827
  });
16826
16828
  return;
16827
16829
  }
16828
16830
  if (isNodeOfType(source, "TemplateLiteral") && (source.expressions?.length ?? 0) > 0) context.report({
16829
16831
  node,
16830
- message: "This ships in the main bundle & slows page load, since the bundler can't code-split a dynamic import path. Use a plain string path instead of one with `${...}`."
16832
+ message: "This can stay in the main bundle because the bundler cannot code-split a dynamic import path with `${dynamic_path}`. Use a plain string path instead."
16831
16833
  });
16832
16834
  },
16833
16835
  CallExpression(node) {
@@ -17118,7 +17120,7 @@ const noEffectEventHandler = defineRule({
17118
17120
  title: "Effect used as an event handler",
17119
17121
  tags: ["test-noise"],
17120
17122
  severity: "warn",
17121
- recommendation: "Move the conditional logic into onClick, onChange, or onSubmit handlers directly",
17123
+ recommendation: "Move event logic into the handler that starts it so the side effect does not run late after an extra render.",
17122
17124
  create: (context) => {
17123
17125
  const propStackTracker = createComponentPropStackTracker();
17124
17126
  return {
@@ -17295,7 +17297,7 @@ const noEffectWithFreshDeps = defineRule({
17295
17297
  //#region src/plugin/rules/security/no-eval.ts
17296
17298
  const noEval = defineRule({
17297
17299
  id: "no-eval",
17298
- title: "Use of eval()",
17300
+ title: "eval() runs untrusted code strings",
17299
17301
  severity: "error",
17300
17302
  recommendation: "Use `JSON.parse` for data, or rewrite the code so it doesn't build and run code from strings.",
17301
17303
  create: (context) => ({
@@ -18168,14 +18170,14 @@ const noFetchInEffect = defineRule({
18168
18170
  id: "no-fetch-in-effect",
18169
18171
  title: "Data fetching inside an effect",
18170
18172
  severity: "warn",
18171
- recommendation: "Use `useQuery()` from @tanstack/react-query, `useSWR()`, or fetch in a Server Component instead",
18173
+ recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
18172
18174
  create: (context) => ({ CallExpression(node) {
18173
18175
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
18174
18176
  const callback = getEffectCallback(node);
18175
18177
  if (!callback) return;
18176
18178
  if (containsFetchCall(callback)) context.report({
18177
18179
  node,
18178
- message: "fetch() inside useEffect races, double-fires & leaks for your users."
18180
+ message: "fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead."
18179
18181
  });
18180
18182
  } })
18181
18183
  });
@@ -18189,9 +18191,9 @@ const ALLOWED_NAMESPACES = new Set([
18189
18191
  const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
18190
18192
  const noFindDomNode = defineRule({
18191
18193
  id: "no-find-dom-node",
18192
- title: "Use of findDOMNode",
18194
+ title: "findDOMNode breaks component encapsulation",
18193
18195
  severity: "warn",
18194
- recommendation: "Use a ref (`useRef` or `createRef`) to reach DOM nodes instead of `findDOMNode`.",
18196
+ recommendation: "Use a ref to reach DOM nodes because `findDOMNode` was removed in React 19 and can crash the app.",
18195
18197
  create: (context) => ({ CallExpression(node) {
18196
18198
  const callee = node.callee;
18197
18199
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
@@ -18228,7 +18230,7 @@ const noFlushSync = defineRule({
18228
18230
  if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
18229
18231
  if (getImportedName$1(specifier) === "flushSync") context.report({
18230
18232
  node: specifier,
18231
- message: "Your users lose View Transitions & concurrent rendering when flushSync from react-dom forces an immediate update."
18233
+ message: "`flushSync` forces an immediate update, which skips View Transitions and concurrent rendering."
18232
18234
  });
18233
18235
  }
18234
18236
  } })
@@ -18332,10 +18334,10 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRend
18332
18334
  //#region src/plugin/rules/architecture/no-giant-component.ts
18333
18335
  const noGiantComponent = defineRule({
18334
18336
  id: "no-giant-component",
18335
- title: "Component is too large",
18337
+ title: "Large component is hard to read and change",
18336
18338
  severity: "warn",
18337
18339
  tags: ["test-noise", "react-jsx-only"],
18338
- recommendation: "Pull each section into its own component, like `<UserHeader />` and `<UserActions />`.",
18340
+ recommendation: "Pull each section into its own component so the parent is easier to read, test, and change.",
18339
18341
  create: (context) => {
18340
18342
  const getOversizedComponentLineCount = (bodyNode) => {
18341
18343
  if (!bodyNode.loc) return null;
@@ -18573,11 +18575,11 @@ const noInlineBounceEasing = defineRule({
18573
18575
  if (!value) continue;
18574
18576
  if ((key === "transition" || key === "transitionTimingFunction" || key === "animation" || key === "animationTimingFunction") && isOvershootCubicBezier(value)) context.report({
18575
18577
  node: property,
18576
- message: "Your users see a dated, bouncy animation, so use ease-out or cubic-bezier(0.16, 1, 0.3, 1) for a smooth finish."
18578
+ message: "This bouncy easing can feel distracting. Use ease-out or cubic-bezier(0.16, 1, 0.3, 1) for a smoother finish."
18577
18579
  });
18578
18580
  if ((key === "animation" || key === "animationName") && hasBounceAnimationName(value)) context.report({
18579
18581
  node: property,
18580
- message: "Your users see a tacky bounce animation, so use a smooth ease-out (like ease-out-quart or expo) for a natural finish."
18582
+ message: "This bounce animation can feel distracting. Use a smooth ease-out, like ease-out-quart or expo, for a natural finish."
18581
18583
  });
18582
18584
  }
18583
18585
  },
@@ -18595,7 +18597,7 @@ const noInlineBounceEasing = defineRule({
18595
18597
  //#region src/plugin/rules/design/no-inline-exhaustive-style.ts
18596
18598
  const noInlineExhaustiveStyle = defineRule({
18597
18599
  id: "no-inline-exhaustive-style",
18598
- title: "Too many inline style properties",
18600
+ title: "Large inline style object rebuilds every render",
18599
18601
  severity: "warn",
18600
18602
  tags: ["test-noise", "react-jsx-only"],
18601
18603
  recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
@@ -18715,7 +18717,7 @@ const noInteractiveElementToNoninteractiveRole = defineRule({
18715
18717
  //#region src/plugin/rules/react-builtins/no-is-mounted.ts
18716
18718
  const noIsMounted = defineRule({
18717
18719
  id: "no-is-mounted",
18718
- title: "Use of isMounted",
18720
+ title: "isMounted lets async callbacks update after unmount",
18719
18721
  severity: "warn",
18720
18722
  recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
18721
18723
  create: (context) => ({ CallExpression(node) {
@@ -18727,7 +18729,7 @@ const noIsMounted = defineRule({
18727
18729
  if (ancestor.type === "MethodDefinition" || ancestor.type === "Property") {
18728
18730
  context.report({
18729
18731
  node,
18730
- message: "`isMounted` is unreliable in modern React & leads to bugs."
18732
+ message: "`isMounted` is unreliable in modern React, so async callbacks can update state after unmount."
18731
18733
  });
18732
18734
  return;
18733
18735
  }
@@ -18832,7 +18834,7 @@ const noLargeAnimatedBlur = defineRule({
18832
18834
  const blurRadius = Number.parseFloat(match[1]);
18833
18835
  if (blurRadius > 10) context.report({
18834
18836
  node: property,
18835
- message: `This can run out of GPU memory on phones because blur(${blurRadius}px) gets heavier as the blur & element grow, so use a smaller blur or a smaller element`
18837
+ message: `Large animated blurs can use significant GPU memory on phones because blur(${blurRadius}px) gets heavier as the blur and element grow. Use a smaller blur or a smaller element.`
18836
18838
  });
18837
18839
  }
18838
18840
  } })
@@ -19076,10 +19078,10 @@ const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
19076
19078
  };
19077
19079
  const noManyBooleanProps = defineRule({
19078
19080
  id: "no-many-boolean-props",
19079
- title: "Too many boolean props",
19081
+ title: "Boolean prop combinations are hard to test",
19080
19082
  severity: "warn",
19081
19083
  tags: ["test-noise", "react-jsx-only"],
19082
- recommendation: "Split into smaller components or named variants, like `<Button.Primary />` and `<DialogConfirm />`, instead of stacking `isPrimary` and `isConfirm` flags.",
19084
+ recommendation: "Split boolean-heavy APIs into smaller components or named variants so combinations stay testable.",
19083
19085
  create: (context) => {
19084
19086
  const reportIfMany = (booleanLikePropNames, componentName, reportNode) => {
19085
19087
  if (booleanLikePropNames.length >= 4) context.report({
@@ -19210,7 +19212,7 @@ const noMoment = defineRule({
19210
19212
  });
19211
19213
  //#endregion
19212
19214
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
19213
- const MESSAGE$17 = "This file is harder to navigate with more than one component.";
19215
+ const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
19214
19216
  const resolveSettings$16 = (settings) => {
19215
19217
  const reactDoctor = settings?.["react-doctor"];
19216
19218
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -19507,7 +19509,7 @@ const noMultiComp = defineRule({
19507
19509
  id: "no-multi-comp",
19508
19510
  title: "Multiple components in one file",
19509
19511
  severity: "warn",
19510
- recommendation: "Move secondary components into their own files.",
19512
+ recommendation: "Move secondary components into their own files so each component stays easier to find, test, and change.",
19511
19513
  category: "Architecture",
19512
19514
  create: (context) => {
19513
19515
  const settings = resolveSettings$16(context.settings);
@@ -19587,11 +19589,11 @@ const noMutableInDeps = defineRule({
19587
19589
  if (!issue) continue;
19588
19590
  if (issue.kind === "ref-current") context.report({
19589
19591
  node: element,
19590
- message: `Your effect silently never re-runs on "${issue.rootName}.current" because changing a ref doesn't redraw the screen.`
19592
+ message: `Changing "${issue.rootName}.current" does not re-render the component, so this dependency will not make the effect run again.`
19591
19593
  });
19592
19594
  else context.report({
19593
19595
  node: element,
19594
- message: `Your effect silently never re-runs on "${issue.rootName}.*" because values like \`location.pathname\` can change without redrawing the screen.`
19596
+ message: `Values like "${issue.rootName}.*" can change without re-rendering the component, so this dependency will not make the effect run again.`
19595
19597
  });
19596
19598
  }
19597
19599
  });
@@ -20022,7 +20024,7 @@ const noNamespace = defineRule({
20022
20024
  id: "no-namespace",
20023
20025
  title: "Namespaced JSX element",
20024
20026
  severity: "warn",
20025
- recommendation: "Drop the namespace and use a plain (Pascal-cased) component or DOM tag.",
20027
+ recommendation: "Use a plain component or DOM tag because React cannot render JSX namespaced names like `ns:Foo`.",
20026
20028
  create: (context) => ({
20027
20029
  JSXOpeningElement(node) {
20028
20030
  if (!isNodeOfType(node.name, "JSXNamespacedName")) return;
@@ -20054,7 +20056,7 @@ const noNestedComponentDefinition = defineRule({
20054
20056
  tags: ["test-noise", "react-jsx-only"],
20055
20057
  severity: "error",
20056
20058
  category: "Correctness",
20057
- recommendation: "Move to a separate file or to module scope above the parent component",
20059
+ recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
20058
20060
  create: (context) => {
20059
20061
  const componentStack = [];
20060
20062
  return {
@@ -20119,7 +20121,7 @@ const noNoninteractiveElementInteractions = defineRule({
20119
20121
  });
20120
20122
  //#endregion
20121
20123
  //#region src/plugin/rules/a11y/no-noninteractive-element-to-interactive-role.ts
20122
- const buildMessage$11 = (tag, role) => `Screen reader users get confused because role \`${role}\` makes \`<${tag}>\` act interactive when it isn't, so use a real interactive element instead.`;
20124
+ const buildMessage$11 = (tag, role) => `Role \`${role}\` gives \`<${tag}>\` interactive semantics even though the element is noninteractive, so screen reader users get the wrong controls.`;
20123
20125
  const DEFAULT_ALLOWED_ROLES = {
20124
20126
  ul: [
20125
20127
  "menu",
@@ -20958,13 +20960,13 @@ const noRandomKey = defineRule({
20958
20960
  if (!freshDescription) return;
20959
20961
  context.report({
20960
20962
  node: node.value,
20961
- message: `Your users lose typed input, focus & scroll position because \`key={${freshDescription}}\` makes a new key every render, so React rebuilds every item. Use a stable id from the item.`
20963
+ message: `A changing key makes React rebuild each item, which can reset typed input, focus, and scroll position. Use a stable id from the item instead of \`key={${freshDescription}}\`.`
20962
20964
  });
20963
20965
  } })
20964
20966
  });
20965
20967
  //#endregion
20966
20968
  //#region src/plugin/rules/react-builtins/no-react-children.ts
20967
- const MESSAGE$14 = "`React.Children` breaks easily when the children change shape.";
20969
+ const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
20968
20970
  const isChildrenIdentifier = (node, contextNode) => {
20969
20971
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
20970
20972
  return isImportedFromModule(contextNode, "Children", "react");
@@ -20978,10 +20980,10 @@ const isReactNamespaceMember = (node, contextNode) => {
20978
20980
  };
20979
20981
  const noReactChildren = defineRule({
20980
20982
  id: "no-react-children",
20981
- title: "Use of React.Children",
20983
+ title: "React.Children is fragile when child shape changes",
20982
20984
  severity: "warn",
20983
20985
  defaultEnabled: false,
20984
- recommendation: "Pass children as props or render them directly instead of using React.Children.",
20986
+ recommendation: "Pass children as explicit props or render them directly so child shape changes do not break traversal logic.",
20985
20987
  category: "Architecture",
20986
20988
  create: (context) => ({ CallExpression(node) {
20987
20989
  const calleeOuter = stripParenExpression(node.callee);
@@ -21079,7 +21081,7 @@ const reportTestUtilsImports = (node, context) => {
21079
21081
  };
21080
21082
  const noReactDomDeprecatedApis = defineRule({
21081
21083
  id: "no-react-dom-deprecated-apis",
21082
- title: "Deprecated react-dom APIs",
21084
+ title: "Deprecated react-dom APIs break in React 19",
21083
21085
  requires: ["react:18"],
21084
21086
  tags: ["test-noise", "migration-hint"],
21085
21087
  severity: "warn",
@@ -21096,7 +21098,7 @@ const noReactDomDeprecatedApis = defineRule({
21096
21098
  });
21097
21099
  const noReact19DeprecatedApis = defineRule({
21098
21100
  id: "no-react19-deprecated-apis",
21099
- title: "Deprecated React 19 APIs",
21101
+ title: "React 19 API migration can break callers",
21100
21102
  requires: ["react:19"],
21101
21103
  tags: ["test-noise", "migration-hint"],
21102
21104
  severity: "warn",
@@ -21204,7 +21206,7 @@ const noRedundantRoles = defineRule({
21204
21206
  title: "Redundant ARIA role",
21205
21207
  tags: ["react-jsx-only"],
21206
21208
  severity: "warn",
21207
- recommendation: "Remove `role` attributes that match what the element already does.",
21209
+ recommendation: "Remove redundant `role` attributes so assistive tech reads the element's native semantics without extra noise.",
21208
21210
  category: "Accessibility",
21209
21211
  create: (context) => {
21210
21212
  const settings = resolveSettings$13(context.settings);
@@ -21274,7 +21276,7 @@ const noRenderInRender = defineRule({
21274
21276
  title: "Component rendered by inline function call",
21275
21277
  severity: "warn",
21276
21278
  tags: ["test-noise"],
21277
- recommendation: "Make it a named component, like `const ListItem = ({ item }) => <div>{item.name}</div>`.",
21279
+ recommendation: "Make it a named component so React preserves its identity and does not remount its state.",
21278
21280
  create: (context) => ({ JSXExpressionContainer(node) {
21279
21281
  const expression = node.expression;
21280
21282
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -21293,7 +21295,7 @@ const noRenderInRender = defineRule({
21293
21295
  const RENDER_PROP_PATTERN = /^render[A-Z]/;
21294
21296
  const noRenderPropChildren = defineRule({
21295
21297
  id: "no-render-prop-children",
21296
- title: "Too many render props",
21298
+ title: "Render-prop slots make this component hard to extend",
21297
21299
  tags: ["test-noise"],
21298
21300
  severity: "warn",
21299
21301
  recommendation: "Swap `renderXxx` props for child components like `<Modal.Header>` or plain `children`, so the parent doesn't control every slot.",
@@ -21488,7 +21490,7 @@ const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
21488
21490
  const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
21489
21491
  const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
21490
21492
  const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
21491
- const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/;
21493
+ const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
21492
21494
  const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
21493
21495
  const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
21494
21496
  const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
@@ -22169,7 +22171,7 @@ const noSelfUpdatingEffect = defineRule({
22169
22171
  reportedStateNames.add(stateName);
22170
22172
  context.report({
22171
22173
  node: setterCall,
22172
- message: `${setterCall.callee.name}() loops forever because it sets \`${stateName}\`, the same state this effect watches.`
22174
+ message: `${setterCall.callee.name}() updates \`${stateName}\`, which is also in this effect's dependency list. Guard the update or move the derivation out of the effect.`
22173
22175
  });
22174
22176
  }
22175
22177
  }
@@ -22201,13 +22203,13 @@ const getParentComponent = (node) => {
22201
22203
  };
22202
22204
  //#endregion
22203
22205
  //#region src/plugin/rules/react-builtins/no-set-state.ts
22204
- const MESSAGE$12 = "Your project discourages `this.setState` here.";
22206
+ const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
22205
22207
  const noSetState = defineRule({
22206
22208
  id: "no-set-state",
22207
- title: "Use of this.setState",
22209
+ title: "Local class state forbidden",
22208
22210
  severity: "warn",
22209
22211
  defaultEnabled: false,
22210
- recommendation: "Lift state up or use an external store instead of `this.setState`.",
22212
+ recommendation: "Lift state up or use an external store so class-local state does not hide ownership.",
22211
22213
  category: "Architecture",
22212
22214
  create: (context) => ({ CallExpression(node) {
22213
22215
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
@@ -22249,7 +22251,7 @@ const noSetStateInRender = defineRule({
22249
22251
  const setterIdentifierName = setterCall.callee.name;
22250
22252
  context.report({
22251
22253
  node: setterCall,
22252
- message: `${setterIdentifierName}() loops forever because it runs during render & triggers another render.`
22254
+ message: `${setterIdentifierName}() triggers another render while rendering. Move it to an effect or event handler, or compute the value during render.`
22253
22255
  });
22254
22256
  }
22255
22257
  };
@@ -22483,9 +22485,9 @@ const resolveSettings$11 = (settings) => {
22483
22485
  };
22484
22486
  const noStringRefs = defineRule({
22485
22487
  id: "no-string-refs",
22486
- title: "Use of string refs",
22488
+ title: "String refs are legacy and fragile",
22487
22489
  severity: "warn",
22488
- recommendation: "Use a callback ref (`ref={(node) => { this.foo = node }}`) or `useRef` instead of string refs.",
22490
+ recommendation: "Use a callback ref or `useRef` so ref ownership is explicit and not tied to legacy string lookup.",
22489
22491
  create: (context) => {
22490
22492
  const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
22491
22493
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -22564,7 +22566,7 @@ const noThisInSfc = defineRule({
22564
22566
  id: "no-this-in-sfc",
22565
22567
  title: "this used in function component",
22566
22568
  severity: "warn",
22567
- recommendation: "Read from the `props` argument instead of `this.props`.",
22569
+ recommendation: "Read from the `props` argument because function components do not have a React instance `this`.",
22568
22570
  create: (context) => {
22569
22571
  const configured = (context.settings?.react)?.createClass;
22570
22572
  const customClassFactoryNames = /* @__PURE__ */ new Set();
@@ -22708,10 +22710,10 @@ const noUncontrolledInput = defineRule({
22708
22710
  const hasAllowedPartner = VALUE_PARTNER_ATTRIBUTES.some((partnerAttributeName) => findJsxAttribute(attributes, partnerAttributeName));
22709
22711
  if (isNodeOfType(valueAttribute.value, "JSXExpressionContainer") && isNodeOfType(valueAttribute.value.expression, "Identifier") && undefinedInitialStateNames.has(valueAttribute.value.expression.name)) {
22710
22712
  const stateName = valueAttribute.value.expression.name;
22711
- const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value & add onChange (or readOnly)";
22713
+ const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value and add onChange (or readOnly)";
22712
22714
  context.report({
22713
22715
  node: child,
22714
- message: `Your users hit a console warning & a field that can reset because "${stateName}" starts undefined, so <${tagName} value={${stateName}}> flips from uncontrolled to controlled. ${partnerHint} (e.g. \`useState("")\`).`
22716
+ message: `This can trigger a console warning and reset the field because "${stateName}" starts undefined, so <${tagName} value={${stateName}}> flips from uncontrolled to controlled. ${partnerHint} (e.g. \`useState("")\`).`
22715
22717
  });
22716
22718
  return;
22717
22719
  }
@@ -22773,7 +22775,7 @@ const noUnescapedEntities = defineRule({
22773
22775
  title: "Unescaped entities in JSX",
22774
22776
  severity: "warn",
22775
22777
  defaultEnabled: false,
22776
- recommendation: "Replace bare `'` / `\"` / `>` / `}` characters in JSX text with HTML entities.",
22778
+ recommendation: "Replace bare `'` / `\"` / `>` / `}` characters with HTML entities so literal UI text is encoded consistently.",
22777
22779
  create: (context) => ({ JSXText(node) {
22778
22780
  const value = node.value;
22779
22781
  for (const character of value) if (character in ESCAPED_VERSIONS) {
@@ -23708,7 +23710,7 @@ const noUnknownProperty = defineRule({
23708
23710
  id: "no-unknown-property",
23709
23711
  title: "Unknown DOM property",
23710
23712
  severity: "warn",
23711
- recommendation: "Use the prop name React expects, like `className`, `htmlFor`, or `tabIndex`.",
23713
+ recommendation: "Use the prop name React expects, like `className`, `htmlFor`, or `tabIndex`, so the attribute is applied correctly.",
23712
23714
  create: (context) => {
23713
23715
  const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
23714
23716
  const ignoreSet = new Set(ignore);
@@ -23794,7 +23796,7 @@ const UNSAFE_ALIASES = new Set([
23794
23796
  "componentWillReceiveProps",
23795
23797
  "componentWillUpdate"
23796
23798
  ]);
23797
- const buildMessage$6 = (methodName) => `\`${methodName}\` causes subtle bugs & React is removing it.`;
23799
+ const buildMessage$6 = (methodName) => `\`${methodName}\` runs during unsafe legacy render timing and is deprecated, so React may double-invoke or remove it.`;
23798
23800
  const resolveSettings$9 = (settings) => {
23799
23801
  const reactDoctor = settings?.["react-doctor"];
23800
23802
  return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
@@ -23827,7 +23829,7 @@ const noUnsafe = defineRule({
23827
23829
  id: "no-unsafe",
23828
23830
  title: "Unsafe legacy lifecycle method",
23829
23831
  severity: "warn",
23830
- recommendation: "Replace `UNSAFE_componentWillMount` / `…WillReceiveProps` / `…WillUpdate` with the modern equivalents.",
23832
+ recommendation: "Move setup to `constructor` or `componentDidMount`, prop-derived state to `getDerivedStateFromProps`, and update side effects to `componentDidUpdate` so React does not rely on deprecated unsafe lifecycles.",
23831
23833
  create: (context) => {
23832
23834
  const { checkAliases } = resolveSettings$9(context.settings);
23833
23835
  const flagsUnsafePrefix = isReactVersionAtLeast(getConfiguredReactMajorMinor(context.settings), 16, 3);
@@ -24083,7 +24085,7 @@ const noUnstableNestedComponents = defineRule({
24083
24085
  id: "no-unstable-nested-components",
24084
24086
  title: "Component defined inside a component",
24085
24087
  severity: "warn",
24086
- recommendation: "Move nested components to the top of the file. Never define one inside another.",
24088
+ recommendation: "Move nested components to module scope so React does not remount them and lose state on every render.",
24087
24089
  category: "Performance",
24088
24090
  create: (context) => {
24089
24091
  const settings = resolveSettings$8(context.settings);
@@ -24257,7 +24259,7 @@ const noWideLetterSpacing = defineRule({
24257
24259
  //#endregion
24258
24260
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
24259
24261
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
24260
- const MESSAGE$9 = "This can loop forever & freeze the component.";
24262
+ const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
24261
24263
  const resolveSettings$7 = (settings) => {
24262
24264
  const reactDoctor = settings?.["react-doctor"];
24263
24265
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -24280,7 +24282,7 @@ const noWillUpdateSetState = defineRule({
24280
24282
  id: "no-will-update-set-state",
24281
24283
  title: "setState in componentWillUpdate",
24282
24284
  severity: "warn",
24283
- recommendation: "Don't set state in `componentWillUpdate`. Use `getDerivedStateFromProps` or `componentDidUpdate` instead.",
24285
+ recommendation: "Avoid setState in componentWillUpdate because it can loop forever; derive state before render or move guarded updates to componentDidUpdate.",
24284
24286
  create: (context) => {
24285
24287
  const { mode } = resolveSettings$7(context.settings);
24286
24288
  const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
@@ -24314,7 +24316,7 @@ const noZIndex9999 = defineRule({
24314
24316
  const zValue = getStylePropertyNumberValue(property);
24315
24317
  if (zValue !== null && Math.abs(zValue) >= 1e3) context.report({
24316
24318
  node: property,
24317
- message: `z-index ${zValue} is way too high & usually hides a layering bug instead of fixing it, so use a small set scale, like 1 to 50.`
24319
+ message: `z-index ${zValue} is unusually high and can hide a layering bug instead of fixing it. Use a small set scale, like 1 to 50.`
24318
24320
  });
24319
24321
  }
24320
24322
  },
@@ -24497,12 +24499,12 @@ const UTILITY_FILE_BASENAMES = new Set([
24497
24499
  ]);
24498
24500
  //#endregion
24499
24501
  //#region src/plugin/rules/react-builtins/only-export-components.ts
24500
- const NAMED_EXPORT_MESSAGE = "Fast Refresh stops working when a file exports non-components.";
24501
- const ANONYMOUS_MESSAGE = "Fast Refresh can't track an unnamed component & full-reloads instead.";
24502
- const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh stops working.";
24503
- const REACT_CONTEXT_MESSAGE = "Fast Refresh stops working when a file exports a context too.";
24504
- const LOCAL_COMPONENT_MESSAGE = "Fast Refresh skips this component because it isn't exported.";
24505
- const NO_EXPORT_MESSAGE = "Fast Refresh can't track this component because the file exports nothing.";
24502
+ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh can't safely preserve component state.";
24503
+ const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
24504
+ const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
24505
+ const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
24506
+ const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
24507
+ const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
24506
24508
  const DEFAULT_REACT_HOCS = [
24507
24509
  "memo",
24508
24510
  "forwardRef",
@@ -24655,7 +24657,7 @@ const onlyExportComponents = defineRule({
24655
24657
  id: "only-export-components",
24656
24658
  title: "Non-component export in component file",
24657
24659
  severity: "warn",
24658
- recommendation: "Move non-component exports out of files that export components.",
24660
+ recommendation: "Move non-component exports out of component files so Fast Refresh can preserve component state instead of full-reloading.",
24659
24661
  category: "Architecture",
24660
24662
  create: (context) => {
24661
24663
  const settings = resolveSettings$6(context.settings);
@@ -24876,10 +24878,10 @@ const isChildrenMemberExpression = (node) => {
24876
24878
  };
24877
24879
  const preactNoChildrenLength = defineRule({
24878
24880
  id: "preact-no-children-length",
24879
- title: "Array methods on props.children",
24881
+ title: "Array methods on Preact children can crash",
24880
24882
  requires: ["preact"],
24881
24883
  severity: "warn",
24882
- recommendation: "Wrap with `toChildArray(children)` from `preact` before accessing array methods or `.length`.",
24884
+ recommendation: "Wrap with `toChildArray(children)` because Preact's `props.children` is not always an array and array methods can crash.",
24883
24885
  create: (context) => ({ MemberExpression(node) {
24884
24886
  if (node.computed) return;
24885
24887
  if (!isNodeOfType(node.property, "Identifier")) return;
@@ -24913,10 +24915,10 @@ const REACT_HOOK_NAMES = new Set([
24913
24915
  const buildMessage$4 = (importedNames) => `Your users hit \`__H\` undefined errors because importing ${importedNames.map((innerName) => `\`${innerName}\``).join(", ")} from \`react\` in a pure-Preact project loads a second copy of the hook state, so import from \`preact/hooks\` (or \`preact/compat\`) instead.`;
24914
24916
  const preactNoReactHooksImport = defineRule({
24915
24917
  id: "preact-no-react-hooks-import",
24916
- title: "Hooks imported from react",
24918
+ title: "React hook imports break pure Preact hook state",
24917
24919
  requires: ["pure-preact"],
24918
24920
  severity: "warn",
24919
- recommendation: "Replace `from \"react\"` with `from \"preact/hooks\"` (or `from \"preact/compat\"` if other React API surface is needed).",
24921
+ recommendation: "Import hooks from `preact/hooks` so they share Preact's renderer state instead of loading a second hook implementation.",
24920
24922
  create: (context) => ({ ImportDeclaration(node) {
24921
24923
  const source = node.source;
24922
24924
  if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
@@ -24976,7 +24978,7 @@ const preactNoRenderArguments = defineRule({
24976
24978
  title: "render() reads props from arguments",
24977
24979
  requires: ["preact"],
24978
24980
  severity: "warn",
24979
- recommendation: "Read state/props from `this.props` / `this.state` inside `render()` instead of declaring positional parameters.",
24981
+ recommendation: "Read from `this.props` and `this.state` because `preact/compat` uses React's parameterless `render()` and positional props/state become undefined.",
24980
24982
  create: (context) => ({ MethodDefinition(node) {
24981
24983
  if (!isInstanceMethodNamedRender(node)) return;
24982
24984
  if (!isInsideEs6Component$1(node)) return;
@@ -24998,7 +25000,7 @@ const preactPreferOndblclick = defineRule({
24998
25000
  title: "onDoubleClick instead of onDblClick",
24999
25001
  requires: ["pure-preact"],
25000
25002
  severity: "warn",
25001
- recommendation: "Rename the handler from `onDoubleClick` to `onDblClick` to match the DOM event name.",
25003
+ recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
25002
25004
  create: (context) => ({ JSXOpeningElement(node) {
25003
25005
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
25004
25006
  const tagName = node.name.name;
@@ -25070,8 +25072,8 @@ const preferDynamicImport = defineRule({
25070
25072
  });
25071
25073
  //#endregion
25072
25074
  //#region src/plugin/rules/react-builtins/prefer-es6-class.ts
25073
- const ALWAYS_MESSAGE$1 = "`createReactClass` is legacy & adds a dependency.";
25074
- const NEVER_MESSAGE$1 = "This component is defined inconsistently.";
25075
+ const ALWAYS_MESSAGE$1 = "`createReactClass` is legacy and adds a dependency, so this component diverges from modern React class syntax.";
25076
+ const NEVER_MESSAGE$1 = "This component uses an ES6 class where `createReactClass` is configured, so component style is inconsistent across the codebase.";
25075
25077
  const resolveSettings$5 = (settings) => {
25076
25078
  const reactDoctor = settings?.["react-doctor"];
25077
25079
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
@@ -25082,7 +25084,7 @@ const preferEs6Class = defineRule({
25082
25084
  title: "createClass instead of ES6 class",
25083
25085
  severity: "warn",
25084
25086
  defaultEnabled: false,
25085
- recommendation: "Pick one component style for the whole codebase: `class extends React.Component` (default) or `createReactClass` (legacy).",
25087
+ recommendation: "Pick one component style so readers do not have to switch between legacy `createReactClass` patterns and modern class components.",
25086
25088
  category: "Architecture",
25087
25089
  create: (context) => {
25088
25090
  const { mode = "always" } = resolveSettings$5(context.settings);
@@ -25241,7 +25243,7 @@ const preferExplicitVariants = defineRule({
25241
25243
  });
25242
25244
  //#endregion
25243
25245
  //#region src/plugin/rules/react-builtins/prefer-function-component.ts
25244
- const MESSAGE$7 = "This class component is harder to maintain than a function component.";
25246
+ const MESSAGE$7 = "This class component keeps behavior in lifecycle methods, so state and effects are harder to follow than in a hook-based function component.";
25245
25247
  const resolveSettings$4 = (settings) => {
25246
25248
  const reactDoctor = settings?.["react-doctor"];
25247
25249
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
@@ -25267,7 +25269,7 @@ const preferFunctionComponent = defineRule({
25267
25269
  title: "Class component instead of function",
25268
25270
  severity: "warn",
25269
25271
  defaultEnabled: false,
25270
- recommendation: "Re-write the class component as a function component using hooks.",
25272
+ recommendation: "Rewrite the class component as a function component so state and effects use modern hook patterns instead of class lifecycles.",
25271
25273
  category: "Architecture",
25272
25274
  create: (context) => {
25273
25275
  const settings = resolveSettings$4(context.settings);
@@ -25619,7 +25621,7 @@ const preferTagOverRole = defineRule({
25619
25621
  title: "Role used instead of HTML tag",
25620
25622
  tags: ["react-jsx-only"],
25621
25623
  severity: "warn",
25622
- recommendation: "Replace `role` with the matching HTML element when one exists.",
25624
+ recommendation: "Use the matching HTML element when one exists so browsers and assistive tech get native semantics.",
25623
25625
  category: "Accessibility",
25624
25626
  create: (context) => ({ JSXOpeningElement(node) {
25625
25627
  const tag = getElementType(node, context.settings);
@@ -25925,7 +25927,7 @@ const preferUseReducer = defineRule({
25925
25927
  title: "Many related useState calls",
25926
25928
  tags: ["test-noise"],
25927
25929
  severity: "warn",
25928
- recommendation: "Group related state: `const [state, dispatch] = useReducer(reducer, { field1, field2, ... })`",
25930
+ recommendation: "Group related state in `useReducer` so one logical update does not fan out into separate renders.",
25929
25931
  create: (context) => {
25930
25932
  const reportExcessiveUseState = (body, componentName) => {
25931
25933
  if (!isNodeOfType(body, "BlockStatement")) return;
@@ -25957,7 +25959,7 @@ const preferUseReducer = defineRule({
25957
25959
  //#region src/plugin/rules/tanstack-query/query-destructure-result.ts
25958
25960
  const queryDestructureResult = defineRule({
25959
25961
  id: "query-destructure-result",
25960
- title: "Destructure TanStack Query result",
25962
+ title: "Whole query result subscribes to every field",
25961
25963
  tags: ["test-noise"],
25962
25964
  requires: ["tanstack-query"],
25963
25965
  severity: "error",
@@ -26010,7 +26012,7 @@ const queryNoQueryInEffect = defineRule({
26010
26012
  tags: ["test-noise"],
26011
26013
  requires: ["tanstack-query"],
26012
26014
  severity: "warn",
26013
- recommendation: "React Query refetches automatically via queryKey changes and the `enabled` option. A manual refetch() in useEffect is usually unnecessary.",
26015
+ recommendation: "Use `queryKey` changes or `enabled` so React Query schedules the fetch once instead of refetching again from `useEffect`.",
26014
26016
  create: (context) => ({ CallExpression(node) {
26015
26017
  if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
26016
26018
  const callback = getEffectCallback(node);
@@ -26333,7 +26335,7 @@ const reduxUseselectorInlineDerivation = defineRule({
26333
26335
  severity: "warn",
26334
26336
  category: "Performance",
26335
26337
  disabledBy: ["react-compiler"],
26336
- recommendation: "Select the raw slice and derive with `useMemo`, or use `createSelector` from `reselect`.",
26338
+ recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
26337
26339
  create: (context) => {
26338
26340
  let aliases = /* @__PURE__ */ new Set();
26339
26341
  return {
@@ -26393,7 +26395,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
26393
26395
  severity: "warn",
26394
26396
  category: "Performance",
26395
26397
  disabledBy: ["react-compiler"],
26396
- recommendation: "Return a primitive, split into multiple useSelector calls, or pass `shallowEqual` from `react-redux` as the second argument.",
26398
+ recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
26397
26399
  create: (context) => {
26398
26400
  let aliases = /* @__PURE__ */ new Set();
26399
26401
  return {
@@ -26419,7 +26421,7 @@ const renderingAnimateSvgWrapper = defineRule({
26419
26421
  title: "Animating an SVG directly",
26420
26422
  tags: ["test-noise"],
26421
26423
  severity: "warn",
26422
- recommendation: "Wrap the SVG: `<motion.div animate={...}><svg>...</svg></motion.div>`",
26424
+ recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
26423
26425
  create: (context) => ({ JSXOpeningElement(node) {
26424
26426
  if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
26425
26427
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
@@ -26569,7 +26571,7 @@ const renderingHydrationMismatchTime = defineRule({
26569
26571
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
26570
26572
  context.report({
26571
26573
  node,
26572
- message: `This breaks hydration because ${matched.display} in JSX gives a different value on the server than in the browser, so move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose`
26574
+ message: `This can cause a hydration mismatch because ${matched.display} in JSX gives a different value on the server than in the browser. Move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose.`
26573
26575
  });
26574
26576
  return;
26575
26577
  }
@@ -26578,7 +26580,7 @@ const renderingHydrationMismatchTime = defineRule({
26578
26580
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
26579
26581
  context.report({
26580
26582
  node: child,
26581
- message: `This breaks hydration because ${pattern.display} reached from JSX gives a different value on the server than in the browser, so move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose`
26583
+ message: `This can cause a hydration mismatch because ${pattern.display} reached from JSX gives a different value on the server than in the browser. Move it into useEffect+useState to run only in the browser, or add suppressHydrationWarning to the parent if it's on purpose.`
26582
26584
  });
26583
26585
  return;
26584
26586
  }
@@ -26857,7 +26859,7 @@ const requireRenderReturn = defineRule({
26857
26859
  id: "require-render-return",
26858
26860
  title: "Render method does not return",
26859
26861
  severity: "error",
26860
- recommendation: "Return JSX from your component's `render` method.",
26862
+ recommendation: "Return JSX or `null` from `render` so the component intentionally shows something or nothing.",
26861
26863
  create: (context) => {
26862
26864
  const checkFunction = (functionNode) => {
26863
26865
  const host = resolveRenderHost(functionNode);
@@ -27179,7 +27181,7 @@ const rerenderLazyRefInit = defineRule({
27179
27181
  tags: ["test-noise"],
27180
27182
  severity: "warn",
27181
27183
  category: "Performance",
27182
- recommendation: "Set it up only once: `const ref = useRef<T | null>(null); if (ref.current === null) ref.current = expensiveCall();`",
27184
+ recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
27183
27185
  create: (context) => ({ CallExpression(node) {
27184
27186
  if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
27185
27187
  const initializer = node.arguments[0];
@@ -27206,7 +27208,7 @@ const rerenderLazyStateInit = defineRule({
27206
27208
  tags: ["test-noise"],
27207
27209
  severity: "warn",
27208
27210
  category: "Performance",
27209
- recommendation: "Wrap in an arrow function so it only runs once: `useState(() => expensiveComputation())`",
27211
+ recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
27210
27212
  create: (context) => ({ CallExpression(node) {
27211
27213
  if (!isHookCall$1(node, "useState") || !node.arguments?.length) return;
27212
27214
  const initializer = node.arguments[0];
@@ -27489,7 +27491,7 @@ const rerenderTransitionsScroll = defineRule({
27489
27491
  }
27490
27492
  context.report({
27491
27493
  node: setStateCall,
27492
- message: `This causes jank because setState in a "${eventName}" handler redraws the screen many times a second, so wrap it in startTransition, use useDeferredValue, or keep the value in a ref & throttle with requestAnimationFrame`
27494
+ message: `This can make scrolling stutter because setState in a "${eventName}" handler redraws on every event. Wrap it in startTransition, use useDeferredValue, or keep the value in a ref and throttle with requestAnimationFrame.`
27493
27495
  });
27494
27496
  } })
27495
27497
  });
@@ -27509,7 +27511,7 @@ const rnAnimateLayoutProperty = defineRetiredRule({
27509
27511
  tags: ["test-noise"],
27510
27512
  requires: ["react-native"],
27511
27513
  severity: "warn",
27512
- recommendation: "Reanimated useAnimatedStyle runs on the UI thread; layout-affecting properties driven by animation helpers, interpolate, or shared values are valid."
27514
+ recommendation: "Retired: Reanimated `useAnimatedStyle` can safely drive layout-affecting properties on the UI thread, so this pattern should not be flagged."
27513
27515
  });
27514
27516
  //#endregion
27515
27517
  //#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
@@ -27540,7 +27542,7 @@ const rnAnimationReactionAsDerived = defineRule({
27540
27542
  if (!isValueAssignment && !isSetCall) return;
27541
27543
  context.report({
27542
27544
  node,
27543
- message: "Your users can see a stale value when this useAnimatedReaction only copies one value to another."
27545
+ message: "This useAnimatedReaction only copies one shared value into another, so it can miss Reanimated's derived-value dependency tracking."
27544
27546
  });
27545
27547
  } })
27546
27548
  });
@@ -27557,17 +27559,17 @@ const JS_BOTTOM_SHEET_PACKAGES = new Set([
27557
27559
  ]);
27558
27560
  const rnBottomSheetPreferNative = defineRule({
27559
27561
  id: "rn-bottom-sheet-prefer-native",
27560
- title: "JS bottom sheet over native Modal",
27562
+ title: "JS bottom sheet misses native sheet behavior",
27561
27563
  tags: ["test-noise"],
27562
27564
  requires: ["react-native"],
27563
27565
  severity: "warn",
27564
- recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">` for native gestures and snap points.",
27566
+ recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">` so the sheet uses platform-native gestures, detents, accessibility, and presentation behavior.",
27565
27567
  create: (context) => ({ ImportDeclaration(node) {
27566
27568
  const source = node.source?.value;
27567
27569
  if (typeof source !== "string" || !JS_BOTTOM_SHEET_PACKAGES.has(source)) return;
27568
27570
  context.report({
27569
27571
  node,
27570
- message: `Your users feel a less native bottom sheet with ${source}.`
27572
+ message: `Users get JS-driven sheet gestures and presentation with ${source}, instead of the platform-native formSheet behavior.`
27571
27573
  });
27572
27574
  } })
27573
27575
  });
@@ -27653,13 +27655,13 @@ const rnDetoxMissingAwait = defineRule({
27653
27655
  if (root.calleeName === "waitFor") {
27654
27656
  context.report({
27655
27657
  node,
27656
- message: "This Detox `waitFor(...)` chain isn't awaited. Prepend `await`."
27658
+ message: "This Detox `waitFor` chain isn't awaited, so the test can continue before the condition settles. Prepend `await`."
27657
27659
  });
27658
27660
  return;
27659
27661
  }
27660
27662
  if (root.calleeName === "expect" && isDetoxExpectSubject(root.rootCall)) context.report({
27661
27663
  node,
27662
- message: "This Detox `expect(element(...))` assertion isn't awaited. Prepend `await`."
27664
+ message: "This Detox `expect(element)` assertion isn't awaited, so the test can pass or fail before the assertion settles. Prepend `await`."
27663
27665
  });
27664
27666
  } };
27665
27667
  }
@@ -28290,7 +28292,7 @@ const rnNoLegacyExpoPackages = defineRule({
28290
28292
  tags: ["test-noise"],
28291
28293
  requires: ["react-native"],
28292
28294
  severity: "warn",
28293
- recommendation: "These Expo packages are no longer maintained. Switch to the recommended replacement package.",
28295
+ recommendation: "Switch to the maintained replacement package so users are not stuck with unfixed bugs in deprecated Expo packages.",
28294
28296
  create: (context) => ({ ImportDeclaration(node) {
28295
28297
  const source = node.source?.value;
28296
28298
  if (typeof source !== "string") return;
@@ -28372,7 +28374,7 @@ const rnNoNonNativeNavigator = defineRule({
28372
28374
  if (!NON_NATIVE_NAVIGATOR_PACKAGES.get(source)) return;
28373
28375
  context.report({
28374
28376
  node,
28375
- message: `Your users feel less native transitions when ${source} uses a JS navigator.`
28377
+ message: `Users get JS-driven transitions and gestures from ${source}, instead of platform-native navigation behavior.`
28376
28378
  });
28377
28379
  } })
28378
28380
  });
@@ -28678,7 +28680,7 @@ const collectReturnedJsxElements = (expression) => {
28678
28680
  };
28679
28681
  const rnNoRenderitemKey = defineRule({
28680
28682
  id: "rn-no-renderitem-key",
28681
- title: "Useless key on renderItem JSX",
28683
+ title: "renderItem key is ignored by React Native lists",
28682
28684
  tags: ["test-noise"],
28683
28685
  requires: ["react-native"],
28684
28686
  severity: "warn",
@@ -28819,7 +28821,7 @@ const rnNoSetNativeProps = defineRule({
28819
28821
  //#region src/plugin/rules/react-native/rn-no-single-element-style-array.ts
28820
28822
  const rnNoSingleElementStyleArray = defineRule({
28821
28823
  id: "rn-no-single-element-style-array",
28822
- title: "Single-element style array",
28824
+ title: "Single-element style array adds wasted allocation",
28823
28825
  tags: ["test-noise"],
28824
28826
  requires: ["react-native"],
28825
28827
  severity: "warn",
@@ -28842,11 +28844,11 @@ const rnNoSingleElementStyleArray = defineRule({
28842
28844
  //#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
28843
28845
  const rnPreferContentInsetAdjustment = defineRetiredRule({
28844
28846
  id: "rn-prefer-content-inset-adjustment",
28845
- title: "Manual safe-area inset adjustment",
28847
+ title: "Manual safe-area insets can duplicate offsets",
28846
28848
  tags: ["test-noise"],
28847
28849
  requires: ["react-native"],
28848
28850
  severity: "warn",
28849
- recommendation: "Prefer native content inset adjustment only when it replaces manual inset plumbing; SafeAreaView wrappers are valid and intentionally ignored."
28851
+ recommendation: "Retired: SafeAreaView wrappers are valid; prefer native content inset adjustment only when manual inset plumbing causes scroll jumps or duplicated safe-area offsets."
28850
28852
  });
28851
28853
  //#endregion
28852
28854
  //#region src/react-native-dependency-names.ts
@@ -29031,7 +29033,7 @@ const rnPreferPressable = defineRule({
29031
29033
  tags: ["test-noise"],
29032
29034
  requires: ["react-native"],
29033
29035
  severity: "warn",
29034
- recommendation: "Use `<Pressable>` from react-native (or react-native-gesture-handler) instead of the old Touchable* components.",
29036
+ recommendation: "Use `<Pressable>` because Touchable* components are frozen and lack Pressable's state-based feedback and accessibility behavior.",
29035
29037
  create: (context) => ({ ImportDeclaration(node) {
29036
29038
  const source = node.source?.value;
29037
29039
  if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
@@ -29467,7 +29469,7 @@ const roleHasRequiredAriaProps = defineRule({
29467
29469
  title: "Role missing required ARIA props",
29468
29470
  tags: ["react-jsx-only"],
29469
29471
  severity: "error",
29470
- recommendation: "Add every required `aria-*` attribute when you set an interactive role.",
29472
+ recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
29471
29473
  category: "Accessibility",
29472
29474
  create: (context) => ({ JSXOpeningElement(node) {
29473
29475
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
@@ -32713,16 +32715,16 @@ const roleSupportsAriaProps = defineRule({
32713
32715
  });
32714
32716
  //#endregion
32715
32717
  //#region src/plugin/rules/react-builtins/rules-of-hooks.ts
32716
- const buildTopLevelMessage = (hookName) => `\`${hookName}\` crashes at the top level of a file.`;
32717
- const buildNonComponentMessage = (hookName, functionName) => `\`${hookName}\` crashes inside \`${functionName}\` because it isn't a component or a hook.`;
32718
- const buildConditionalMessage = (hookName) => `\`${hookName}\` crashes when you call it conditionally.`;
32719
- const buildLoopMessage = (hookName) => `\`${hookName}\` crashes when you call it inside a loop.`;
32720
- const buildAsyncMessage = (hookName) => `\`${hookName}\` crashes inside an async function.`;
32721
- const buildClassComponentMessage = (hookName) => `\`${hookName}\` crashes in a class component.`;
32722
- const buildTryMessage = (hookName) => `\`${hookName}\` crashes inside a try/catch/finally block.`;
32718
+ const buildTopLevelMessage = (hookName) => `\`${hookName}\` can only run inside a React component or custom Hook because React needs that render scope to track Hook state.`;
32719
+ const buildNonComponentMessage = (hookName, functionName) => `\`${hookName}\` runs inside \`${functionName}\`, which is not a component or Hook, so React cannot attach Hook state to a render.`;
32720
+ const buildConditionalMessage = (hookName) => `\`${hookName}\` changes Hook order between renders when called conditionally, so React can attach state to the wrong Hook.`;
32721
+ const buildLoopMessage = (hookName) => `\`${hookName}\` can run a different number of times inside a loop, so React can attach state to the wrong Hook.`;
32722
+ const buildAsyncMessage = (hookName) => `\`${hookName}\` runs inside an async function, so React cannot guarantee the same Hook order during render.`;
32723
+ const buildClassComponentMessage = (hookName) => `\`${hookName}\` cannot run in a class component because Hooks require a function component or custom Hook render scope.`;
32724
+ const buildTryMessage = (hookName) => `\`${hookName}\` can be skipped by try/catch/finally control flow, so React can attach state to the wrong Hook.`;
32723
32725
  const buildEffectEventCallMessage = (bindingName) => `\`${bindingName}\` comes from useEffectEvent, so it only works when called from Effects in the same component.`;
32724
32726
  const buildEffectEventAssignmentMessage = (bindingName) => `${buildEffectEventCallMessage(bindingName)} It also breaks if saved in a variable or passed around.`;
32725
- const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent breaks when passed around.`;
32727
+ const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent only works inside Effects in the same component, so passing it around breaks the event/dependency split.`;
32726
32728
  const ASCII_UPPERCASE_A = 65;
32727
32729
  const ASCII_UPPERCASE_Z = 90;
32728
32730
  const EFFECT_HOOK_NAMES = new Set([
@@ -32975,7 +32977,7 @@ const rulesOfHooks = defineRule({
32975
32977
  title: "Hook called conditionally",
32976
32978
  severity: "error",
32977
32979
  tags: ["test-noise"],
32978
- recommendation: "Call hooks at the top level of a React function component or a custom Hook.",
32980
+ recommendation: "Call hooks at the top level of a React function component or custom Hook so React sees the same hook order on every render.",
32979
32981
  category: "Correctness",
32980
32982
  create: (context) => {
32981
32983
  const settings = resolveSettings$3(context.settings);
@@ -33103,13 +33105,13 @@ const rulesOfHooks = defineRule({
33103
33105
  });
33104
33106
  //#endregion
33105
33107
  //#region src/plugin/rules/a11y/scope.ts
33106
- const MESSAGE$3 = "Screen reader users get no help from `scope` here because it only works on `<th>` cells, so remove it.";
33108
+ const MESSAGE$3 = "The `scope` attribute only works on `<th>` cells, so screen readers get no table-header help from it here.";
33107
33109
  const scope = defineRule({
33108
33110
  id: "scope",
33109
33111
  title: "scope attribute on non-th element",
33110
33112
  tags: ["react-jsx-only"],
33111
33113
  severity: "warn",
33112
- recommendation: "Only use `scope` on `<th>` cells.",
33114
+ recommendation: "Remove `scope` from this element or move it to the related `<th>` cell.",
33113
33115
  category: "Accessibility",
33114
33116
  create: (context) => ({ JSXOpeningElement(node) {
33115
33117
  const scopeAttribute = hasJsxProp(node.attributes, "scope");
@@ -33124,7 +33126,7 @@ const scope = defineRule({
33124
33126
  });
33125
33127
  //#endregion
33126
33128
  //#region src/plugin/rules/react-builtins/self-closing-comp.ts
33127
- const MESSAGE$2 = "This tag has no children.";
33129
+ const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
33128
33130
  const resolveSettings$2 = (settings) => {
33129
33131
  const reactDoctor = settings?.["react-doctor"];
33130
33132
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.selfClosingComp ?? {} : {};
@@ -33143,7 +33145,7 @@ const selfClosingComp = defineRule({
33143
33145
  title: "Element not self-closing",
33144
33146
  severity: "warn",
33145
33147
  defaultEnabled: false,
33146
- recommendation: "Use the self-closing form `<X />` for elements with no children.",
33148
+ recommendation: "Use `<X />` for childless elements so empty closing tags do not add noise.",
33147
33149
  category: "Architecture",
33148
33150
  create: (context) => {
33149
33151
  const settings = resolveSettings$2(context.settings);
@@ -33340,9 +33342,9 @@ const getCandidateFromDefaultDeclaration = (node) => {
33340
33342
  };
33341
33343
  const serverAuthActions = defineRule({
33342
33344
  id: "server-auth-actions",
33343
- title: "Server action without auth check",
33345
+ title: "Unauthenticated server action can be called directly",
33344
33346
  severity: "error",
33345
- recommendation: "Add `const session = await auth()` at the top, and throw or redirect if the user isn't allowed before touching any data.",
33347
+ recommendation: "Check auth before touching data because exported server actions can be called directly by unauthenticated clients.",
33346
33348
  create: (context) => {
33347
33349
  let fileHasUseServerDirective = false;
33348
33350
  const customAuthFunctionNames = getReactDoctorStringArraySetting(context.settings, "serverAuthFunctionNames");
@@ -33480,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
33480
33482
  }
33481
33483
  return false;
33482
33484
  };
33483
- const APP_ROUTER_FILE_PATTERN = /\/app\/(?:[^/]+\/)*(?:route|page|layout|template|loading|error|default)\.(?:tsx?|jsx?)$/;
33485
+ const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
33484
33486
  const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
33485
33487
  const serverFetchWithoutRevalidate = defineRule({
33486
33488
  id: "server-fetch-without-revalidate",
@@ -33723,8 +33725,8 @@ const serverSequentialIndependentAwait = defineRule({
33723
33725
  });
33724
33726
  //#endregion
33725
33727
  //#region src/plugin/rules/react-builtins/state-in-constructor.ts
33726
- const ALWAYS_MESSAGE = "This component's state is set up inconsistently.";
33727
- const NEVER_MESSAGE = "This component's state is set up inconsistently.";
33728
+ const ALWAYS_MESSAGE = "This class uses a state field instead of the configured constructor pattern, so state setup is inconsistent across the codebase.";
33729
+ const NEVER_MESSAGE = "This class sets state in the constructor instead of the configured class-field pattern, so state setup is inconsistent across the codebase.";
33728
33730
  const resolveSettings$1 = (settings) => {
33729
33731
  const reactDoctor = settings?.["react-doctor"];
33730
33732
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.stateInConstructor ?? {} : {}).mode ?? "always" };
@@ -33756,7 +33758,7 @@ const stateInConstructor = defineRule({
33756
33758
  title: "State initialized in constructor",
33757
33759
  severity: "warn",
33758
33760
  defaultEnabled: false,
33759
- recommendation: "Pick one way to set up state in class components and stick with it.",
33761
+ recommendation: "Use one class-state setup pattern so readers know where initial state lives.",
33760
33762
  category: "Architecture",
33761
33763
  create: (context) => {
33762
33764
  const { mode } = resolveSettings$1(context.settings);
@@ -33859,7 +33861,7 @@ const stylePropObject = defineRule({
33859
33861
  id: "style-prop-object",
33860
33862
  title: "Style prop is not an object",
33861
33863
  severity: "warn",
33862
- recommendation: "Pass the `style` prop as `{{ color: 'red' }}` (object literal), not a string.",
33864
+ recommendation: "Pass `style` as an object so React can apply CSS properties instead of ignoring a string style value.",
33863
33865
  category: "Correctness",
33864
33866
  create: (context) => {
33865
33867
  const { allow } = resolveSettings(context.settings);
@@ -34205,11 +34207,11 @@ const tanstackStartMissingHeadContent = defineRule({
34205
34207
  //#region src/plugin/rules/tanstack-start/tanstack-start-no-anchor-element.ts
34206
34208
  const tanstackStartNoAnchorElement = defineRule({
34207
34209
  id: "tanstack-start-no-anchor-element",
34208
- title: "Plain anchor for internal navigation",
34210
+ title: "Plain anchor reloads TanStack Router navigation",
34209
34211
  tags: ["test-noise"],
34210
34212
  requires: ["tanstack-start"],
34211
34213
  severity: "warn",
34212
- recommendation: "`import { Link } from '@tanstack/react-router'` for type-safe routes, preloading via `preload=\"intent\"`, and client-side navigation",
34214
+ recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
34213
34215
  create: (context) => ({ JSXOpeningElement(node) {
34214
34216
  const filename = normalizeFilename$1(context.filename ?? "");
34215
34217
  if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
@@ -34222,7 +34224,7 @@ const tanstackStartNoAnchorElement = defineRule({
34222
34224
  else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
34223
34225
  if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
34224
34226
  node,
34225
- message: "Plain <a> reloads the whole page on internal navigation."
34227
+ message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
34226
34228
  });
34227
34229
  } })
34228
34230
  });
@@ -34294,7 +34296,7 @@ const tanstackStartNoNavigateInRender = defineRule({
34294
34296
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
34295
34297
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) context.report({
34296
34298
  node,
34297
- message: "navigate() during render causes hydration errors."
34299
+ message: "navigate() runs during render here, so server and browser output can diverge during hydration."
34298
34300
  });
34299
34301
  },
34300
34302
  "CallExpression:exit"(node) {
@@ -34378,7 +34380,7 @@ const tanstackStartNoUseServerInHandler = defineRule({
34378
34380
  if (!isNodeOfType(body, "BlockStatement")) return;
34379
34381
  if (body.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && (statement.directive === "use server" || isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use server"))) context.report({
34380
34382
  node: handlerFunction,
34381
- message: "\"use server\" inside a createServerFn handler causes compile errors."
34383
+ message: "\"use server\" inside a createServerFn handler duplicates TanStack Start's server boundary, so the route can fail to compile."
34382
34384
  });
34383
34385
  } })
34384
34386
  });
@@ -34452,11 +34454,11 @@ const tanstackStartRedirectInTryCatch = defineRule({
34452
34454
  //#region src/plugin/rules/tanstack-start/tanstack-start-route-property-order.ts
34453
34455
  const tanstackStartRoutePropertyOrder = defineRule({
34454
34456
  id: "tanstack-start-route-property-order",
34455
- title: "Wrong route property order",
34457
+ title: "Route property order breaks type inference",
34456
34458
  tags: ["test-noise"],
34457
34459
  requires: ["tanstack-start"],
34458
34460
  severity: "error",
34459
- recommendation: "Follow the order: params/validateSearch loaderDeps context beforeLoad loader head. See https://tanstack.com/router/latest/docs/eslint/create-route-property-order",
34461
+ recommendation: "Follow the route property order because TanStack Router's type inference depends on earlier properties feeding later ones.",
34460
34462
  create: (context) => ({ CallExpression(node) {
34461
34463
  const optionsObject = getRouteOptionsObject(node);
34462
34464
  if (!optionsObject) return;
@@ -34486,7 +34488,7 @@ const tanstackStartRoutePropertyOrder = defineRule({
34486
34488
  //#region src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts
34487
34489
  const tanstackStartServerFnMethodOrder = defineRule({
34488
34490
  id: "tanstack-start-server-fn-method-order",
34489
- title: "Wrong server function method order",
34491
+ title: "Server function method order breaks type inference",
34490
34492
  tags: ["test-noise"],
34491
34493
  requires: ["tanstack-start"],
34492
34494
  severity: "error",
@@ -34567,7 +34569,7 @@ const useLazyMotion = defineRule({
34567
34569
  return getImportedName$1(specifier) === "motion";
34568
34570
  })) context.report({
34569
34571
  node,
34570
- message: "Importing \"motion\" ships about 30 kb of extra code to your users & slows page load. Use \"m\" with LazyMotion instead."
34572
+ message: "Importing \"motion\" ships about 30 kb of extra code and slows page load. Use \"m\" with LazyMotion instead."
34571
34573
  });
34572
34574
  } })
34573
34575
  });
@@ -34604,7 +34606,7 @@ const voidDomElementsNoChildren = defineRule({
34604
34606
  id: "void-dom-elements-no-children",
34605
34607
  title: "Children on a void element",
34606
34608
  severity: "warn",
34607
- recommendation: "Remove the children, or use a tag that can hold children.",
34609
+ recommendation: "Remove the children or use a non-void tag so React does not drop content the element cannot render.",
34608
34610
  create: (context) => ({
34609
34611
  JSXElement(node) {
34610
34612
  const openingElement = node.openingElement;
@@ -34752,7 +34754,7 @@ const DEPRECATED_ZOD_ERROR_MEMBERS = new Set([
34752
34754
  "formErrors",
34753
34755
  "format"
34754
34756
  ]);
34755
- const ZOD_ERROR_API_MESSAGE = "Zod 4 dropped this ZodError method, so it breaks when you upgrade.";
34757
+ const ZOD_ERROR_API_MESSAGE = "This ZodError API was removed in Zod 4, so error handling can break during the upgrade.";
34756
34758
  const isZodErrorReference = (node) => {
34757
34759
  const inner = stripParenExpression(node);
34758
34760
  if (isNodeOfType(inner, "Identifier")) return getZodNamedImport(inner) === "ZodError";
@@ -34784,7 +34786,7 @@ const isReceiverOfDeprecatedZodErrorMember = (callExpression) => {
34784
34786
  };
34785
34787
  const zodV4NoDeprecatedErrorApis = defineRule({
34786
34788
  id: "zod-v4-no-deprecated-error-apis",
34787
- title: "Deprecated Zod error API",
34789
+ title: "Zod 3 error API breaks in Zod 4",
34788
34790
  requires: ["zod:4"],
34789
34791
  tags: ["migration-hint"],
34790
34792
  severity: "warn",
@@ -34873,7 +34875,7 @@ const parseCallUsesErrorMap = (callExpression) => {
34873
34875
  };
34874
34876
  const zodV4NoDeprecatedErrorCustomization = defineRule({
34875
34877
  id: "zod-v4-no-deprecated-error-customization",
34876
- title: "Deprecated Zod error customization",
34878
+ title: "Zod 3 error customization breaks in Zod 4",
34877
34879
  requires: ["zod:4"],
34878
34880
  tags: ["migration-hint"],
34879
34881
  severity: "warn",
@@ -34882,7 +34884,7 @@ const zodV4NoDeprecatedErrorCustomization = defineRule({
34882
34884
  if (!factoryUsesDeprecatedErrorParameter(node) && !parseCallUsesErrorMap(node)) return;
34883
34885
  context.report({
34884
34886
  node,
34885
- message: "Zod 4 changed how you customize error messages, so this breaks when you upgrade."
34887
+ message: "This Zod 3 error-customization form is not compatible with Zod 4, so custom messages can stop applying during the upgrade."
34886
34888
  });
34887
34889
  } })
34888
34890
  });
@@ -34942,7 +34944,7 @@ const LITERAL_FACTORY = new Set(["literal"]);
34942
34944
  const reportSchemaMigration = (context, node) => {
34943
34945
  context.report({
34944
34946
  node,
34945
- message: "Zod 4 deprecated or changed this API, so it breaks when you upgrade."
34947
+ message: "This Zod 3 schema API changed in Zod 4, so this schema can fail after the upgrade."
34946
34948
  });
34947
34949
  };
34948
34950
  const isCallToDeprecatedTopLevelFactory = (callExpression) => isZodFactoryCall(callExpression, DEPRECATED_TOP_LEVEL_FACTORIES);
@@ -34994,7 +34996,7 @@ const isZodNamespaceImportMemberCreate = (memberExpression) => {
34994
34996
  };
34995
34997
  const zodV4NoDeprecatedSchemaApis = defineRule({
34996
34998
  id: "zod-v4-no-deprecated-schema-apis",
34997
- title: "Deprecated Zod schema API",
34999
+ title: "Zod 3 schema API breaks in Zod 4",
34998
35000
  requires: ["zod:4"],
34999
35001
  tags: ["migration-hint"],
35000
35002
  severity: "warn",
@@ -35047,7 +35049,7 @@ const zodV4PreferTopLevelStringFormats = defineRule({
35047
35049
  if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
35048
35050
  context.report({
35049
35051
  node,
35050
- message: "Zod 4 deprecated format methods on `z.string()`, so they break when you upgrade."
35052
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
35051
35053
  });
35052
35054
  } })
35053
35055
  });