oxlint-plugin-react-doctor 0.4.2-dev.772385f → 0.4.2-dev.773e235
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.
- package/dist/index.js +341 -313
- 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
|
|
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
|
|
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 = "
|
|
1373
|
-
const MISSING_ALT_VALUE = "
|
|
1374
|
-
const ARIA_LABEL_VALUE = "
|
|
1375
|
-
const ARIA_LABELLEDBY_VALUE = "
|
|
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 = "
|
|
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) => `
|
|
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
|
|
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: "
|
|
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
|
|
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`
|
|
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
|
|
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}
|
|
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
|
|
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}
|
|
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
|
|
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 \"…\"
|
|
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: "
|
|
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
|
|
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
|
|
5251
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
5207
5252
|
context.report({
|
|
5208
5253
|
node,
|
|
5209
|
-
message: `\`${firstUsage.resourceName}
|
|
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
|
|
5858
|
-
const buildLiteralDepMessage = (hookName) => `A literal in \`${hookName}\`'s dependency array never changes
|
|
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
|
|
5864
|
-
const buildSpreadDepMessage = (hookName) =>
|
|
5865
|
-
const buildComplexDepMessage = (hookName) =>
|
|
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
|
|
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: "
|
|
6822
|
+
title: "Blocked component prop bypasses API contract",
|
|
6778
6823
|
severity: "warn",
|
|
6779
6824
|
defaultEnabled: false,
|
|
6780
|
-
recommendation: "
|
|
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: "
|
|
6874
|
+
title: "Blocked DOM prop bypasses project contract",
|
|
6830
6875
|
severity: "warn",
|
|
6831
|
-
recommendation: "
|
|
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: "
|
|
6949
|
+
title: "Blocked element bypasses approved UI primitives",
|
|
6905
6950
|
severity: "warn",
|
|
6906
|
-
recommendation: "
|
|
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: "
|
|
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 = "
|
|
7012
|
-
const NAMING_CONVENTION_MESSAGE = "
|
|
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
|
|
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 = "
|
|
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`
|
|
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=\"\"`
|
|
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
|
|
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: "
|
|
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
|
|
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 = () =>
|
|
8941
|
-
const ALWAYS_MESSAGE$2 = () =>
|
|
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: "
|
|
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
|
|
9013
|
-
const REQUIRED_BRACES_MESSAGE = "This value needs
|
|
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: "
|
|
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
|
|
9193
|
-
const EXTENSION_ONLY_FOR_JSX = (extension) => `\`${extension}\` files are
|
|
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
|
|
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 = "
|
|
9269
|
-
const ELEMENT_MESSAGE = "
|
|
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: "
|
|
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}"
|
|
9308
|
-
const buildHandlerPropMessage = (propKey, propValue) => `The prop "${propKey}" passes
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12840
|
+
title: "Plain img ships unoptimized images",
|
|
12839
12841
|
tags: ["test-noise"],
|
|
12840
12842
|
requires: ["nextjs"],
|
|
12841
12843
|
severity: "warn",
|
|
12842
|
-
recommendation: "`
|
|
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
|
|
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
|
|
12860
|
+
title: "Plain script can block Next.js rendering",
|
|
12859
12861
|
tags: ["test-noise"],
|
|
12860
12862
|
requires: ["nextjs"],
|
|
12861
12863
|
severity: "warn",
|
|
12862
|
-
recommendation: "`
|
|
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>
|
|
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
|
|
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
|
|
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()
|
|
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\"
|
|
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: "
|
|
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
|
|
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 = "
|
|
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
|
|
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 = "
|
|
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
|
|
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`
|
|
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: "
|
|
15572
|
+
title: "cloneElement makes child props fragile",
|
|
15571
15573
|
severity: "warn",
|
|
15572
15574
|
defaultEnabled: false,
|
|
15573
|
-
recommendation: "Pass children
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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 = "
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
18194
|
+
title: "findDOMNode breaks component encapsulation",
|
|
18193
18195
|
severity: "warn",
|
|
18194
|
-
recommendation: "Use a ref
|
|
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: "
|
|
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: "
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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: `
|
|
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: "
|
|
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
|
|
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
|
|
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: `
|
|
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: `
|
|
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: "
|
|
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
|
|
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) => `
|
|
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: `
|
|
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`
|
|
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: "
|
|
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
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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.",
|
|
@@ -21473,12 +21475,22 @@ const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
|
21473
21475
|
/^public-token-(?:live|test)-/,
|
|
21474
21476
|
/^pk\.eyJ/
|
|
21475
21477
|
];
|
|
21478
|
+
const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
21479
|
+
/^[\s._\-*\u2022xX]{8,}$/,
|
|
21480
|
+
/(?:\.{3,}|\u2026|[*\u2022]{3,})/,
|
|
21481
|
+
/(?:^|[_\-\s])(?:your|redacted|masked|placeholder|replace[_\-\s]?me|changeme)(?:$|[_\-\s])/i,
|
|
21482
|
+
/<[^>]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^>]*>/i,
|
|
21483
|
+
/\[[^\]]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^\]]*\]/i,
|
|
21484
|
+
/\{[^}]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^}]*\}/i
|
|
21485
|
+
];
|
|
21486
|
+
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
21487
|
+
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
21476
21488
|
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
21477
21489
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
21478
21490
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
21479
21491
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
21480
21492
|
const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
|
|
21481
|
-
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/;
|
|
21493
|
+
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
|
|
21482
21494
|
const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
|
|
21483
21495
|
const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
|
|
21484
21496
|
const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
|
|
@@ -21758,6 +21770,15 @@ const isInsideServerOnlyScope = (node) => {
|
|
|
21758
21770
|
return false;
|
|
21759
21771
|
};
|
|
21760
21772
|
//#endregion
|
|
21773
|
+
//#region src/plugin/utils/is-placeholder-secret-value.ts
|
|
21774
|
+
const isPlaceholderSecretValue = (literalValue, options) => {
|
|
21775
|
+
const trimmedValue = literalValue.trim();
|
|
21776
|
+
if (trimmedValue.length === 0) return false;
|
|
21777
|
+
if (SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue))) return true;
|
|
21778
|
+
if (!options.allowContextualExamples) return false;
|
|
21779
|
+
return SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS.some((pattern) => pattern.test(trimmedValue));
|
|
21780
|
+
};
|
|
21781
|
+
//#endregion
|
|
21761
21782
|
//#region src/plugin/rules/security/no-secrets-in-client-code.ts
|
|
21762
21783
|
const noSecretsInClientCode = defineRule({
|
|
21763
21784
|
id: "no-secrets-in-client-code",
|
|
@@ -21786,21 +21807,28 @@ const noSecretsInClientCode = defineRule({
|
|
|
21786
21807
|
if (!isNodeOfType(node.init, "Literal") || typeof node.init.value !== "string") return;
|
|
21787
21808
|
const variableName = node.id.name;
|
|
21788
21809
|
const literalValue = node.init.value;
|
|
21810
|
+
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
21811
|
+
const hasPlaceholderContext = SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(variableName) || componentOrHookName !== null && SECRET_PLACEHOLDER_CONTEXT_PATTERN.test(componentOrHookName);
|
|
21812
|
+
const isUnambiguousPlaceholderValue = isPlaceholderSecretValue(literalValue, { allowContextualExamples: false });
|
|
21813
|
+
const isPlaceholderValueForVariableHeuristic = isPlaceholderSecretValue(literalValue, { allowContextualExamples: hasPlaceholderContext });
|
|
21789
21814
|
if (PUBLIC_CLIENT_KEY_PATTERNS.some((pattern) => pattern.test(literalValue))) return;
|
|
21815
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) {
|
|
21816
|
+
if (!isUnambiguousPlaceholderValue) context.report({
|
|
21817
|
+
node,
|
|
21818
|
+
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21819
|
+
});
|
|
21820
|
+
return;
|
|
21821
|
+
}
|
|
21790
21822
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
21791
21823
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
21792
21824
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
21793
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && literalValue.length > 24) {
|
|
21825
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPlaceholderValueForVariableHeuristic && literalValue.length > 24) {
|
|
21794
21826
|
context.report({
|
|
21795
21827
|
node,
|
|
21796
21828
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
21797
21829
|
});
|
|
21798
21830
|
return;
|
|
21799
21831
|
}
|
|
21800
|
-
if (SECRET_PATTERNS.some((pattern) => pattern.test(literalValue))) context.report({
|
|
21801
|
-
node,
|
|
21802
|
-
message: "This hardcoded secret is a security vulnerability: it ships to the browser where anyone can read it."
|
|
21803
|
-
});
|
|
21804
21832
|
}
|
|
21805
21833
|
};
|
|
21806
21834
|
}
|
|
@@ -22143,7 +22171,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
22143
22171
|
reportedStateNames.add(stateName);
|
|
22144
22172
|
context.report({
|
|
22145
22173
|
node: setterCall,
|
|
22146
|
-
message: `${setterCall.callee.name}()
|
|
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.`
|
|
22147
22175
|
});
|
|
22148
22176
|
}
|
|
22149
22177
|
}
|
|
@@ -22175,13 +22203,13 @@ const getParentComponent = (node) => {
|
|
|
22175
22203
|
};
|
|
22176
22204
|
//#endregion
|
|
22177
22205
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
22178
|
-
const MESSAGE$12 = "
|
|
22206
|
+
const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
22179
22207
|
const noSetState = defineRule({
|
|
22180
22208
|
id: "no-set-state",
|
|
22181
|
-
title: "
|
|
22209
|
+
title: "Local class state forbidden",
|
|
22182
22210
|
severity: "warn",
|
|
22183
22211
|
defaultEnabled: false,
|
|
22184
|
-
recommendation: "Lift state up or use an external store
|
|
22212
|
+
recommendation: "Lift state up or use an external store so class-local state does not hide ownership.",
|
|
22185
22213
|
category: "Architecture",
|
|
22186
22214
|
create: (context) => ({ CallExpression(node) {
|
|
22187
22215
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
@@ -22223,7 +22251,7 @@ const noSetStateInRender = defineRule({
|
|
|
22223
22251
|
const setterIdentifierName = setterCall.callee.name;
|
|
22224
22252
|
context.report({
|
|
22225
22253
|
node: setterCall,
|
|
22226
|
-
message: `${setterIdentifierName}()
|
|
22254
|
+
message: `${setterIdentifierName}() triggers another render while rendering. Move it to an effect or event handler, or compute the value during render.`
|
|
22227
22255
|
});
|
|
22228
22256
|
}
|
|
22229
22257
|
};
|
|
@@ -22457,9 +22485,9 @@ const resolveSettings$11 = (settings) => {
|
|
|
22457
22485
|
};
|
|
22458
22486
|
const noStringRefs = defineRule({
|
|
22459
22487
|
id: "no-string-refs",
|
|
22460
|
-
title: "
|
|
22488
|
+
title: "String refs are legacy and fragile",
|
|
22461
22489
|
severity: "warn",
|
|
22462
|
-
recommendation: "Use a callback ref
|
|
22490
|
+
recommendation: "Use a callback ref or `useRef` so ref ownership is explicit and not tied to legacy string lookup.",
|
|
22463
22491
|
create: (context) => {
|
|
22464
22492
|
const { noTemplateLiterals = false } = resolveSettings$11(context.settings);
|
|
22465
22493
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
@@ -22538,7 +22566,7 @@ const noThisInSfc = defineRule({
|
|
|
22538
22566
|
id: "no-this-in-sfc",
|
|
22539
22567
|
title: "this used in function component",
|
|
22540
22568
|
severity: "warn",
|
|
22541
|
-
recommendation: "Read from the `props` argument
|
|
22569
|
+
recommendation: "Read from the `props` argument because function components do not have a React instance `this`.",
|
|
22542
22570
|
create: (context) => {
|
|
22543
22571
|
const configured = (context.settings?.react)?.createClass;
|
|
22544
22572
|
const customClassFactoryNames = /* @__PURE__ */ new Set();
|
|
@@ -22682,10 +22710,10 @@ const noUncontrolledInput = defineRule({
|
|
|
22682
22710
|
const hasAllowedPartner = VALUE_PARTNER_ATTRIBUTES.some((partnerAttributeName) => findJsxAttribute(attributes, partnerAttributeName));
|
|
22683
22711
|
if (isNodeOfType(valueAttribute.value, "JSXExpressionContainer") && isNodeOfType(valueAttribute.value.expression, "Identifier") && undefinedInitialStateNames.has(valueAttribute.value.expression.name)) {
|
|
22684
22712
|
const stateName = valueAttribute.value.expression.name;
|
|
22685
|
-
const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value
|
|
22713
|
+
const partnerHint = hasAllowedPartner ? "Give useState a starting value" : "Give useState a starting value and add onChange (or readOnly)";
|
|
22686
22714
|
context.report({
|
|
22687
22715
|
node: child,
|
|
22688
|
-
message: `
|
|
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("")\`).`
|
|
22689
22717
|
});
|
|
22690
22718
|
return;
|
|
22691
22719
|
}
|
|
@@ -22747,7 +22775,7 @@ const noUnescapedEntities = defineRule({
|
|
|
22747
22775
|
title: "Unescaped entities in JSX",
|
|
22748
22776
|
severity: "warn",
|
|
22749
22777
|
defaultEnabled: false,
|
|
22750
|
-
recommendation: "Replace bare `'` / `\"` / `>` / `}` characters
|
|
22778
|
+
recommendation: "Replace bare `'` / `\"` / `>` / `}` characters with HTML entities so literal UI text is encoded consistently.",
|
|
22751
22779
|
create: (context) => ({ JSXText(node) {
|
|
22752
22780
|
const value = node.value;
|
|
22753
22781
|
for (const character of value) if (character in ESCAPED_VERSIONS) {
|
|
@@ -23682,7 +23710,7 @@ const noUnknownProperty = defineRule({
|
|
|
23682
23710
|
id: "no-unknown-property",
|
|
23683
23711
|
title: "Unknown DOM property",
|
|
23684
23712
|
severity: "warn",
|
|
23685
|
-
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.",
|
|
23686
23714
|
create: (context) => {
|
|
23687
23715
|
const { ignore = [], requireDataLowercase = false } = resolveSettings$10(context.settings);
|
|
23688
23716
|
const ignoreSet = new Set(ignore);
|
|
@@ -23768,7 +23796,7 @@ const UNSAFE_ALIASES = new Set([
|
|
|
23768
23796
|
"componentWillReceiveProps",
|
|
23769
23797
|
"componentWillUpdate"
|
|
23770
23798
|
]);
|
|
23771
|
-
const buildMessage$6 = (methodName) => `\`${methodName}\`
|
|
23799
|
+
const buildMessage$6 = (methodName) => `\`${methodName}\` runs during unsafe legacy render timing and is deprecated, so React may double-invoke or remove it.`;
|
|
23772
23800
|
const resolveSettings$9 = (settings) => {
|
|
23773
23801
|
const reactDoctor = settings?.["react-doctor"];
|
|
23774
23802
|
return { checkAliases: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noUnsafe ?? {} : {}).checkAliases ?? false };
|
|
@@ -23801,7 +23829,7 @@ const noUnsafe = defineRule({
|
|
|
23801
23829
|
id: "no-unsafe",
|
|
23802
23830
|
title: "Unsafe legacy lifecycle method",
|
|
23803
23831
|
severity: "warn",
|
|
23804
|
-
recommendation: "
|
|
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.",
|
|
23805
23833
|
create: (context) => {
|
|
23806
23834
|
const { checkAliases } = resolveSettings$9(context.settings);
|
|
23807
23835
|
const flagsUnsafePrefix = isReactVersionAtLeast(getConfiguredReactMajorMinor(context.settings), 16, 3);
|
|
@@ -24057,7 +24085,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
24057
24085
|
id: "no-unstable-nested-components",
|
|
24058
24086
|
title: "Component defined inside a component",
|
|
24059
24087
|
severity: "warn",
|
|
24060
|
-
recommendation: "Move nested components to
|
|
24088
|
+
recommendation: "Move nested components to module scope so React does not remount them and lose state on every render.",
|
|
24061
24089
|
category: "Performance",
|
|
24062
24090
|
create: (context) => {
|
|
24063
24091
|
const settings = resolveSettings$8(context.settings);
|
|
@@ -24231,7 +24259,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
24231
24259
|
//#endregion
|
|
24232
24260
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
24233
24261
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
24234
|
-
const MESSAGE$9 = "
|
|
24262
|
+
const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
24235
24263
|
const resolveSettings$7 = (settings) => {
|
|
24236
24264
|
const reactDoctor = settings?.["react-doctor"];
|
|
24237
24265
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -24254,7 +24282,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
24254
24282
|
id: "no-will-update-set-state",
|
|
24255
24283
|
title: "setState in componentWillUpdate",
|
|
24256
24284
|
severity: "warn",
|
|
24257
|
-
recommendation: "
|
|
24285
|
+
recommendation: "Avoid setState in componentWillUpdate because it can loop forever; derive state before render or move guarded updates to componentDidUpdate.",
|
|
24258
24286
|
create: (context) => {
|
|
24259
24287
|
const { mode } = resolveSettings$7(context.settings);
|
|
24260
24288
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
@@ -24288,7 +24316,7 @@ const noZIndex9999 = defineRule({
|
|
|
24288
24316
|
const zValue = getStylePropertyNumberValue(property);
|
|
24289
24317
|
if (zValue !== null && Math.abs(zValue) >= 1e3) context.report({
|
|
24290
24318
|
node: property,
|
|
24291
|
-
message: `z-index ${zValue} is
|
|
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.`
|
|
24292
24320
|
});
|
|
24293
24321
|
}
|
|
24294
24322
|
},
|
|
@@ -24471,12 +24499,12 @@ const UTILITY_FILE_BASENAMES = new Set([
|
|
|
24471
24499
|
]);
|
|
24472
24500
|
//#endregion
|
|
24473
24501
|
//#region src/plugin/rules/react-builtins/only-export-components.ts
|
|
24474
|
-
const NAMED_EXPORT_MESSAGE = "Fast Refresh
|
|
24475
|
-
const ANONYMOUS_MESSAGE = "Fast Refresh can't track
|
|
24476
|
-
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh
|
|
24477
|
-
const REACT_CONTEXT_MESSAGE = "
|
|
24478
|
-
const LOCAL_COMPONENT_MESSAGE = "Fast Refresh skips
|
|
24479
|
-
const NO_EXPORT_MESSAGE = "Fast Refresh can't track
|
|
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.";
|
|
24480
24508
|
const DEFAULT_REACT_HOCS = [
|
|
24481
24509
|
"memo",
|
|
24482
24510
|
"forwardRef",
|
|
@@ -24629,7 +24657,7 @@ const onlyExportComponents = defineRule({
|
|
|
24629
24657
|
id: "only-export-components",
|
|
24630
24658
|
title: "Non-component export in component file",
|
|
24631
24659
|
severity: "warn",
|
|
24632
|
-
recommendation: "Move non-component exports out of files
|
|
24660
|
+
recommendation: "Move non-component exports out of component files so Fast Refresh can preserve component state instead of full-reloading.",
|
|
24633
24661
|
category: "Architecture",
|
|
24634
24662
|
create: (context) => {
|
|
24635
24663
|
const settings = resolveSettings$6(context.settings);
|
|
@@ -24850,10 +24878,10 @@ const isChildrenMemberExpression = (node) => {
|
|
|
24850
24878
|
};
|
|
24851
24879
|
const preactNoChildrenLength = defineRule({
|
|
24852
24880
|
id: "preact-no-children-length",
|
|
24853
|
-
title: "Array methods on
|
|
24881
|
+
title: "Array methods on Preact children can crash",
|
|
24854
24882
|
requires: ["preact"],
|
|
24855
24883
|
severity: "warn",
|
|
24856
|
-
recommendation: "Wrap with `toChildArray(children)`
|
|
24884
|
+
recommendation: "Wrap with `toChildArray(children)` because Preact's `props.children` is not always an array and array methods can crash.",
|
|
24857
24885
|
create: (context) => ({ MemberExpression(node) {
|
|
24858
24886
|
if (node.computed) return;
|
|
24859
24887
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
@@ -24887,10 +24915,10 @@ const REACT_HOOK_NAMES = new Set([
|
|
|
24887
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.`;
|
|
24888
24916
|
const preactNoReactHooksImport = defineRule({
|
|
24889
24917
|
id: "preact-no-react-hooks-import",
|
|
24890
|
-
title: "
|
|
24918
|
+
title: "React hook imports break pure Preact hook state",
|
|
24891
24919
|
requires: ["pure-preact"],
|
|
24892
24920
|
severity: "warn",
|
|
24893
|
-
recommendation: "
|
|
24921
|
+
recommendation: "Import hooks from `preact/hooks` so they share Preact's renderer state instead of loading a second hook implementation.",
|
|
24894
24922
|
create: (context) => ({ ImportDeclaration(node) {
|
|
24895
24923
|
const source = node.source;
|
|
24896
24924
|
if (!isNodeOfType(source, "Literal") || source.value !== "react") return;
|
|
@@ -24950,7 +24978,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
24950
24978
|
title: "render() reads props from arguments",
|
|
24951
24979
|
requires: ["preact"],
|
|
24952
24980
|
severity: "warn",
|
|
24953
|
-
recommendation: "Read
|
|
24981
|
+
recommendation: "Read from `this.props` and `this.state` because `preact/compat` uses React's parameterless `render()` and positional props/state become undefined.",
|
|
24954
24982
|
create: (context) => ({ MethodDefinition(node) {
|
|
24955
24983
|
if (!isInstanceMethodNamedRender(node)) return;
|
|
24956
24984
|
if (!isInsideEs6Component$1(node)) return;
|
|
@@ -24972,7 +25000,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
24972
25000
|
title: "onDoubleClick instead of onDblClick",
|
|
24973
25001
|
requires: ["pure-preact"],
|
|
24974
25002
|
severity: "warn",
|
|
24975
|
-
recommendation: "Rename
|
|
25003
|
+
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
24976
25004
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
24977
25005
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24978
25006
|
const tagName = node.name.name;
|
|
@@ -25044,8 +25072,8 @@ const preferDynamicImport = defineRule({
|
|
|
25044
25072
|
});
|
|
25045
25073
|
//#endregion
|
|
25046
25074
|
//#region src/plugin/rules/react-builtins/prefer-es6-class.ts
|
|
25047
|
-
const ALWAYS_MESSAGE$1 = "`createReactClass` is legacy
|
|
25048
|
-
const NEVER_MESSAGE$1 = "This component is
|
|
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.";
|
|
25049
25077
|
const resolveSettings$5 = (settings) => {
|
|
25050
25078
|
const reactDoctor = settings?.["react-doctor"];
|
|
25051
25079
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
@@ -25056,7 +25084,7 @@ const preferEs6Class = defineRule({
|
|
|
25056
25084
|
title: "createClass instead of ES6 class",
|
|
25057
25085
|
severity: "warn",
|
|
25058
25086
|
defaultEnabled: false,
|
|
25059
|
-
recommendation: "Pick one component style
|
|
25087
|
+
recommendation: "Pick one component style so readers do not have to switch between legacy `createReactClass` patterns and modern class components.",
|
|
25060
25088
|
category: "Architecture",
|
|
25061
25089
|
create: (context) => {
|
|
25062
25090
|
const { mode = "always" } = resolveSettings$5(context.settings);
|
|
@@ -25215,7 +25243,7 @@ const preferExplicitVariants = defineRule({
|
|
|
25215
25243
|
});
|
|
25216
25244
|
//#endregion
|
|
25217
25245
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
25218
|
-
const MESSAGE$7 = "This class 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.";
|
|
25219
25247
|
const resolveSettings$4 = (settings) => {
|
|
25220
25248
|
const reactDoctor = settings?.["react-doctor"];
|
|
25221
25249
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -25241,7 +25269,7 @@ const preferFunctionComponent = defineRule({
|
|
|
25241
25269
|
title: "Class component instead of function",
|
|
25242
25270
|
severity: "warn",
|
|
25243
25271
|
defaultEnabled: false,
|
|
25244
|
-
recommendation: "
|
|
25272
|
+
recommendation: "Rewrite the class component as a function component so state and effects use modern hook patterns instead of class lifecycles.",
|
|
25245
25273
|
category: "Architecture",
|
|
25246
25274
|
create: (context) => {
|
|
25247
25275
|
const settings = resolveSettings$4(context.settings);
|
|
@@ -25593,7 +25621,7 @@ const preferTagOverRole = defineRule({
|
|
|
25593
25621
|
title: "Role used instead of HTML tag",
|
|
25594
25622
|
tags: ["react-jsx-only"],
|
|
25595
25623
|
severity: "warn",
|
|
25596
|
-
recommendation: "
|
|
25624
|
+
recommendation: "Use the matching HTML element when one exists so browsers and assistive tech get native semantics.",
|
|
25597
25625
|
category: "Accessibility",
|
|
25598
25626
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
25599
25627
|
const tag = getElementType(node, context.settings);
|
|
@@ -25899,7 +25927,7 @@ const preferUseReducer = defineRule({
|
|
|
25899
25927
|
title: "Many related useState calls",
|
|
25900
25928
|
tags: ["test-noise"],
|
|
25901
25929
|
severity: "warn",
|
|
25902
|
-
recommendation: "Group related state
|
|
25930
|
+
recommendation: "Group related state in `useReducer` so one logical update does not fan out into separate renders.",
|
|
25903
25931
|
create: (context) => {
|
|
25904
25932
|
const reportExcessiveUseState = (body, componentName) => {
|
|
25905
25933
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
@@ -25931,7 +25959,7 @@ const preferUseReducer = defineRule({
|
|
|
25931
25959
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
25932
25960
|
const queryDestructureResult = defineRule({
|
|
25933
25961
|
id: "query-destructure-result",
|
|
25934
|
-
title: "
|
|
25962
|
+
title: "Whole query result subscribes to every field",
|
|
25935
25963
|
tags: ["test-noise"],
|
|
25936
25964
|
requires: ["tanstack-query"],
|
|
25937
25965
|
severity: "error",
|
|
@@ -25984,7 +26012,7 @@ const queryNoQueryInEffect = defineRule({
|
|
|
25984
26012
|
tags: ["test-noise"],
|
|
25985
26013
|
requires: ["tanstack-query"],
|
|
25986
26014
|
severity: "warn",
|
|
25987
|
-
recommendation: "
|
|
26015
|
+
recommendation: "Use `queryKey` changes or `enabled` so React Query schedules the fetch once instead of refetching again from `useEffect`.",
|
|
25988
26016
|
create: (context) => ({ CallExpression(node) {
|
|
25989
26017
|
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25990
26018
|
const callback = getEffectCallback(node);
|
|
@@ -26307,7 +26335,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
26307
26335
|
severity: "warn",
|
|
26308
26336
|
category: "Performance",
|
|
26309
26337
|
disabledBy: ["react-compiler"],
|
|
26310
|
-
recommendation: "Select the raw slice and
|
|
26338
|
+
recommendation: "Select the raw slice and memoize derivation so Redux actions do not rebuild a collection and redraw this component.",
|
|
26311
26339
|
create: (context) => {
|
|
26312
26340
|
let aliases = /* @__PURE__ */ new Set();
|
|
26313
26341
|
return {
|
|
@@ -26367,7 +26395,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
26367
26395
|
severity: "warn",
|
|
26368
26396
|
category: "Performance",
|
|
26369
26397
|
disabledBy: ["react-compiler"],
|
|
26370
|
-
recommendation: "Return a
|
|
26398
|
+
recommendation: "Return a stable selected value, split selectors, or pass `shallowEqual` so every Redux action does not redraw this component.",
|
|
26371
26399
|
create: (context) => {
|
|
26372
26400
|
let aliases = /* @__PURE__ */ new Set();
|
|
26373
26401
|
return {
|
|
@@ -26393,7 +26421,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
26393
26421
|
title: "Animating an SVG directly",
|
|
26394
26422
|
tags: ["test-noise"],
|
|
26395
26423
|
severity: "warn",
|
|
26396
|
-
recommendation: "Wrap the SVG
|
|
26424
|
+
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
26397
26425
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
26398
26426
|
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
26399
26427
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
@@ -26543,7 +26571,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
26543
26571
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
26544
26572
|
context.report({
|
|
26545
26573
|
node,
|
|
26546
|
-
message: `This
|
|
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.`
|
|
26547
26575
|
});
|
|
26548
26576
|
return;
|
|
26549
26577
|
}
|
|
@@ -26552,7 +26580,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
26552
26580
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
26553
26581
|
context.report({
|
|
26554
26582
|
node: child,
|
|
26555
|
-
message: `This
|
|
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.`
|
|
26556
26584
|
});
|
|
26557
26585
|
return;
|
|
26558
26586
|
}
|
|
@@ -26831,7 +26859,7 @@ const requireRenderReturn = defineRule({
|
|
|
26831
26859
|
id: "require-render-return",
|
|
26832
26860
|
title: "Render method does not return",
|
|
26833
26861
|
severity: "error",
|
|
26834
|
-
recommendation: "Return JSX
|
|
26862
|
+
recommendation: "Return JSX or `null` from `render` so the component intentionally shows something or nothing.",
|
|
26835
26863
|
create: (context) => {
|
|
26836
26864
|
const checkFunction = (functionNode) => {
|
|
26837
26865
|
const host = resolveRenderHost(functionNode);
|
|
@@ -27153,7 +27181,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
27153
27181
|
tags: ["test-noise"],
|
|
27154
27182
|
severity: "warn",
|
|
27155
27183
|
category: "Performance",
|
|
27156
|
-
recommendation: "
|
|
27184
|
+
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
27157
27185
|
create: (context) => ({ CallExpression(node) {
|
|
27158
27186
|
if (!isHookCall$1(node, "useRef") || !node.arguments?.length) return;
|
|
27159
27187
|
const initializer = node.arguments[0];
|
|
@@ -27180,7 +27208,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
27180
27208
|
tags: ["test-noise"],
|
|
27181
27209
|
severity: "warn",
|
|
27182
27210
|
category: "Performance",
|
|
27183
|
-
recommendation: "Wrap in an arrow function so
|
|
27211
|
+
recommendation: "Wrap expensive initial state in an arrow function so the initializer does not rerun and get thrown away on every render.",
|
|
27184
27212
|
create: (context) => ({ CallExpression(node) {
|
|
27185
27213
|
if (!isHookCall$1(node, "useState") || !node.arguments?.length) return;
|
|
27186
27214
|
const initializer = node.arguments[0];
|
|
@@ -27463,7 +27491,7 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
27463
27491
|
}
|
|
27464
27492
|
context.report({
|
|
27465
27493
|
node: setStateCall,
|
|
27466
|
-
message: `This
|
|
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.`
|
|
27467
27495
|
});
|
|
27468
27496
|
} })
|
|
27469
27497
|
});
|
|
@@ -27483,7 +27511,7 @@ const rnAnimateLayoutProperty = defineRetiredRule({
|
|
|
27483
27511
|
tags: ["test-noise"],
|
|
27484
27512
|
requires: ["react-native"],
|
|
27485
27513
|
severity: "warn",
|
|
27486
|
-
recommendation: "Reanimated useAnimatedStyle
|
|
27514
|
+
recommendation: "Retired: Reanimated `useAnimatedStyle` can safely drive layout-affecting properties on the UI thread, so this pattern should not be flagged."
|
|
27487
27515
|
});
|
|
27488
27516
|
//#endregion
|
|
27489
27517
|
//#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
|
|
@@ -27514,7 +27542,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
27514
27542
|
if (!isValueAssignment && !isSetCall) return;
|
|
27515
27543
|
context.report({
|
|
27516
27544
|
node,
|
|
27517
|
-
message: "
|
|
27545
|
+
message: "This useAnimatedReaction only copies one shared value into another, so it can miss Reanimated's derived-value dependency tracking."
|
|
27518
27546
|
});
|
|
27519
27547
|
} })
|
|
27520
27548
|
});
|
|
@@ -27531,17 +27559,17 @@ const JS_BOTTOM_SHEET_PACKAGES = new Set([
|
|
|
27531
27559
|
]);
|
|
27532
27560
|
const rnBottomSheetPreferNative = defineRule({
|
|
27533
27561
|
id: "rn-bottom-sheet-prefer-native",
|
|
27534
|
-
title: "JS bottom sheet
|
|
27562
|
+
title: "JS bottom sheet misses native sheet behavior",
|
|
27535
27563
|
tags: ["test-noise"],
|
|
27536
27564
|
requires: ["react-native"],
|
|
27537
27565
|
severity: "warn",
|
|
27538
|
-
recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">`
|
|
27566
|
+
recommendation: "On RN v7+, use `<Modal presentationStyle=\"formSheet\">` so the sheet uses platform-native gestures, detents, accessibility, and presentation behavior.",
|
|
27539
27567
|
create: (context) => ({ ImportDeclaration(node) {
|
|
27540
27568
|
const source = node.source?.value;
|
|
27541
27569
|
if (typeof source !== "string" || !JS_BOTTOM_SHEET_PACKAGES.has(source)) return;
|
|
27542
27570
|
context.report({
|
|
27543
27571
|
node,
|
|
27544
|
-
message: `
|
|
27572
|
+
message: `Users get JS-driven sheet gestures and presentation with ${source}, instead of the platform-native formSheet behavior.`
|
|
27545
27573
|
});
|
|
27546
27574
|
} })
|
|
27547
27575
|
});
|
|
@@ -27627,13 +27655,13 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
27627
27655
|
if (root.calleeName === "waitFor") {
|
|
27628
27656
|
context.report({
|
|
27629
27657
|
node,
|
|
27630
|
-
message: "This Detox `waitFor
|
|
27658
|
+
message: "This Detox `waitFor` chain isn't awaited, so the test can continue before the condition settles. Prepend `await`."
|
|
27631
27659
|
});
|
|
27632
27660
|
return;
|
|
27633
27661
|
}
|
|
27634
27662
|
if (root.calleeName === "expect" && isDetoxExpectSubject(root.rootCall)) context.report({
|
|
27635
27663
|
node,
|
|
27636
|
-
message: "This Detox `expect(element
|
|
27664
|
+
message: "This Detox `expect(element)` assertion isn't awaited, so the test can pass or fail before the assertion settles. Prepend `await`."
|
|
27637
27665
|
});
|
|
27638
27666
|
} };
|
|
27639
27667
|
}
|
|
@@ -28264,7 +28292,7 @@ const rnNoLegacyExpoPackages = defineRule({
|
|
|
28264
28292
|
tags: ["test-noise"],
|
|
28265
28293
|
requires: ["react-native"],
|
|
28266
28294
|
severity: "warn",
|
|
28267
|
-
recommendation: "
|
|
28295
|
+
recommendation: "Switch to the maintained replacement package so users are not stuck with unfixed bugs in deprecated Expo packages.",
|
|
28268
28296
|
create: (context) => ({ ImportDeclaration(node) {
|
|
28269
28297
|
const source = node.source?.value;
|
|
28270
28298
|
if (typeof source !== "string") return;
|
|
@@ -28346,7 +28374,7 @@ const rnNoNonNativeNavigator = defineRule({
|
|
|
28346
28374
|
if (!NON_NATIVE_NAVIGATOR_PACKAGES.get(source)) return;
|
|
28347
28375
|
context.report({
|
|
28348
28376
|
node,
|
|
28349
|
-
message: `
|
|
28377
|
+
message: `Users get JS-driven transitions and gestures from ${source}, instead of platform-native navigation behavior.`
|
|
28350
28378
|
});
|
|
28351
28379
|
} })
|
|
28352
28380
|
});
|
|
@@ -28652,7 +28680,7 @@ const collectReturnedJsxElements = (expression) => {
|
|
|
28652
28680
|
};
|
|
28653
28681
|
const rnNoRenderitemKey = defineRule({
|
|
28654
28682
|
id: "rn-no-renderitem-key",
|
|
28655
|
-
title: "
|
|
28683
|
+
title: "renderItem key is ignored by React Native lists",
|
|
28656
28684
|
tags: ["test-noise"],
|
|
28657
28685
|
requires: ["react-native"],
|
|
28658
28686
|
severity: "warn",
|
|
@@ -28793,7 +28821,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
28793
28821
|
//#region src/plugin/rules/react-native/rn-no-single-element-style-array.ts
|
|
28794
28822
|
const rnNoSingleElementStyleArray = defineRule({
|
|
28795
28823
|
id: "rn-no-single-element-style-array",
|
|
28796
|
-
title: "Single-element style array",
|
|
28824
|
+
title: "Single-element style array adds wasted allocation",
|
|
28797
28825
|
tags: ["test-noise"],
|
|
28798
28826
|
requires: ["react-native"],
|
|
28799
28827
|
severity: "warn",
|
|
@@ -28816,11 +28844,11 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28816
28844
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28817
28845
|
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28818
28846
|
id: "rn-prefer-content-inset-adjustment",
|
|
28819
|
-
title: "Manual safe-area
|
|
28847
|
+
title: "Manual safe-area insets can duplicate offsets",
|
|
28820
28848
|
tags: ["test-noise"],
|
|
28821
28849
|
requires: ["react-native"],
|
|
28822
28850
|
severity: "warn",
|
|
28823
|
-
recommendation: "
|
|
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."
|
|
28824
28852
|
});
|
|
28825
28853
|
//#endregion
|
|
28826
28854
|
//#region src/react-native-dependency-names.ts
|
|
@@ -29005,7 +29033,7 @@ const rnPreferPressable = defineRule({
|
|
|
29005
29033
|
tags: ["test-noise"],
|
|
29006
29034
|
requires: ["react-native"],
|
|
29007
29035
|
severity: "warn",
|
|
29008
|
-
recommendation: "Use `<Pressable>`
|
|
29036
|
+
recommendation: "Use `<Pressable>` because Touchable* components are frozen and lack Pressable's state-based feedback and accessibility behavior.",
|
|
29009
29037
|
create: (context) => ({ ImportDeclaration(node) {
|
|
29010
29038
|
const source = node.source?.value;
|
|
29011
29039
|
if (typeof source !== "string" || !TOUCHABLE_SOURCES.has(source)) return;
|
|
@@ -29441,7 +29469,7 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
29441
29469
|
title: "Role missing required ARIA props",
|
|
29442
29470
|
tags: ["react-jsx-only"],
|
|
29443
29471
|
severity: "error",
|
|
29444
|
-
recommendation: "Add every required `aria-*` attribute
|
|
29472
|
+
recommendation: "Add every required `aria-*` attribute so assistive tech can expose the role's state correctly.",
|
|
29445
29473
|
category: "Accessibility",
|
|
29446
29474
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
29447
29475
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
@@ -32687,16 +32715,16 @@ const roleSupportsAriaProps = defineRule({
|
|
|
32687
32715
|
});
|
|
32688
32716
|
//#endregion
|
|
32689
32717
|
//#region src/plugin/rules/react-builtins/rules-of-hooks.ts
|
|
32690
|
-
const buildTopLevelMessage = (hookName) => `\`${hookName}\`
|
|
32691
|
-
const buildNonComponentMessage = (hookName, functionName) => `\`${hookName}\`
|
|
32692
|
-
const buildConditionalMessage = (hookName) => `\`${hookName}\`
|
|
32693
|
-
const buildLoopMessage = (hookName) => `\`${hookName}\`
|
|
32694
|
-
const buildAsyncMessage = (hookName) => `\`${hookName}\`
|
|
32695
|
-
const buildClassComponentMessage = (hookName) => `\`${hookName}\`
|
|
32696
|
-
const buildTryMessage = (hookName) => `\`${hookName}\`
|
|
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.`;
|
|
32697
32725
|
const buildEffectEventCallMessage = (bindingName) => `\`${bindingName}\` comes from useEffectEvent, so it only works when called from Effects in the same component.`;
|
|
32698
32726
|
const buildEffectEventAssignmentMessage = (bindingName) => `${buildEffectEventCallMessage(bindingName)} It also breaks if saved in a variable or passed around.`;
|
|
32699
|
-
const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent breaks
|
|
32727
|
+
const buildEffectEventPassedDownMessage = () => `A function from useEffectEvent only works inside Effects in the same component, so passing it around breaks the event/dependency split.`;
|
|
32700
32728
|
const ASCII_UPPERCASE_A = 65;
|
|
32701
32729
|
const ASCII_UPPERCASE_Z = 90;
|
|
32702
32730
|
const EFFECT_HOOK_NAMES = new Set([
|
|
@@ -32949,7 +32977,7 @@ const rulesOfHooks = defineRule({
|
|
|
32949
32977
|
title: "Hook called conditionally",
|
|
32950
32978
|
severity: "error",
|
|
32951
32979
|
tags: ["test-noise"],
|
|
32952
|
-
recommendation: "Call hooks at the top level of a React function component or
|
|
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.",
|
|
32953
32981
|
category: "Correctness",
|
|
32954
32982
|
create: (context) => {
|
|
32955
32983
|
const settings = resolveSettings$3(context.settings);
|
|
@@ -33077,13 +33105,13 @@ const rulesOfHooks = defineRule({
|
|
|
33077
33105
|
});
|
|
33078
33106
|
//#endregion
|
|
33079
33107
|
//#region src/plugin/rules/a11y/scope.ts
|
|
33080
|
-
const MESSAGE$3 = "
|
|
33108
|
+
const MESSAGE$3 = "The `scope` attribute only works on `<th>` cells, so screen readers get no table-header help from it here.";
|
|
33081
33109
|
const scope = defineRule({
|
|
33082
33110
|
id: "scope",
|
|
33083
33111
|
title: "scope attribute on non-th element",
|
|
33084
33112
|
tags: ["react-jsx-only"],
|
|
33085
33113
|
severity: "warn",
|
|
33086
|
-
recommendation: "
|
|
33114
|
+
recommendation: "Remove `scope` from this element or move it to the related `<th>` cell.",
|
|
33087
33115
|
category: "Accessibility",
|
|
33088
33116
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
33089
33117
|
const scopeAttribute = hasJsxProp(node.attributes, "scope");
|
|
@@ -33098,7 +33126,7 @@ const scope = defineRule({
|
|
|
33098
33126
|
});
|
|
33099
33127
|
//#endregion
|
|
33100
33128
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
33101
|
-
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.";
|
|
33102
33130
|
const resolveSettings$2 = (settings) => {
|
|
33103
33131
|
const reactDoctor = settings?.["react-doctor"];
|
|
33104
33132
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.selfClosingComp ?? {} : {};
|
|
@@ -33117,7 +33145,7 @@ const selfClosingComp = defineRule({
|
|
|
33117
33145
|
title: "Element not self-closing",
|
|
33118
33146
|
severity: "warn",
|
|
33119
33147
|
defaultEnabled: false,
|
|
33120
|
-
recommendation: "Use
|
|
33148
|
+
recommendation: "Use `<X />` for childless elements so empty closing tags do not add noise.",
|
|
33121
33149
|
category: "Architecture",
|
|
33122
33150
|
create: (context) => {
|
|
33123
33151
|
const settings = resolveSettings$2(context.settings);
|
|
@@ -33314,9 +33342,9 @@ const getCandidateFromDefaultDeclaration = (node) => {
|
|
|
33314
33342
|
};
|
|
33315
33343
|
const serverAuthActions = defineRule({
|
|
33316
33344
|
id: "server-auth-actions",
|
|
33317
|
-
title: "
|
|
33345
|
+
title: "Unauthenticated server action can be called directly",
|
|
33318
33346
|
severity: "error",
|
|
33319
|
-
recommendation: "
|
|
33347
|
+
recommendation: "Check auth before touching data because exported server actions can be called directly by unauthenticated clients.",
|
|
33320
33348
|
create: (context) => {
|
|
33321
33349
|
let fileHasUseServerDirective = false;
|
|
33322
33350
|
const customAuthFunctionNames = getReactDoctorStringArraySetting(context.settings, "serverAuthFunctionNames");
|
|
@@ -33454,7 +33482,7 @@ const objectExpressionHasNextRevalidate = (objectExpression) => {
|
|
|
33454
33482
|
}
|
|
33455
33483
|
return false;
|
|
33456
33484
|
};
|
|
33457
|
-
const APP_ROUTER_FILE_PATTERN =
|
|
33485
|
+
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
33458
33486
|
const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
|
|
33459
33487
|
const serverFetchWithoutRevalidate = defineRule({
|
|
33460
33488
|
id: "server-fetch-without-revalidate",
|
|
@@ -33697,8 +33725,8 @@ const serverSequentialIndependentAwait = defineRule({
|
|
|
33697
33725
|
});
|
|
33698
33726
|
//#endregion
|
|
33699
33727
|
//#region src/plugin/rules/react-builtins/state-in-constructor.ts
|
|
33700
|
-
const ALWAYS_MESSAGE = "This
|
|
33701
|
-
const NEVER_MESSAGE = "This
|
|
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.";
|
|
33702
33730
|
const resolveSettings$1 = (settings) => {
|
|
33703
33731
|
const reactDoctor = settings?.["react-doctor"];
|
|
33704
33732
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.stateInConstructor ?? {} : {}).mode ?? "always" };
|
|
@@ -33730,7 +33758,7 @@ const stateInConstructor = defineRule({
|
|
|
33730
33758
|
title: "State initialized in constructor",
|
|
33731
33759
|
severity: "warn",
|
|
33732
33760
|
defaultEnabled: false,
|
|
33733
|
-
recommendation: "
|
|
33761
|
+
recommendation: "Use one class-state setup pattern so readers know where initial state lives.",
|
|
33734
33762
|
category: "Architecture",
|
|
33735
33763
|
create: (context) => {
|
|
33736
33764
|
const { mode } = resolveSettings$1(context.settings);
|
|
@@ -33833,7 +33861,7 @@ const stylePropObject = defineRule({
|
|
|
33833
33861
|
id: "style-prop-object",
|
|
33834
33862
|
title: "Style prop is not an object",
|
|
33835
33863
|
severity: "warn",
|
|
33836
|
-
recommendation: "Pass
|
|
33864
|
+
recommendation: "Pass `style` as an object so React can apply CSS properties instead of ignoring a string style value.",
|
|
33837
33865
|
category: "Correctness",
|
|
33838
33866
|
create: (context) => {
|
|
33839
33867
|
const { allow } = resolveSettings(context.settings);
|
|
@@ -34179,11 +34207,11 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
34179
34207
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-anchor-element.ts
|
|
34180
34208
|
const tanstackStartNoAnchorElement = defineRule({
|
|
34181
34209
|
id: "tanstack-start-no-anchor-element",
|
|
34182
|
-
title: "Plain anchor
|
|
34210
|
+
title: "Plain anchor reloads TanStack Router navigation",
|
|
34183
34211
|
tags: ["test-noise"],
|
|
34184
34212
|
requires: ["tanstack-start"],
|
|
34185
34213
|
severity: "warn",
|
|
34186
|
-
recommendation: "`
|
|
34214
|
+
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
34187
34215
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
34188
34216
|
const filename = normalizeFilename$1(context.filename ?? "");
|
|
34189
34217
|
if (!TANSTACK_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
@@ -34196,7 +34224,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
34196
34224
|
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
34197
34225
|
if (typeof hrefValue === "string" && hrefValue.startsWith("/")) context.report({
|
|
34198
34226
|
node,
|
|
34199
|
-
message: "Plain <a> reloads the whole page
|
|
34227
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
34200
34228
|
});
|
|
34201
34229
|
} })
|
|
34202
34230
|
});
|
|
@@ -34268,7 +34296,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
34268
34296
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
34269
34297
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) context.report({
|
|
34270
34298
|
node,
|
|
34271
|
-
message: "navigate() during render
|
|
34299
|
+
message: "navigate() runs during render here, so server and browser output can diverge during hydration."
|
|
34272
34300
|
});
|
|
34273
34301
|
},
|
|
34274
34302
|
"CallExpression:exit"(node) {
|
|
@@ -34352,7 +34380,7 @@ const tanstackStartNoUseServerInHandler = defineRule({
|
|
|
34352
34380
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
34353
34381
|
if (body.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && (statement.directive === "use server" || isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use server"))) context.report({
|
|
34354
34382
|
node: handlerFunction,
|
|
34355
|
-
message: "\"use server\" inside a createServerFn handler
|
|
34383
|
+
message: "\"use server\" inside a createServerFn handler duplicates TanStack Start's server boundary, so the route can fail to compile."
|
|
34356
34384
|
});
|
|
34357
34385
|
} })
|
|
34358
34386
|
});
|
|
@@ -34426,11 +34454,11 @@ const tanstackStartRedirectInTryCatch = defineRule({
|
|
|
34426
34454
|
//#region src/plugin/rules/tanstack-start/tanstack-start-route-property-order.ts
|
|
34427
34455
|
const tanstackStartRoutePropertyOrder = defineRule({
|
|
34428
34456
|
id: "tanstack-start-route-property-order",
|
|
34429
|
-
title: "
|
|
34457
|
+
title: "Route property order breaks type inference",
|
|
34430
34458
|
tags: ["test-noise"],
|
|
34431
34459
|
requires: ["tanstack-start"],
|
|
34432
34460
|
severity: "error",
|
|
34433
|
-
recommendation: "Follow the order
|
|
34461
|
+
recommendation: "Follow the route property order because TanStack Router's type inference depends on earlier properties feeding later ones.",
|
|
34434
34462
|
create: (context) => ({ CallExpression(node) {
|
|
34435
34463
|
const optionsObject = getRouteOptionsObject(node);
|
|
34436
34464
|
if (!optionsObject) return;
|
|
@@ -34460,7 +34488,7 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
34460
34488
|
//#region src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts
|
|
34461
34489
|
const tanstackStartServerFnMethodOrder = defineRule({
|
|
34462
34490
|
id: "tanstack-start-server-fn-method-order",
|
|
34463
|
-
title: "
|
|
34491
|
+
title: "Server function method order breaks type inference",
|
|
34464
34492
|
tags: ["test-noise"],
|
|
34465
34493
|
requires: ["tanstack-start"],
|
|
34466
34494
|
severity: "error",
|
|
@@ -34541,7 +34569,7 @@ const useLazyMotion = defineRule({
|
|
|
34541
34569
|
return getImportedName$1(specifier) === "motion";
|
|
34542
34570
|
})) context.report({
|
|
34543
34571
|
node,
|
|
34544
|
-
message: "Importing \"motion\" ships about 30 kb of extra code
|
|
34572
|
+
message: "Importing \"motion\" ships about 30 kb of extra code and slows page load. Use \"m\" with LazyMotion instead."
|
|
34545
34573
|
});
|
|
34546
34574
|
} })
|
|
34547
34575
|
});
|
|
@@ -34578,7 +34606,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
34578
34606
|
id: "void-dom-elements-no-children",
|
|
34579
34607
|
title: "Children on a void element",
|
|
34580
34608
|
severity: "warn",
|
|
34581
|
-
recommendation: "Remove the children
|
|
34609
|
+
recommendation: "Remove the children or use a non-void tag so React does not drop content the element cannot render.",
|
|
34582
34610
|
create: (context) => ({
|
|
34583
34611
|
JSXElement(node) {
|
|
34584
34612
|
const openingElement = node.openingElement;
|
|
@@ -34726,7 +34754,7 @@ const DEPRECATED_ZOD_ERROR_MEMBERS = new Set([
|
|
|
34726
34754
|
"formErrors",
|
|
34727
34755
|
"format"
|
|
34728
34756
|
]);
|
|
34729
|
-
const ZOD_ERROR_API_MESSAGE = "
|
|
34757
|
+
const ZOD_ERROR_API_MESSAGE = "This ZodError API was removed in Zod 4, so error handling can break during the upgrade.";
|
|
34730
34758
|
const isZodErrorReference = (node) => {
|
|
34731
34759
|
const inner = stripParenExpression(node);
|
|
34732
34760
|
if (isNodeOfType(inner, "Identifier")) return getZodNamedImport(inner) === "ZodError";
|
|
@@ -34758,7 +34786,7 @@ const isReceiverOfDeprecatedZodErrorMember = (callExpression) => {
|
|
|
34758
34786
|
};
|
|
34759
34787
|
const zodV4NoDeprecatedErrorApis = defineRule({
|
|
34760
34788
|
id: "zod-v4-no-deprecated-error-apis",
|
|
34761
|
-
title: "
|
|
34789
|
+
title: "Zod 3 error API breaks in Zod 4",
|
|
34762
34790
|
requires: ["zod:4"],
|
|
34763
34791
|
tags: ["migration-hint"],
|
|
34764
34792
|
severity: "warn",
|
|
@@ -34847,7 +34875,7 @@ const parseCallUsesErrorMap = (callExpression) => {
|
|
|
34847
34875
|
};
|
|
34848
34876
|
const zodV4NoDeprecatedErrorCustomization = defineRule({
|
|
34849
34877
|
id: "zod-v4-no-deprecated-error-customization",
|
|
34850
|
-
title: "
|
|
34878
|
+
title: "Zod 3 error customization breaks in Zod 4",
|
|
34851
34879
|
requires: ["zod:4"],
|
|
34852
34880
|
tags: ["migration-hint"],
|
|
34853
34881
|
severity: "warn",
|
|
@@ -34856,7 +34884,7 @@ const zodV4NoDeprecatedErrorCustomization = defineRule({
|
|
|
34856
34884
|
if (!factoryUsesDeprecatedErrorParameter(node) && !parseCallUsesErrorMap(node)) return;
|
|
34857
34885
|
context.report({
|
|
34858
34886
|
node,
|
|
34859
|
-
message: "Zod
|
|
34887
|
+
message: "This Zod 3 error-customization form is not compatible with Zod 4, so custom messages can stop applying during the upgrade."
|
|
34860
34888
|
});
|
|
34861
34889
|
} })
|
|
34862
34890
|
});
|
|
@@ -34916,7 +34944,7 @@ const LITERAL_FACTORY = new Set(["literal"]);
|
|
|
34916
34944
|
const reportSchemaMigration = (context, node) => {
|
|
34917
34945
|
context.report({
|
|
34918
34946
|
node,
|
|
34919
|
-
message: "Zod
|
|
34947
|
+
message: "This Zod 3 schema API changed in Zod 4, so this schema can fail after the upgrade."
|
|
34920
34948
|
});
|
|
34921
34949
|
};
|
|
34922
34950
|
const isCallToDeprecatedTopLevelFactory = (callExpression) => isZodFactoryCall(callExpression, DEPRECATED_TOP_LEVEL_FACTORIES);
|
|
@@ -34968,7 +34996,7 @@ const isZodNamespaceImportMemberCreate = (memberExpression) => {
|
|
|
34968
34996
|
};
|
|
34969
34997
|
const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
34970
34998
|
id: "zod-v4-no-deprecated-schema-apis",
|
|
34971
|
-
title: "
|
|
34999
|
+
title: "Zod 3 schema API breaks in Zod 4",
|
|
34972
35000
|
requires: ["zod:4"],
|
|
34973
35001
|
tags: ["migration-hint"],
|
|
34974
35002
|
severity: "warn",
|
|
@@ -35021,7 +35049,7 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
35021
35049
|
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
35022
35050
|
context.report({
|
|
35023
35051
|
node,
|
|
35024
|
-
message: "
|
|
35052
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
35025
35053
|
});
|
|
35026
35054
|
} })
|
|
35027
35055
|
});
|