oxlint-plugin-react-doctor 0.8.1-dev.a4eaedd → 0.8.1-dev.a667b45
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.d.ts +47 -47
- package/dist/index.js +571 -738
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2622,7 +2622,7 @@ const PREFER_ALT = "Screen readers skip a decorative image more reliably with `a
|
|
|
2622
2622
|
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.";
|
|
2623
2623
|
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`.";
|
|
2624
2624
|
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`.";
|
|
2625
|
-
const resolveSettings$
|
|
2625
|
+
const resolveSettings$53 = (settings) => {
|
|
2626
2626
|
const reactDoctor = settings?.["react-doctor"];
|
|
2627
2627
|
return typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.altText ?? {} : {};
|
|
2628
2628
|
};
|
|
@@ -2745,7 +2745,7 @@ const altText = defineRule({
|
|
|
2745
2745
|
category: "Accessibility",
|
|
2746
2746
|
create: (context) => {
|
|
2747
2747
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
2748
|
-
const settings = resolveSettings$
|
|
2748
|
+
const settings = resolveSettings$53(context.settings);
|
|
2749
2749
|
const checkImg = !settings.elements || settings.elements.includes("img");
|
|
2750
2750
|
const checkObject = !settings.elements || settings.elements.includes("object");
|
|
2751
2751
|
const checkArea = !settings.elements || settings.elements.includes("area");
|
|
@@ -2807,7 +2807,7 @@ const DEFAULT_AMBIGUOUS = [
|
|
|
2807
2807
|
"a link",
|
|
2808
2808
|
"learn more"
|
|
2809
2809
|
];
|
|
2810
|
-
const resolveSettings$
|
|
2810
|
+
const resolveSettings$52 = (settings) => {
|
|
2811
2811
|
const reactDoctor = settings?.["react-doctor"];
|
|
2812
2812
|
return { words: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorAmbiguousText ?? {} : {}).words ?? DEFAULT_AMBIGUOUS };
|
|
2813
2813
|
};
|
|
@@ -2857,7 +2857,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
2857
2857
|
category: "Accessibility",
|
|
2858
2858
|
create: (context) => {
|
|
2859
2859
|
if (isTestlikeFilename(context.filename)) return {};
|
|
2860
|
-
const settings = resolveSettings$
|
|
2860
|
+
const settings = resolveSettings$52(context.settings);
|
|
2861
2861
|
const ambiguousSet = new Set(settings.words.map((word) => word.toLowerCase()));
|
|
2862
2862
|
return { JSXElement(node) {
|
|
2863
2863
|
if (getElementType(node.openingElement, context.settings) !== "a") return;
|
|
@@ -3173,7 +3173,7 @@ const getJsxPropStaticStringValues = (attribute, scopes) => getJsxPropStaticStri
|
|
|
3173
3173
|
const getJsxPropExhaustiveStaticStringValues = (attribute, scopes) => getJsxPropStaticStringValuesWithMode(attribute, scopes, null, true);
|
|
3174
3174
|
//#endregion
|
|
3175
3175
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
3176
|
-
const MESSAGE$
|
|
3176
|
+
const MESSAGE$62 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
3177
3177
|
const hasLinkRole = (roleValue) => roleValue.trim().split(/\s+/).find((roleToken) => VALID_ARIA_ROLES.has(roleToken)) === "link";
|
|
3178
3178
|
const isTransComponentsTemplate = (node) => {
|
|
3179
3179
|
let current = node.parent;
|
|
@@ -3215,7 +3215,7 @@ const anchorHasContent = defineRule({
|
|
|
3215
3215
|
if (isTransComponentsTemplate(node)) return;
|
|
3216
3216
|
context.report({
|
|
3217
3217
|
node: opening.name,
|
|
3218
|
-
message: MESSAGE$
|
|
3218
|
+
message: MESSAGE$62
|
|
3219
3219
|
});
|
|
3220
3220
|
} };
|
|
3221
3221
|
}
|
|
@@ -3225,7 +3225,7 @@ const anchorHasContent = defineRule({
|
|
|
3225
3225
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
3226
3226
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
3227
3227
|
const MESSAGE_CANT_BE_ANCHOR = "Keyboard users can't trigger this link because it's a click handler with no real `href`, so use `<button>` instead.";
|
|
3228
|
-
const resolveSettings$
|
|
3228
|
+
const resolveSettings$51 = (settings) => {
|
|
3229
3229
|
const reactDoctor = settings?.["react-doctor"];
|
|
3230
3230
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.anchorIsValid ?? {} : {};
|
|
3231
3231
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
@@ -3268,7 +3268,7 @@ const anchorIsValid = defineRule({
|
|
|
3268
3268
|
recommendation: "Give links a real destination. Use `<button>` for in-page actions.",
|
|
3269
3269
|
category: "Accessibility",
|
|
3270
3270
|
create: (context) => {
|
|
3271
|
-
const settings = resolveSettings$
|
|
3271
|
+
const settings = resolveSettings$51(context.settings);
|
|
3272
3272
|
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
3273
3273
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
3274
3274
|
return { JSXOpeningElement(node) {
|
|
@@ -3703,7 +3703,7 @@ const parseJsxValue = (value) => {
|
|
|
3703
3703
|
};
|
|
3704
3704
|
//#endregion
|
|
3705
3705
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
3706
|
-
const MESSAGE$
|
|
3706
|
+
const MESSAGE$61 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
3707
3707
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
3708
3708
|
id: "aria-activedescendant-has-tabindex",
|
|
3709
3709
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -3721,7 +3721,7 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
3721
3721
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
3722
3722
|
context.report({
|
|
3723
3723
|
node: node.name,
|
|
3724
|
-
message: MESSAGE$
|
|
3724
|
+
message: MESSAGE$61
|
|
3725
3725
|
});
|
|
3726
3726
|
return;
|
|
3727
3727
|
}
|
|
@@ -3729,7 +3729,7 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
3729
3729
|
if (canContentEditableBeTabbable(node, context.scopes, context.settings)) return;
|
|
3730
3730
|
context.report({
|
|
3731
3731
|
node: node.name,
|
|
3732
|
-
message: MESSAGE$
|
|
3732
|
+
message: MESSAGE$61
|
|
3733
3733
|
});
|
|
3734
3734
|
} })
|
|
3735
3735
|
});
|
|
@@ -4181,7 +4181,7 @@ const ariaProptypes = defineRule({
|
|
|
4181
4181
|
//#endregion
|
|
4182
4182
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
4183
4183
|
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}`;
|
|
4184
|
-
const resolveSettings$
|
|
4184
|
+
const resolveSettings$50 = (settings) => {
|
|
4185
4185
|
const reactDoctor = settings?.["react-doctor"];
|
|
4186
4186
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.ariaRole ?? {} : {};
|
|
4187
4187
|
return {
|
|
@@ -4197,7 +4197,7 @@ const ariaRole = defineRule({
|
|
|
4197
4197
|
recommendation: "Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.",
|
|
4198
4198
|
category: "Accessibility",
|
|
4199
4199
|
create: (context) => {
|
|
4200
|
-
const settings = resolveSettings$
|
|
4200
|
+
const settings = resolveSettings$50(context.settings);
|
|
4201
4201
|
return { JSXOpeningElement(node) {
|
|
4202
4202
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
4203
4203
|
if (!roleAttribute) return;
|
|
@@ -6827,7 +6827,7 @@ const stripThisParameter = (parameters) => {
|
|
|
6827
6827
|
};
|
|
6828
6828
|
//#endregion
|
|
6829
6829
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
6830
|
-
const MESSAGE$
|
|
6830
|
+
const MESSAGE$60 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
6831
6831
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
6832
6832
|
const STORAGE_GLOBALS = new Set([
|
|
6833
6833
|
"window",
|
|
@@ -6999,7 +6999,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
6999
6999
|
})) return;
|
|
7000
7000
|
context.report({
|
|
7001
7001
|
node,
|
|
7002
|
-
message: MESSAGE$
|
|
7002
|
+
message: MESSAGE$60
|
|
7003
7003
|
});
|
|
7004
7004
|
},
|
|
7005
7005
|
AssignmentExpression(node) {
|
|
@@ -7010,7 +7010,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
7010
7010
|
if (!propertyName || !isAuthCredentialKey(propertyName)) return;
|
|
7011
7011
|
context.report({
|
|
7012
7012
|
node: target,
|
|
7013
|
-
message: MESSAGE$
|
|
7013
|
+
message: MESSAGE$60
|
|
7014
7014
|
});
|
|
7015
7015
|
}
|
|
7016
7016
|
}))
|
|
@@ -7115,7 +7115,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
7115
7115
|
"select",
|
|
7116
7116
|
"form"
|
|
7117
7117
|
]);
|
|
7118
|
-
const resolveSettings$
|
|
7118
|
+
const resolveSettings$49 = (settings) => {
|
|
7119
7119
|
const reactDoctor = settings?.["react-doctor"];
|
|
7120
7120
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
|
|
7121
7121
|
return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
|
|
@@ -7128,7 +7128,7 @@ const autocompleteValid = defineRule({
|
|
|
7128
7128
|
recommendation: "Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",
|
|
7129
7129
|
category: "Accessibility",
|
|
7130
7130
|
create: skipNonProductionFiles((context) => {
|
|
7131
|
-
const settings = resolveSettings$
|
|
7131
|
+
const settings = resolveSettings$49(context.settings);
|
|
7132
7132
|
return { JSXOpeningElement: (node) => {
|
|
7133
7133
|
const tag = getElementType(node, context.settings);
|
|
7134
7134
|
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
|
|
@@ -7149,7 +7149,7 @@ const CI_INSTALL_NEAR_SECRET_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b
|
|
|
7149
7149
|
const INSTALL_COMMAND_PATTERN = /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b/i;
|
|
7150
7150
|
const SECRET_REFERENCE_PATTERN = /\bsecrets\.[A-Z0-9_]+/;
|
|
7151
7151
|
const IGNORE_SCRIPTS_FLAG_PATTERN = /--ignore-scripts\b/;
|
|
7152
|
-
const MESSAGE$
|
|
7152
|
+
const MESSAGE$59 = "The build or install pipeline can execute package lifecycle code while CI secrets may be present.";
|
|
7153
7153
|
const isWorkflowPath = (relativePath) => /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(relativePath);
|
|
7154
7154
|
const scanWorkflowContent = (content) => {
|
|
7155
7155
|
const lines = content.split("\n");
|
|
@@ -7194,7 +7194,7 @@ const scanWorkflowContent = (content) => {
|
|
|
7194
7194
|
const installLineOffset = Math.max(step.lines.findIndex((stepLine) => INSTALL_COMMAND_PATTERN.test(stepLine)), 0);
|
|
7195
7195
|
const installColumnIndex = step.lines[installLineOffset].search(INSTALL_COMMAND_PATTERN);
|
|
7196
7196
|
return [{
|
|
7197
|
-
message: MESSAGE$
|
|
7197
|
+
message: MESSAGE$59,
|
|
7198
7198
|
line: step.startLineIndex + installLineOffset + 1,
|
|
7199
7199
|
column: (installColumnIndex === -1 ? 0 : installColumnIndex) + 1
|
|
7200
7200
|
}];
|
|
@@ -7204,7 +7204,7 @@ const scanWorkflowContent = (content) => {
|
|
|
7204
7204
|
const scanNonWorkflowConfig = scanByPattern({
|
|
7205
7205
|
shouldScan: (file) => isConfigOrCiPath(file.relativePath) && !file.relativePath.endsWith("package.json") && !isWorkflowPath(file.relativePath),
|
|
7206
7206
|
pattern: CI_INSTALL_NEAR_SECRET_PATTERN,
|
|
7207
|
-
message: MESSAGE$
|
|
7207
|
+
message: MESSAGE$59
|
|
7208
7208
|
});
|
|
7209
7209
|
const scan = (file) => {
|
|
7210
7210
|
if (isWorkflowPath(file.relativePath)) return scanWorkflowContent(file.content);
|
|
@@ -7247,7 +7247,7 @@ const isCreateElementCall = (node) => {
|
|
|
7247
7247
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
7248
7248
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
7249
7249
|
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
7250
|
-
const resolveSettings$
|
|
7250
|
+
const resolveSettings$48 = (settings) => {
|
|
7251
7251
|
const reactDoctor = settings?.["react-doctor"];
|
|
7252
7252
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.buttonHasType ?? {} : {};
|
|
7253
7253
|
return {
|
|
@@ -7485,7 +7485,7 @@ const buttonHasType = defineRule({
|
|
|
7485
7485
|
severity: "warn",
|
|
7486
7486
|
recommendation: "Set an explicit button `type` so plain buttons do not submit forms by accident: `type=\"button\"`, `\"submit\"`, or `\"reset\"`.",
|
|
7487
7487
|
create: (context) => {
|
|
7488
|
-
const settings = resolveSettings$
|
|
7488
|
+
const settings = resolveSettings$48(context.settings);
|
|
7489
7489
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
7490
7490
|
return {
|
|
7491
7491
|
JSXOpeningElement(node) {
|
|
@@ -7566,7 +7566,7 @@ const buttonHasType = defineRule({
|
|
|
7566
7566
|
//#region src/plugin/rules/react-builtins/checked-requires-onchange-or-readonly.ts
|
|
7567
7567
|
const MISSING_MESSAGE$1 = "Your users can't toggle this input because `checked` has no `onChange`.";
|
|
7568
7568
|
const EXCLUSIVE_MESSAGE = "This input mixes `checked` with `defaultChecked`, so React can't tell whether it is controlled or uncontrolled.";
|
|
7569
|
-
const resolveSettings$
|
|
7569
|
+
const resolveSettings$47 = (settings) => {
|
|
7570
7570
|
const reactDoctor = settings?.["react-doctor"];
|
|
7571
7571
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.checkedRequiresOnchangeOrReadonly ?? {} : {};
|
|
7572
7572
|
return {
|
|
@@ -7667,7 +7667,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
7667
7667
|
recommendation: "Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",
|
|
7668
7668
|
category: "Correctness",
|
|
7669
7669
|
create: (context) => {
|
|
7670
|
-
const settings = resolveSettings$
|
|
7670
|
+
const settings = resolveSettings$47(context.settings);
|
|
7671
7671
|
const reportFromPresence = (presence) => {
|
|
7672
7672
|
if (presence.checkedNode && presence.defaultCheckedNode && !presence.bothCheckedAndDefaultForwarded && !settings.ignoreExclusiveCheckedAttribute) context.report({
|
|
7673
7673
|
node: presence.checkedNode,
|
|
@@ -7954,7 +7954,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
7954
7954
|
};
|
|
7955
7955
|
//#endregion
|
|
7956
7956
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
7957
|
-
const MESSAGE$
|
|
7957
|
+
const MESSAGE$58 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
7958
7958
|
const KEY_HANDLERS = [
|
|
7959
7959
|
"onKeyUp",
|
|
7960
7960
|
"onKeyDown",
|
|
@@ -8165,7 +8165,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
8165
8165
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
|
|
8166
8166
|
context.report({
|
|
8167
8167
|
node: node.name,
|
|
8168
|
-
message: MESSAGE$
|
|
8168
|
+
message: MESSAGE$58
|
|
8169
8169
|
});
|
|
8170
8170
|
} };
|
|
8171
8171
|
}
|
|
@@ -10257,7 +10257,7 @@ const getClassNameLiteral = (classAttribute) => {
|
|
|
10257
10257
|
};
|
|
10258
10258
|
//#endregion
|
|
10259
10259
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
10260
|
-
const MESSAGE$
|
|
10260
|
+
const MESSAGE$57 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
10261
10261
|
const NON_OPERABLE_ELEMENTS = new Set([
|
|
10262
10262
|
"td",
|
|
10263
10263
|
"th",
|
|
@@ -10380,7 +10380,7 @@ const WRAPPER_LABEL_PROP = "label";
|
|
|
10380
10380
|
const SELECT_ELEMENT = "select";
|
|
10381
10381
|
const DEFAULT_DEPTH = 5;
|
|
10382
10382
|
const MAX_DEPTH = 25;
|
|
10383
|
-
const resolveSettings$
|
|
10383
|
+
const resolveSettings$46 = (settings) => {
|
|
10384
10384
|
const reactDoctor = settings?.["react-doctor"];
|
|
10385
10385
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.controlHasAssociatedLabel ?? {} : {};
|
|
10386
10386
|
return {
|
|
@@ -10713,7 +10713,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10713
10713
|
recommendation: "Give every interactive control a label screen readers can read.",
|
|
10714
10714
|
category: "Accessibility",
|
|
10715
10715
|
create: (context) => {
|
|
10716
|
-
const settings = resolveSettings$
|
|
10716
|
+
const settings = resolveSettings$46(context.settings);
|
|
10717
10717
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
10718
10718
|
const iconComponentNames = /* @__PURE__ */ new Set();
|
|
10719
10719
|
const iconNamespaceNames = /* @__PURE__ */ new Set();
|
|
@@ -10787,7 +10787,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
10787
10787
|
if (candidate.enclosingBindingName !== null && labelEmbeddedNames.has(candidate.enclosingBindingName)) continue;
|
|
10788
10788
|
context.report({
|
|
10789
10789
|
node: candidate.opening,
|
|
10790
|
-
message: MESSAGE$
|
|
10790
|
+
message: MESSAGE$57
|
|
10791
10791
|
});
|
|
10792
10792
|
}
|
|
10793
10793
|
}
|
|
@@ -12594,7 +12594,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
12594
12594
|
});
|
|
12595
12595
|
//#endregion
|
|
12596
12596
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
12597
|
-
const MESSAGE$
|
|
12597
|
+
const MESSAGE$56 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
12598
12598
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
12599
12599
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
12600
12600
|
"aria-label",
|
|
@@ -12619,7 +12619,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
12619
12619
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
12620
12620
|
context.report({
|
|
12621
12621
|
node: node.name,
|
|
12622
|
-
message: MESSAGE$
|
|
12622
|
+
message: MESSAGE$56
|
|
12623
12623
|
});
|
|
12624
12624
|
} };
|
|
12625
12625
|
}
|
|
@@ -12659,13 +12659,13 @@ const isEs6Component = (node) => {
|
|
|
12659
12659
|
};
|
|
12660
12660
|
//#endregion
|
|
12661
12661
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
12662
|
-
const MESSAGE$
|
|
12662
|
+
const MESSAGE$55 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
12663
12663
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
12664
12664
|
"observer",
|
|
12665
12665
|
"lazy",
|
|
12666
12666
|
"withTracking"
|
|
12667
12667
|
];
|
|
12668
|
-
const resolveSettings$
|
|
12668
|
+
const resolveSettings$45 = (settings) => {
|
|
12669
12669
|
const reactDoctor = settings?.["react-doctor"];
|
|
12670
12670
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.displayName ?? {} : {};
|
|
12671
12671
|
const additionalHoCs = new Set(ruleSettings.additionalHoCs ?? DEFAULT_ADDITIONAL_HOCS);
|
|
@@ -12847,12 +12847,12 @@ const displayName = defineRule({
|
|
|
12847
12847
|
recommendation: "Give each component a `displayName` so DevTools shows a clear name.",
|
|
12848
12848
|
category: "Architecture",
|
|
12849
12849
|
create: (context) => {
|
|
12850
|
-
const settings = resolveSettings$
|
|
12850
|
+
const settings = resolveSettings$45(context.settings);
|
|
12851
12851
|
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
12852
12852
|
const reportAt = (node) => {
|
|
12853
12853
|
context.report({
|
|
12854
12854
|
node,
|
|
12855
|
-
message: MESSAGE$
|
|
12855
|
+
message: MESSAGE$55
|
|
12856
12856
|
});
|
|
12857
12857
|
};
|
|
12858
12858
|
return {
|
|
@@ -14147,7 +14147,7 @@ const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE
|
|
|
14147
14147
|
return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
|
|
14148
14148
|
}
|
|
14149
14149
|
if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
|
|
14150
|
-
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number"
|
|
14150
|
+
if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
|
|
14151
14151
|
if (isFunctionLike$1(unwrappedExpression)) {
|
|
14152
14152
|
const rangeStart = getRangeStart(unwrappedExpression);
|
|
14153
14153
|
return rangeStart === null ? null : `function:${rangeStart}`;
|
|
@@ -14176,11 +14176,9 @@ const resolveForEachProjection = (expression, context) => {
|
|
|
14176
14176
|
const collectionKey = resolveExpressionKey(forEachCallee.object, context);
|
|
14177
14177
|
if (!collectionKey) return null;
|
|
14178
14178
|
const firstParameter = callbackNode.params[0];
|
|
14179
|
-
const
|
|
14180
|
-
const propertyName = isNodeOfType(firstParameter, "ObjectPattern")
|
|
14181
|
-
const
|
|
14182
|
-
if (assignmentPattern && !defaultValueKey) return null;
|
|
14183
|
-
const parameterProjection = firstParameter === symbol.bindingIdentifier ? "value" : propertyName && defaultValueKey ? `${propertyName}=default:${defaultValueKey}` : propertyName;
|
|
14179
|
+
const bindingProperty = symbol.bindingIdentifier.parent;
|
|
14180
|
+
const propertyName = isNodeOfType(firstParameter, "ObjectPattern") && isNodeOfType(bindingProperty, "Property") && bindingProperty.parent === firstParameter && bindingProperty.value === symbol.bindingIdentifier ? getStaticPropertyKeyName(bindingProperty) : null;
|
|
14181
|
+
const parameterProjection = firstParameter === symbol.bindingIdentifier ? "value" : propertyName;
|
|
14184
14182
|
if (!parameterProjection) return null;
|
|
14185
14183
|
return {
|
|
14186
14184
|
collectionKey,
|
|
@@ -14192,23 +14190,13 @@ const resolveForEachProjectionKey = (expression, context) => {
|
|
|
14192
14190
|
return projection ? `forEach:${projection.collectionKey}:${projection.projectionKey}` : null;
|
|
14193
14191
|
};
|
|
14194
14192
|
const resolveResourceIdentityKey = (expression, context) => resolveForEachProjectionKey(expression, context) ?? resolveExpressionKey(expression, context);
|
|
14195
|
-
const resolveEventListenerCaptureValueIdentityKey = (expression, context) => {
|
|
14196
|
-
if (!expression) return null;
|
|
14197
|
-
const directIdentityKey = resolveResourceIdentityKey(expression, context);
|
|
14198
|
-
if (directIdentityKey) return directIdentityKey;
|
|
14199
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
14200
|
-
if (!isNodeOfType(unwrappedExpression, "BinaryExpression") && !isNodeOfType(unwrappedExpression, "LogicalExpression")) return null;
|
|
14201
|
-
const leftIdentityKey = resolveEventListenerCaptureValueIdentityKey(unwrappedExpression.left, context);
|
|
14202
|
-
const rightIdentityKey = resolveEventListenerCaptureValueIdentityKey(unwrappedExpression.right, context);
|
|
14203
|
-
return leftIdentityKey && rightIdentityKey ? `${unwrappedExpression.type}:${unwrappedExpression.operator}:${leftIdentityKey}:${rightIdentityKey}` : null;
|
|
14204
|
-
};
|
|
14205
14193
|
const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqueOptionsIdentity) => {
|
|
14206
14194
|
const capture = resolveEventListenerCapture(optionsNode, { allowIndeterminateEntries: true });
|
|
14207
14195
|
if (capture !== null) return `capture:${String(capture)}`;
|
|
14208
14196
|
if (!optionsNode) return null;
|
|
14209
14197
|
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
14210
14198
|
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) {
|
|
14211
|
-
const optionsKey = allowOpaqueOptionsIdentity ?
|
|
14199
|
+
const optionsKey = allowOpaqueOptionsIdentity ? resolveResourceIdentityKey(unwrappedOptions, context) : null;
|
|
14212
14200
|
return optionsKey ? `options:${optionsKey}` : null;
|
|
14213
14201
|
}
|
|
14214
14202
|
let captureKey = "capture:false";
|
|
@@ -14223,7 +14211,7 @@ const resolveEventListenerCaptureIdentityKey = (optionsNode, context, allowOpaqu
|
|
|
14223
14211
|
continue;
|
|
14224
14212
|
}
|
|
14225
14213
|
if (propertyName === "capture") {
|
|
14226
|
-
const propertyValueKey =
|
|
14214
|
+
const propertyValueKey = resolveResourceIdentityKey(property.value, context);
|
|
14227
14215
|
captureKey = propertyValueKey ? `capture-value:${propertyValueKey}` : null;
|
|
14228
14216
|
}
|
|
14229
14217
|
}
|
|
@@ -14823,24 +14811,7 @@ const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, co
|
|
|
14823
14811
|
return null;
|
|
14824
14812
|
};
|
|
14825
14813
|
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
|
|
14826
|
-
const
|
|
14827
|
-
const usageAncestors = /* @__PURE__ */ new Set();
|
|
14828
|
-
let usageAncestor = usageNode;
|
|
14829
|
-
while (usageAncestor && usageAncestor !== owner) {
|
|
14830
|
-
usageAncestors.add(usageAncestor);
|
|
14831
|
-
usageAncestor = usageAncestor.parent ?? null;
|
|
14832
|
-
}
|
|
14833
|
-
let descendant = node;
|
|
14834
|
-
let ancestor = descendant.parent ?? null;
|
|
14835
|
-
while (ancestor && ancestor !== owner) {
|
|
14836
|
-
const guardedSubtree = isNodeOfType(ancestor, "IfStatement") && (ancestor.consequent === descendant || ancestor.alternate === descendant) || isNodeOfType(ancestor, "ConditionalExpression") && (ancestor.consequent === descendant || ancestor.alternate === descendant) || isNodeOfType(ancestor, "LogicalExpression") && ancestor.right === descendant || isNodeOfType(ancestor, "AssignmentPattern") && ancestor.right === descendant || (isNodeOfType(ancestor, "ForStatement") || isNodeOfType(ancestor, "ForInStatement") || isNodeOfType(ancestor, "ForOfStatement") || isNodeOfType(ancestor, "WhileStatement") || isNodeOfType(ancestor, "DoWhileStatement")) && ancestor.body === descendant ? descendant : isNodeOfType(ancestor, "SwitchCase") ? ancestor : null;
|
|
14837
|
-
if (guardedSubtree && !usageAncestors.has(guardedSubtree)) return true;
|
|
14838
|
-
descendant = ancestor;
|
|
14839
|
-
ancestor = descendant.parent ?? null;
|
|
14840
|
-
}
|
|
14841
|
-
return false;
|
|
14842
|
-
};
|
|
14843
|
-
const hasRerunReleaseBeforeUsage = (callback, usage, context, allowUnreleasedPathsWithoutUsage = false) => {
|
|
14814
|
+
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
14844
14815
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
14845
14816
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
14846
14817
|
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
@@ -14851,8 +14822,7 @@ const hasRerunReleaseBeforeUsage = (callback, usage, context, allowUnreleasedPat
|
|
|
14851
14822
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14852
14823
|
const releaseStart = getRangeStart(child);
|
|
14853
14824
|
const handleGuard = findDirectHandleGuardForRelease(child, callback, usage, context);
|
|
14854
|
-
|
|
14855
|
-
if (releaseStart === null || releaseStart >= usageStart || releaseBlock !== usageBlock && !handleGuard || !handleGuard && hasExecutionBoundaryNotSharedWithUsage(child, usage.node, callback)) return;
|
|
14825
|
+
if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
|
|
14856
14826
|
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
14857
14827
|
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
14858
14828
|
return;
|
|
@@ -14860,7 +14830,7 @@ const hasRerunReleaseBeforeUsage = (callback, usage, context, allowUnreleasedPat
|
|
|
14860
14830
|
const helperFunction = resolveStableValue(child.callee, context);
|
|
14861
14831
|
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
|
|
14862
14832
|
});
|
|
14863
|
-
return
|
|
14833
|
+
return doNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
|
|
14864
14834
|
};
|
|
14865
14835
|
const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
14866
14836
|
const componentFunction = findEnclosingFunction$1(callback);
|
|
@@ -14880,7 +14850,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
14880
14850
|
});
|
|
14881
14851
|
return didFindUnmountCleanup;
|
|
14882
14852
|
};
|
|
14883
|
-
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null &&
|
|
14853
|
+
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
14884
14854
|
const collectBlockingBooleanStates = (expression, blockedExpressionValue, guardNode, context) => {
|
|
14885
14855
|
const unwrappedExpression = stripParenExpression(expression);
|
|
14886
14856
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return collectBlockingBooleanStates(unwrappedExpression.argument, !blockedExpressionValue, guardNode, context);
|
|
@@ -19046,7 +19016,7 @@ const compileGlob = (pattern) => {
|
|
|
19046
19016
|
};
|
|
19047
19017
|
//#endregion
|
|
19048
19018
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
19049
|
-
const resolveSettings$
|
|
19019
|
+
const resolveSettings$44 = (settings) => {
|
|
19050
19020
|
const reactDoctor = settings?.["react-doctor"];
|
|
19051
19021
|
return { forbid: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidComponentProps ?? {} : {}).forbid ?? [] };
|
|
19052
19022
|
};
|
|
@@ -19098,7 +19068,7 @@ const forbidComponentProps = defineRule({
|
|
|
19098
19068
|
recommendation: "Configure blocked component props so callers cannot bypass the component API contract.",
|
|
19099
19069
|
category: "Architecture",
|
|
19100
19070
|
create: (context) => {
|
|
19101
|
-
const entries = resolveSettings$
|
|
19071
|
+
const entries = resolveSettings$44(context.settings).forbid.map(normalizeEntry);
|
|
19102
19072
|
return { JSXOpeningElement(node) {
|
|
19103
19073
|
if (entries.length === 0) return;
|
|
19104
19074
|
if (!isSupportedJsxName(node.name)) return;
|
|
@@ -19127,7 +19097,7 @@ const forbidComponentProps = defineRule({
|
|
|
19127
19097
|
//#endregion
|
|
19128
19098
|
//#region src/plugin/rules/react-builtins/forbid-dom-props.ts
|
|
19129
19099
|
const buildMessage$24 = (propName, customMessage) => customMessage ?? `Your project blocks the \`${propName}\` prop on plain HTML tags, so this bypasses the agreed DOM API contract.`;
|
|
19130
|
-
const resolveSettings$
|
|
19100
|
+
const resolveSettings$43 = (settings) => {
|
|
19131
19101
|
const reactDoctor = settings?.["react-doctor"];
|
|
19132
19102
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidDomProps ?? {} : {};
|
|
19133
19103
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -19149,7 +19119,7 @@ const forbidDomProps = defineRule({
|
|
|
19149
19119
|
recommendation: "Configure blocked DOM props so plain HTML tags stay on the agreed DOM API surface.",
|
|
19150
19120
|
category: "Architecture",
|
|
19151
19121
|
create: (context) => {
|
|
19152
|
-
const forbidMap = resolveSettings$
|
|
19122
|
+
const forbidMap = resolveSettings$43(context.settings);
|
|
19153
19123
|
return { JSXOpeningElement(node) {
|
|
19154
19124
|
if (forbidMap.size === 0) return;
|
|
19155
19125
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -19212,7 +19182,7 @@ const isReactFunctionCall = (node, expectedCall) => {
|
|
|
19212
19182
|
//#endregion
|
|
19213
19183
|
//#region src/plugin/rules/react-builtins/forbid-elements.ts
|
|
19214
19184
|
const buildMessage$23 = (element, customHelp) => customHelp ? `Your project blocks \`<${element}>\` here. ${customHelp}` : `Your project blocks \`<${element}>\` here, so code stays on the approved UI surface.`;
|
|
19215
|
-
const resolveSettings$
|
|
19185
|
+
const resolveSettings$42 = (settings) => {
|
|
19216
19186
|
const reactDoctor = settings?.["react-doctor"];
|
|
19217
19187
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.forbidElements ?? {} : {};
|
|
19218
19188
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -19228,7 +19198,7 @@ const forbidElements = defineRule({
|
|
|
19228
19198
|
recommendation: "Configure blocked elements so code stays on the approved UI primitives.",
|
|
19229
19199
|
category: "Architecture",
|
|
19230
19200
|
create: (context) => {
|
|
19231
|
-
const forbidMap = resolveSettings$
|
|
19201
|
+
const forbidMap = resolveSettings$42(context.settings);
|
|
19232
19202
|
return {
|
|
19233
19203
|
JSXOpeningElement(node) {
|
|
19234
19204
|
if (forbidMap.size === 0) return;
|
|
@@ -19264,7 +19234,7 @@ const forbidElements = defineRule({
|
|
|
19264
19234
|
});
|
|
19265
19235
|
//#endregion
|
|
19266
19236
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
19267
|
-
const MESSAGE$
|
|
19237
|
+
const MESSAGE$54 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
19268
19238
|
const forwardRefUsesRef = defineRule({
|
|
19269
19239
|
id: "forward-ref-uses-ref",
|
|
19270
19240
|
title: "forwardRef without ref parameter",
|
|
@@ -19287,7 +19257,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
19287
19257
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
19288
19258
|
context.report({
|
|
19289
19259
|
node: inner,
|
|
19290
|
-
message: MESSAGE$
|
|
19260
|
+
message: MESSAGE$54
|
|
19291
19261
|
});
|
|
19292
19262
|
} })
|
|
19293
19263
|
});
|
|
@@ -19328,7 +19298,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
19328
19298
|
});
|
|
19329
19299
|
//#endregion
|
|
19330
19300
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
19331
|
-
const MESSAGE$
|
|
19301
|
+
const MESSAGE$53 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
19332
19302
|
const DEFAULT_HEADING_TAGS = [
|
|
19333
19303
|
"h1",
|
|
19334
19304
|
"h2",
|
|
@@ -19337,7 +19307,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
19337
19307
|
"h5",
|
|
19338
19308
|
"h6"
|
|
19339
19309
|
];
|
|
19340
|
-
const resolveSettings$
|
|
19310
|
+
const resolveSettings$41 = (settings) => {
|
|
19341
19311
|
const reactDoctor = settings?.["react-doctor"];
|
|
19342
19312
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
19343
19313
|
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
@@ -19350,7 +19320,7 @@ const headingHasContent = defineRule({
|
|
|
19350
19320
|
recommendation: "Put readable text in every heading.",
|
|
19351
19321
|
category: "Accessibility",
|
|
19352
19322
|
create: (context) => {
|
|
19353
|
-
const settings = resolveSettings$
|
|
19323
|
+
const settings = resolveSettings$41(context.settings);
|
|
19354
19324
|
return { JSXOpeningElement(node) {
|
|
19355
19325
|
const elementType = getElementType(node, context.settings);
|
|
19356
19326
|
if (!settings.headingTags.has(elementType)) return;
|
|
@@ -19362,7 +19332,7 @@ const headingHasContent = defineRule({
|
|
|
19362
19332
|
for (const attribute of ["aria-label", "aria-labelledby"]) if (hasJsxPropIgnoreCase(node.attributes, attribute)) return;
|
|
19363
19333
|
context.report({
|
|
19364
19334
|
node,
|
|
19365
|
-
message: MESSAGE$
|
|
19335
|
+
message: MESSAGE$53
|
|
19366
19336
|
});
|
|
19367
19337
|
} };
|
|
19368
19338
|
}
|
|
@@ -19371,7 +19341,7 @@ const headingHasContent = defineRule({
|
|
|
19371
19341
|
//#region src/plugin/rules/react-builtins/hook-use-state.ts
|
|
19372
19342
|
const REQUIRE_DESTRUCTURE_MESSAGE = "`useState` should be destructured as `[value, setValue]` so readers can see the state value and setter together.";
|
|
19373
19343
|
const NAMING_CONVENTION_MESSAGE = "This `useState` setter does not match its value name, so updates are harder to trace.";
|
|
19374
|
-
const resolveSettings$
|
|
19344
|
+
const resolveSettings$40 = (settings) => {
|
|
19375
19345
|
const reactDoctor = settings?.["react-doctor"];
|
|
19376
19346
|
return { allowDestructuredState: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.hookUseState ?? {} : {}).allowDestructuredState ?? false };
|
|
19377
19347
|
};
|
|
@@ -19398,7 +19368,7 @@ const hookUseState = defineRule({
|
|
|
19398
19368
|
recommendation: "Destructure `useState` as `const [thing, setThing] = useState(…)` so state reads and writes stay visible together.",
|
|
19399
19369
|
category: "Architecture",
|
|
19400
19370
|
create: (context) => {
|
|
19401
|
-
const { allowDestructuredState } = resolveSettings$
|
|
19371
|
+
const { allowDestructuredState } = resolveSettings$40(context.settings);
|
|
19402
19372
|
return { CallExpression(node) {
|
|
19403
19373
|
const callExpressionNode = node;
|
|
19404
19374
|
if (!isReactFunctionCall(callExpressionNode, "useState")) return;
|
|
@@ -19530,8 +19500,8 @@ const hooksNoNanInDeps = defineRule({
|
|
|
19530
19500
|
});
|
|
19531
19501
|
//#endregion
|
|
19532
19502
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
19533
|
-
const MESSAGE$
|
|
19534
|
-
const resolveSettings$
|
|
19503
|
+
const MESSAGE$52 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
19504
|
+
const resolveSettings$39 = (settings) => {
|
|
19535
19505
|
const reactDoctor = settings?.["react-doctor"];
|
|
19536
19506
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
19537
19507
|
};
|
|
@@ -19568,7 +19538,7 @@ const htmlHasLang = defineRule({
|
|
|
19568
19538
|
recommendation: "Set `<html lang=\"…\">` so screen readers know the page language.",
|
|
19569
19539
|
category: "Accessibility",
|
|
19570
19540
|
create: (context) => {
|
|
19571
|
-
const settings = resolveSettings$
|
|
19541
|
+
const settings = resolveSettings$39(context.settings);
|
|
19572
19542
|
const tagSet = new Set(settings.htmlTags);
|
|
19573
19543
|
return { JSXOpeningElement(node) {
|
|
19574
19544
|
const tag = getElementType(node, context.settings);
|
|
@@ -19577,13 +19547,13 @@ const htmlHasLang = defineRule({
|
|
|
19577
19547
|
if (!lang) {
|
|
19578
19548
|
context.report({
|
|
19579
19549
|
node: node.name,
|
|
19580
|
-
message: MESSAGE$
|
|
19550
|
+
message: MESSAGE$52
|
|
19581
19551
|
});
|
|
19582
19552
|
return;
|
|
19583
19553
|
}
|
|
19584
19554
|
if (evaluateLang(lang.value) === "empty") context.report({
|
|
19585
19555
|
node: lang,
|
|
19586
|
-
message: MESSAGE$
|
|
19556
|
+
message: MESSAGE$52
|
|
19587
19557
|
});
|
|
19588
19558
|
} };
|
|
19589
19559
|
}
|
|
@@ -19856,7 +19826,7 @@ const isJsxFragmentElement = (node, scopes) => {
|
|
|
19856
19826
|
};
|
|
19857
19827
|
//#endregion
|
|
19858
19828
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
19859
|
-
const MESSAGE$
|
|
19829
|
+
const MESSAGE$51 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
19860
19830
|
const isStaticallyAriaHidden = (openingElement) => {
|
|
19861
19831
|
const ariaHiddenAttribute = getAuthoritativeJsxAttribute(openingElement.attributes, "aria-hidden", false);
|
|
19862
19832
|
if (!ariaHiddenAttribute) return false;
|
|
@@ -19945,14 +19915,14 @@ const iframeHasTitle = defineRule({
|
|
|
19945
19915
|
if (!titleAttr) {
|
|
19946
19916
|
if (hasSpread || tag === "iframe") context.report({
|
|
19947
19917
|
node: node.name,
|
|
19948
|
-
message: MESSAGE$
|
|
19918
|
+
message: MESSAGE$51
|
|
19949
19919
|
});
|
|
19950
19920
|
return;
|
|
19951
19921
|
}
|
|
19952
19922
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
19953
19923
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
19954
19924
|
node: titleAttr,
|
|
19955
|
-
message: MESSAGE$
|
|
19925
|
+
message: MESSAGE$51
|
|
19956
19926
|
});
|
|
19957
19927
|
} })
|
|
19958
19928
|
});
|
|
@@ -20077,14 +20047,14 @@ const iframeMissingSandbox = defineRule({
|
|
|
20077
20047
|
});
|
|
20078
20048
|
//#endregion
|
|
20079
20049
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
20080
|
-
const MESSAGE$
|
|
20050
|
+
const MESSAGE$50 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
20081
20051
|
const DEFAULT_COMPONENTS = ["img"];
|
|
20082
20052
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
20083
20053
|
"image",
|
|
20084
20054
|
"photo",
|
|
20085
20055
|
"picture"
|
|
20086
20056
|
];
|
|
20087
|
-
const resolveSettings$
|
|
20057
|
+
const resolveSettings$38 = (settings) => {
|
|
20088
20058
|
const reactDoctor = settings?.["react-doctor"];
|
|
20089
20059
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.imgRedundantAlt ?? {} : {};
|
|
20090
20060
|
return {
|
|
@@ -20132,7 +20102,7 @@ const imgRedundantAlt = defineRule({
|
|
|
20132
20102
|
category: "Accessibility",
|
|
20133
20103
|
create: (context) => {
|
|
20134
20104
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
20135
|
-
const settings = resolveSettings$
|
|
20105
|
+
const settings = resolveSettings$38(context.settings);
|
|
20136
20106
|
return { JSXOpeningElement(node) {
|
|
20137
20107
|
const tag = getElementType(node, context.settings);
|
|
20138
20108
|
if (!settings.components.includes(tag)) return;
|
|
@@ -20142,7 +20112,7 @@ const imgRedundantAlt = defineRule({
|
|
|
20142
20112
|
if (!altAttribute) return;
|
|
20143
20113
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
20144
20114
|
node: altAttribute,
|
|
20145
|
-
message: MESSAGE$
|
|
20115
|
+
message: MESSAGE$50
|
|
20146
20116
|
});
|
|
20147
20117
|
} };
|
|
20148
20118
|
}
|
|
@@ -20440,7 +20410,7 @@ const COMPOSITE_ITEM_ROLES = new Set([
|
|
|
20440
20410
|
"row",
|
|
20441
20411
|
"gridcell"
|
|
20442
20412
|
]);
|
|
20443
|
-
const resolveSettings$
|
|
20413
|
+
const resolveSettings$37 = (settings) => {
|
|
20444
20414
|
const reactDoctor = settings?.["react-doctor"];
|
|
20445
20415
|
return { tabbable: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.interactiveSupportsFocus ?? {} : {}).tabbable ?? DEFAULT_TABBABLE_ROLES };
|
|
20446
20416
|
};
|
|
@@ -20452,7 +20422,7 @@ const interactiveSupportsFocus = defineRule({
|
|
|
20452
20422
|
recommendation: "Add keyboard focus support so users can reach interactive elements without a pointer.",
|
|
20453
20423
|
category: "Accessibility",
|
|
20454
20424
|
create: (context) => {
|
|
20455
|
-
const settings = resolveSettings$
|
|
20425
|
+
const settings = resolveSettings$37(context.settings);
|
|
20456
20426
|
const tabbableSet = new Set(settings.tabbable);
|
|
20457
20427
|
return { JSXOpeningElement(node) {
|
|
20458
20428
|
if (isLocalTestScaffoldJsx(node, context)) return;
|
|
@@ -20674,7 +20644,7 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
20674
20644
|
//#endregion
|
|
20675
20645
|
//#region src/plugin/rules/jotai/jotai-select-atom-in-render-body.ts
|
|
20676
20646
|
const JOTAI_SELECT_ATOM_SOURCES = ["jotai/utils", "jotai"];
|
|
20677
|
-
const COMPONENT_NAME_PATTERN
|
|
20647
|
+
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
20678
20648
|
const isImportedSelectAtom = (callExpression) => {
|
|
20679
20649
|
if (!isNodeOfType(callExpression.callee, "Identifier")) return false;
|
|
20680
20650
|
const localName = callExpression.callee.name;
|
|
@@ -20710,14 +20680,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
20710
20680
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
20711
20681
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
20712
20682
|
const declaredName = functionNode.id.name;
|
|
20713
|
-
return COMPONENT_NAME_PATTERN
|
|
20683
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$3.test(declaredName);
|
|
20714
20684
|
}
|
|
20715
20685
|
let cursor = functionNode.parent ?? null;
|
|
20716
20686
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
20717
20687
|
if (!cursor) return false;
|
|
20718
20688
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
20719
20689
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
20720
|
-
return COMPONENT_NAME_PATTERN
|
|
20690
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$3.test(cursor.id.name);
|
|
20721
20691
|
};
|
|
20722
20692
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
20723
20693
|
let didFindRenderPathInvocation = false;
|
|
@@ -22106,7 +22076,7 @@ const getHoistableRegExpConstructionKind = (node, context) => {
|
|
|
22106
22076
|
if (!STATEFUL_REGEXP_FLAGS_PATTERN.test(effectiveFlags)) return "stateless";
|
|
22107
22077
|
return isSafeStatefulReplaceAllSearch(node, effectiveFlags, context) ? "statefulReplaceAll" : null;
|
|
22108
22078
|
};
|
|
22109
|
-
const MESSAGE$
|
|
22079
|
+
const MESSAGE$49 = "`new RegExp()` rebuilds the pattern on every loop pass. Move it to a constant outside the loop.";
|
|
22110
22080
|
const jsHoistRegexp = defineRule({
|
|
22111
22081
|
id: "js-hoist-regexp",
|
|
22112
22082
|
title: "RegExp built inside a loop",
|
|
@@ -22123,7 +22093,7 @@ const jsHoistRegexp = defineRule({
|
|
|
22123
22093
|
if (constructionKind === "statefulReplaceAll" && cachedEnvironmentHazard === "replaceAllIntegrityLost") return;
|
|
22124
22094
|
context.report({
|
|
22125
22095
|
node,
|
|
22126
|
-
message: MESSAGE$
|
|
22096
|
+
message: MESSAGE$49
|
|
22127
22097
|
});
|
|
22128
22098
|
};
|
|
22129
22099
|
return createLoopAwareVisitors({
|
|
@@ -23748,7 +23718,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
23748
23718
|
const NEVER_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
23749
23719
|
const ALWAYS_MESSAGE$2 = () => "This boolean prop style disagrees with the project setting, so equivalent true props are harder to scan consistently.";
|
|
23750
23720
|
const FALSE_OMITTED_MESSAGE = (attributeName) => `\`${attributeName}={false}\` does nothing, so the explicit false value adds noise without changing output.`;
|
|
23751
|
-
const resolveSettings$
|
|
23721
|
+
const resolveSettings$36 = (settings) => {
|
|
23752
23722
|
const reactDoctor = settings?.["react-doctor"];
|
|
23753
23723
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxBooleanValue ?? {} : {};
|
|
23754
23724
|
return {
|
|
@@ -23766,7 +23736,7 @@ const jsxBooleanValue = defineRule({
|
|
|
23766
23736
|
recommendation: "Use one boolean-attribute style so equivalent true props scan the same across the codebase.",
|
|
23767
23737
|
category: "Architecture",
|
|
23768
23738
|
create: (context) => {
|
|
23769
|
-
const settings = resolveSettings$
|
|
23739
|
+
const settings = resolveSettings$36(context.settings);
|
|
23770
23740
|
const alwaysSet = new Set(settings.always);
|
|
23771
23741
|
const neverSet = new Set(settings.never);
|
|
23772
23742
|
return { JSXAttribute(node) {
|
|
@@ -23820,7 +23790,7 @@ const jsxBooleanValue = defineRule({
|
|
|
23820
23790
|
const UNNECESSARY_BRACES_MESSAGE = "These curly braces wrap a literal value, so they add JSX noise without changing output.";
|
|
23821
23791
|
const REQUIRED_BRACES_MESSAGE = "This JSX value needs `{ }` so React reads it as an expression instead of text.";
|
|
23822
23792
|
const isAllowedMode = (value) => value === "always" || value === "never" || value === "ignore";
|
|
23823
|
-
const resolveSettings$
|
|
23793
|
+
const resolveSettings$35 = (settings) => {
|
|
23824
23794
|
const reactDoctor = settings?.["react-doctor"];
|
|
23825
23795
|
const ruleSettingsRaw = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxCurlyBracePresence : void 0;
|
|
23826
23796
|
if (isAllowedMode(ruleSettingsRaw)) return {
|
|
@@ -23916,7 +23886,7 @@ const jsxCurlyBracePresence = defineRule({
|
|
|
23916
23886
|
recommendation: "Use one JSX literal style so equivalent markup scans the same without extra JSX noise.",
|
|
23917
23887
|
category: "Architecture",
|
|
23918
23888
|
create: (context) => {
|
|
23919
|
-
const settings = resolveSettings$
|
|
23889
|
+
const settings = resolveSettings$35(context.settings);
|
|
23920
23890
|
return {
|
|
23921
23891
|
JSXAttribute(node) {
|
|
23922
23892
|
const value = node.value;
|
|
@@ -23999,7 +23969,7 @@ const containsJsxElement = (root) => {
|
|
|
23999
23969
|
//#region src/plugin/rules/react-builtins/jsx-filename-extension.ts
|
|
24000
23970
|
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.`;
|
|
24001
23971
|
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.`;
|
|
24002
|
-
const resolveSettings$
|
|
23972
|
+
const resolveSettings$34 = (settings) => {
|
|
24003
23973
|
const reactDoctor = settings?.["react-doctor"];
|
|
24004
23974
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFilenameExtension ?? {} : {};
|
|
24005
23975
|
return {
|
|
@@ -24021,7 +23991,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
24021
23991
|
recommendation: "Name JSX files with the configured extension so file names accurately signal which files contain JSX.",
|
|
24022
23992
|
category: "Architecture",
|
|
24023
23993
|
create: (context) => {
|
|
24024
|
-
const settings = resolveSettings$
|
|
23994
|
+
const settings = resolveSettings$34(context.settings);
|
|
24025
23995
|
const allowedExtensions = normalizeExtensions(settings.extensions);
|
|
24026
23996
|
const filename = normalizeFilename(context.filename ?? "fixture.tsx");
|
|
24027
23997
|
const extensionOnly = path.extname(filename).slice(1);
|
|
@@ -24062,7 +24032,7 @@ const jsxFilenameExtension = defineRule({
|
|
|
24062
24032
|
//#region src/plugin/rules/react-builtins/jsx-fragments.ts
|
|
24063
24033
|
const SYNTAX_MESSAGE = "`<React.Fragment>` is used where shorthand fragments are configured, so similar wrappers look different across the codebase.";
|
|
24064
24034
|
const ELEMENT_MESSAGE = "Fragment shorthand is used where explicit fragments are configured, so similar wrappers look different across the codebase.";
|
|
24065
|
-
const resolveSettings$
|
|
24035
|
+
const resolveSettings$33 = (settings) => {
|
|
24066
24036
|
const reactDoctor = settings?.["react-doctor"];
|
|
24067
24037
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxFragments ?? {} : {}).mode ?? "syntax" };
|
|
24068
24038
|
};
|
|
@@ -24074,7 +24044,7 @@ const jsxFragments = defineRule({
|
|
|
24074
24044
|
recommendation: "Use one fragment style so identical wrappers do not look different across files.",
|
|
24075
24045
|
category: "Architecture",
|
|
24076
24046
|
create: (context) => {
|
|
24077
|
-
const { mode } = resolveSettings$
|
|
24047
|
+
const { mode } = resolveSettings$33(context.settings);
|
|
24078
24048
|
return {
|
|
24079
24049
|
JSXElement(node) {
|
|
24080
24050
|
if (mode !== "syntax") return;
|
|
@@ -24118,7 +24088,7 @@ const buildPropRegex = (handlerPropPrefix) => {
|
|
|
24118
24088
|
const escaped = tokens.map((token) => token.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
24119
24089
|
return new RegExp(`^(${escaped})[A-Z].*$`);
|
|
24120
24090
|
};
|
|
24121
|
-
const resolveSettings$
|
|
24091
|
+
const resolveSettings$32 = (settings) => {
|
|
24122
24092
|
const reactDoctor = settings?.["react-doctor"];
|
|
24123
24093
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxHandlerNames ?? {} : {};
|
|
24124
24094
|
let handlerPrefix = DEFAULT_HANDLER_PREFIX;
|
|
@@ -24194,7 +24164,7 @@ const jsxHandlerNames = defineRule({
|
|
|
24194
24164
|
recommendation: "Use the `on…` prefix for event-handler props and `handle…` for handlers so readers can trace event flow.",
|
|
24195
24165
|
category: "Architecture",
|
|
24196
24166
|
create: (context) => {
|
|
24197
|
-
const settings = resolveSettings$
|
|
24167
|
+
const settings = resolveSettings$32(context.settings);
|
|
24198
24168
|
return { JSXAttribute(node) {
|
|
24199
24169
|
if (settings.ignoreComponentNames.length > 0) {
|
|
24200
24170
|
const opening = node.parent;
|
|
@@ -24269,19 +24239,6 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
24269
24239
|
return false;
|
|
24270
24240
|
};
|
|
24271
24241
|
//#endregion
|
|
24272
|
-
//#region src/plugin/utils/is-component-function.ts
|
|
24273
|
-
const isFunctionAssignedToComponent = (functionNode) => {
|
|
24274
|
-
let cursor = functionNode.parent ?? null;
|
|
24275
|
-
while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
24276
|
-
if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
|
|
24277
|
-
return isNodeOfType(cursor, "ExportDefaultDeclaration");
|
|
24278
|
-
};
|
|
24279
|
-
const isComponentFunction$1 = (node) => {
|
|
24280
|
-
if (!isFunctionLike$1(node)) return false;
|
|
24281
|
-
if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
|
|
24282
|
-
return isFunctionAssignedToComponent(node);
|
|
24283
|
-
};
|
|
24284
|
-
//#endregion
|
|
24285
24242
|
//#region src/plugin/utils/is-const-declared-binding.ts
|
|
24286
24243
|
const BINDING_PATTERN_ANCESTOR_TYPES = new Set([
|
|
24287
24244
|
"VariableDeclarator",
|
|
@@ -24326,17 +24283,11 @@ const ITERATOR_METHOD_NAMES$1 = new Set([
|
|
|
24326
24283
|
"flatMap",
|
|
24327
24284
|
"from"
|
|
24328
24285
|
]);
|
|
24329
|
-
const RENDERING_CALL_NAMES = new Set([
|
|
24330
|
-
"createPortal",
|
|
24331
|
-
"hydrate",
|
|
24332
|
-
"hydrateRoot",
|
|
24333
|
-
"render"
|
|
24334
|
-
]);
|
|
24335
24286
|
const MISSING_KEY_ARRAY = "Your users can see the wrong data when this array reorders.";
|
|
24336
24287
|
const MISSING_KEY_ITERATOR = "Your users can see the wrong data when this list reorders.";
|
|
24337
|
-
const
|
|
24288
|
+
const KEY_AFTER_SPREAD = "Place this `key` before the `{...spread}` so the JSX transform can always extract it.";
|
|
24338
24289
|
const DUPLICATE_KEY = (keyValue) => `Your users can see the wrong data because two elements share the key "${keyValue}".`;
|
|
24339
|
-
const resolveSettings$
|
|
24290
|
+
const resolveSettings$31 = (settings) => {
|
|
24340
24291
|
const reactDoctor = settings?.["react-doctor"];
|
|
24341
24292
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxKey ?? {} : {};
|
|
24342
24293
|
return {
|
|
@@ -24350,33 +24301,6 @@ const findArrayVariableDeclarator = (arrayExpression) => {
|
|
|
24350
24301
|
if (ancestor && isNodeOfType(ancestor, "VariableDeclarator") && ancestor.init === wrapped && isNodeOfType(ancestor.id, "Identifier")) return ancestor;
|
|
24351
24302
|
return null;
|
|
24352
24303
|
};
|
|
24353
|
-
const isArrayNestedInObjectProperty = (arrayExpression) => {
|
|
24354
|
-
let current = findTransparentExpressionRoot(arrayExpression);
|
|
24355
|
-
while (current.parent) {
|
|
24356
|
-
const parent = current.parent;
|
|
24357
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === current || parent.alternate === current) || isNodeOfType(parent, "LogicalExpression") && (parent.left === current || parent.right === current)) {
|
|
24358
|
-
current = findTransparentExpressionRoot(parent);
|
|
24359
|
-
continue;
|
|
24360
|
-
}
|
|
24361
|
-
return isNodeOfType(parent, "Property") && parent.value === current;
|
|
24362
|
-
}
|
|
24363
|
-
return false;
|
|
24364
|
-
};
|
|
24365
|
-
const isArrayPassedToNonRenderingCall = (arrayExpression, scopes) => {
|
|
24366
|
-
const transparentArray = findTransparentExpressionRoot(arrayExpression);
|
|
24367
|
-
const callExpression = transparentArray.parent;
|
|
24368
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression") || !callExpression.arguments.some((argument) => argument === transparentArray)) return false;
|
|
24369
|
-
const argumentIndex = callExpression.arguments.findIndex((argument) => argument === transparentArray);
|
|
24370
|
-
if (isReactApiCall(callExpression, "createElement", scopes, {
|
|
24371
|
-
allowGlobalReactNamespace: true,
|
|
24372
|
-
allowUnboundBareCalls: true,
|
|
24373
|
-
resolveNamedAliases: true
|
|
24374
|
-
})) return argumentIndex < 2;
|
|
24375
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
24376
|
-
if (isNodeOfType(callee, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.name);
|
|
24377
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return !RENDERING_CALL_NAMES.has(callee.property.name);
|
|
24378
|
-
return true;
|
|
24379
|
-
};
|
|
24380
24304
|
const isIdentityIteratorCallback = (callback) => {
|
|
24381
24305
|
const callbackFunction = stripParenExpression(callback);
|
|
24382
24306
|
if (!isNodeOfType(callbackFunction, "ArrowFunctionExpression") && !isNodeOfType(callbackFunction, "FunctionExpression")) return true;
|
|
@@ -24473,7 +24397,7 @@ const findNamedCallbackIteratorCall = (functionNode) => {
|
|
|
24473
24397
|
namedCallbackIteratorCallCache.set(functionNode, iteratorCall);
|
|
24474
24398
|
return iteratorCall;
|
|
24475
24399
|
};
|
|
24476
|
-
const findEnclosingIteratorContext = (jsxNode
|
|
24400
|
+
const findEnclosingIteratorContext = (jsxNode) => {
|
|
24477
24401
|
let current = jsxNode;
|
|
24478
24402
|
let isOutsideContainingFunction = false;
|
|
24479
24403
|
let didSeeReturnStatement = false;
|
|
@@ -24495,8 +24419,6 @@ const findEnclosingIteratorContext = (jsxNode, scopes) => {
|
|
|
24495
24419
|
isOutsideContainingFunction = true;
|
|
24496
24420
|
} else if (isNodeOfType(parent, "ArrayExpression")) {
|
|
24497
24421
|
if (isOutsideContainingFunction) return null;
|
|
24498
|
-
if (isArrayNestedInObjectProperty(parent)) return null;
|
|
24499
|
-
if (isArrayPassedToNonRenderingCall(parent, scopes)) return null;
|
|
24500
24422
|
const arrayParent = parent.parent;
|
|
24501
24423
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
24502
24424
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
@@ -24578,15 +24500,9 @@ const isWithinChildrenToArray = (jsxNode) => {
|
|
|
24578
24500
|
const isKeyPropertyName = (propertyKey) => {
|
|
24579
24501
|
if (isNodeOfType(propertyKey, "Identifier")) return propertyKey.name === "key";
|
|
24580
24502
|
if (isNodeOfType(propertyKey, "Literal")) return String(propertyKey.value) === "key";
|
|
24581
|
-
return
|
|
24503
|
+
return true;
|
|
24582
24504
|
};
|
|
24583
24505
|
const isThisPropsMember = (expression) => isNodeOfType(expression, "MemberExpression") && !expression.computed && isNodeOfType(expression.object, "ThisExpression") && isNodeOfType(expression.property, "Identifier") && expression.property.name === "props";
|
|
24584
|
-
const isDirectComponentPropsParameter = (bindingIdentifier, functionNode) => {
|
|
24585
|
-
if (!isFunctionLike$1(functionNode) || !isComponentFunction$1(functionNode)) return false;
|
|
24586
|
-
const firstParameter = functionNode.params?.[0];
|
|
24587
|
-
if (!firstParameter) return false;
|
|
24588
|
-
return (isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter) === bindingIdentifier;
|
|
24589
|
-
};
|
|
24590
24506
|
const isFunctionParameterPropsRest = (bindingIdentifier) => {
|
|
24591
24507
|
const restElement = bindingIdentifier.parent;
|
|
24592
24508
|
if (!restElement || !isNodeOfType(restElement, "RestElement")) return false;
|
|
@@ -24596,7 +24512,7 @@ const isFunctionParameterPropsRest = (bindingIdentifier) => {
|
|
|
24596
24512
|
const parameterNode = patternParent && isNodeOfType(patternParent, "AssignmentPattern") && patternParent.left === objectPattern ? patternParent : objectPattern;
|
|
24597
24513
|
const functionNode = parameterNode.parent;
|
|
24598
24514
|
if (!functionNode || !isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "FunctionDeclaration")) return false;
|
|
24599
|
-
return
|
|
24515
|
+
return functionNode.params.some((param) => param === parameterNode);
|
|
24600
24516
|
};
|
|
24601
24517
|
const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
|
|
24602
24518
|
const restElement = bindingIdentifier.parent;
|
|
@@ -24609,14 +24525,14 @@ const isRestBindingWithKeyExtracted = (bindingIdentifier) => {
|
|
|
24609
24525
|
}
|
|
24610
24526
|
return false;
|
|
24611
24527
|
};
|
|
24612
|
-
const
|
|
24613
|
-
let
|
|
24528
|
+
const isBindingObjectMutated = (scopeOwner, bindingName) => {
|
|
24529
|
+
let didFindMutation = false;
|
|
24614
24530
|
walkAst(scopeOwner, (node) => {
|
|
24615
|
-
if (
|
|
24531
|
+
if (didFindMutation) return false;
|
|
24616
24532
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression")) {
|
|
24617
24533
|
const assignedObject = stripParenExpression(node.left.object);
|
|
24618
|
-
if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName
|
|
24619
|
-
|
|
24534
|
+
if (isNodeOfType(assignedObject, "Identifier") && assignedObject.name === bindingName) {
|
|
24535
|
+
didFindMutation = true;
|
|
24620
24536
|
return false;
|
|
24621
24537
|
}
|
|
24622
24538
|
}
|
|
@@ -24624,63 +24540,59 @@ const isBindingAssignedKey = (scopeOwner, bindingName) => {
|
|
|
24624
24540
|
const callee = node.callee;
|
|
24625
24541
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Object" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "assign") {
|
|
24626
24542
|
const firstArgument = node.arguments[0];
|
|
24627
|
-
if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName)
|
|
24628
|
-
|
|
24629
|
-
|
|
24630
|
-
if (source.properties.some((property) => isNodeOfType(property, "Property") && isKeyPropertyName(property.key))) {
|
|
24631
|
-
didFindKeyAssignment = true;
|
|
24632
|
-
return false;
|
|
24633
|
-
}
|
|
24543
|
+
if (firstArgument && isNodeOfType(firstArgument, "Identifier") && firstArgument.name === bindingName) {
|
|
24544
|
+
didFindMutation = true;
|
|
24545
|
+
return false;
|
|
24634
24546
|
}
|
|
24635
24547
|
}
|
|
24636
24548
|
}
|
|
24637
24549
|
});
|
|
24638
|
-
return
|
|
24550
|
+
return didFindMutation;
|
|
24639
24551
|
};
|
|
24640
|
-
const
|
|
24552
|
+
const spreadExpressionCanCarryKey = (expression, depth) => {
|
|
24641
24553
|
const inner = stripParenExpression(expression);
|
|
24642
24554
|
if (isNodeOfType(inner, "ObjectExpression")) {
|
|
24643
24555
|
for (const property of inner.properties) {
|
|
24644
24556
|
if (isNodeOfType(property, "SpreadElement")) {
|
|
24645
|
-
if (depth >= 3)
|
|
24646
|
-
if (
|
|
24557
|
+
if (depth >= 3) return true;
|
|
24558
|
+
if (spreadExpressionCanCarryKey(property.argument, depth + 1)) return true;
|
|
24647
24559
|
continue;
|
|
24648
24560
|
}
|
|
24649
|
-
if (!isNodeOfType(property, "Property"))
|
|
24561
|
+
if (!isNodeOfType(property, "Property")) return true;
|
|
24562
|
+
if (property.computed) return true;
|
|
24650
24563
|
if (isKeyPropertyName(property.key)) return true;
|
|
24651
24564
|
}
|
|
24652
24565
|
return false;
|
|
24653
24566
|
}
|
|
24654
24567
|
if (isNodeOfType(inner, "ConditionalExpression")) {
|
|
24655
|
-
if (depth >= 3) return
|
|
24656
|
-
return
|
|
24568
|
+
if (depth >= 3) return true;
|
|
24569
|
+
return spreadExpressionCanCarryKey(inner.consequent, depth + 1) || spreadExpressionCanCarryKey(inner.alternate, depth + 1);
|
|
24657
24570
|
}
|
|
24658
24571
|
if (isNodeOfType(inner, "LogicalExpression")) {
|
|
24659
|
-
if (depth >= 3) return
|
|
24660
|
-
if (inner.operator === "&&") return
|
|
24661
|
-
return
|
|
24572
|
+
if (depth >= 3) return true;
|
|
24573
|
+
if (inner.operator === "&&") return spreadExpressionCanCarryKey(inner.right, depth + 1);
|
|
24574
|
+
return spreadExpressionCanCarryKey(inner.left, depth + 1) || spreadExpressionCanCarryKey(inner.right, depth + 1);
|
|
24662
24575
|
}
|
|
24663
24576
|
if (isNodeOfType(inner, "Literal")) return false;
|
|
24664
24577
|
if (isThisPropsMember(inner)) return false;
|
|
24665
24578
|
if (isNodeOfType(inner, "Identifier")) {
|
|
24666
24579
|
if (inner.name === "undefined") return false;
|
|
24667
|
-
if (depth >= 3) return
|
|
24580
|
+
if (depth >= 3) return true;
|
|
24668
24581
|
const binding = findVariableInitializer(inner, inner.name);
|
|
24669
|
-
if (!binding) return
|
|
24582
|
+
if (!binding) return true;
|
|
24670
24583
|
if (isRestBindingWithKeyExtracted(binding.bindingIdentifier)) return false;
|
|
24671
24584
|
if (isFunctionParameterPropsRest(binding.bindingIdentifier)) return false;
|
|
24672
|
-
if (
|
|
24673
|
-
if (
|
|
24674
|
-
|
|
24675
|
-
return spreadExpressionHasKey(binding.initializer, depth + 1);
|
|
24585
|
+
if (!isConstDeclaredBinding(binding) || !binding.initializer) return true;
|
|
24586
|
+
if (isBindingObjectMutated(binding.scopeOwner, inner.name)) return true;
|
|
24587
|
+
return spreadExpressionCanCarryKey(binding.initializer, depth + 1);
|
|
24676
24588
|
}
|
|
24677
|
-
return
|
|
24589
|
+
return true;
|
|
24678
24590
|
};
|
|
24679
|
-
const spreadCanOverwriteKey = (spreadAttribute) =>
|
|
24591
|
+
const spreadCanOverwriteKey = (spreadAttribute) => spreadExpressionCanCarryKey(spreadAttribute.argument, 0);
|
|
24680
24592
|
const checkKeyBeforeSpread = (context, openingElement) => {
|
|
24681
24593
|
let keyIndex = null;
|
|
24682
24594
|
let keyAttribute = null;
|
|
24683
|
-
let
|
|
24595
|
+
let firstKeyCarryingSpreadIndex = null;
|
|
24684
24596
|
for (let attributeIndex = 0; attributeIndex < openingElement.attributes.length; attributeIndex++) {
|
|
24685
24597
|
const attribute = openingElement.attributes[attributeIndex];
|
|
24686
24598
|
if (isNodeOfType(attribute, "JSXAttribute")) {
|
|
@@ -24688,11 +24600,11 @@ const checkKeyBeforeSpread = (context, openingElement) => {
|
|
|
24688
24600
|
keyIndex = attributeIndex;
|
|
24689
24601
|
keyAttribute = attribute;
|
|
24690
24602
|
}
|
|
24691
|
-
} else if (isNodeOfType(attribute, "JSXSpreadAttribute") && spreadCanOverwriteKey(attribute))
|
|
24603
|
+
} else if (isNodeOfType(attribute, "JSXSpreadAttribute") && firstKeyCarryingSpreadIndex === null && spreadCanOverwriteKey(attribute)) firstKeyCarryingSpreadIndex = attributeIndex;
|
|
24692
24604
|
}
|
|
24693
|
-
if (keyIndex !== null &&
|
|
24605
|
+
if (keyIndex !== null && firstKeyCarryingSpreadIndex !== null && firstKeyCarryingSpreadIndex < keyIndex && keyAttribute) context.report({
|
|
24694
24606
|
node: keyAttribute,
|
|
24695
|
-
message:
|
|
24607
|
+
message: KEY_AFTER_SPREAD
|
|
24696
24608
|
});
|
|
24697
24609
|
};
|
|
24698
24610
|
const getKeyAttributeValueString = (openingElement) => {
|
|
@@ -24736,7 +24648,7 @@ const jsxKey = defineRule({
|
|
|
24736
24648
|
severity: "error",
|
|
24737
24649
|
recommendation: "Add a stable `key` prop so React can keep list items matched to the right data when the list changes.",
|
|
24738
24650
|
create: (context) => {
|
|
24739
|
-
const settings = resolveSettings$
|
|
24651
|
+
const settings = resolveSettings$31(context.settings);
|
|
24740
24652
|
return {
|
|
24741
24653
|
JSXElement(node) {
|
|
24742
24654
|
const openingElement = node.openingElement;
|
|
@@ -24754,7 +24666,7 @@ const jsxKey = defineRule({
|
|
|
24754
24666
|
else seenKeys.add(keyValue.keyValue);
|
|
24755
24667
|
}
|
|
24756
24668
|
}
|
|
24757
|
-
const enclosingContext = findEnclosingIteratorContext(node
|
|
24669
|
+
const enclosingContext = findEnclosingIteratorContext(node);
|
|
24758
24670
|
if (!enclosingContext) return;
|
|
24759
24671
|
if (isWithinChildrenToArray(node)) return;
|
|
24760
24672
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
@@ -24790,7 +24702,7 @@ const jsxKey = defineRule({
|
|
|
24790
24702
|
//#region src/plugin/rules/react-builtins/jsx-max-depth.ts
|
|
24791
24703
|
const buildMessage$19 = (depth, max) => `This JSX is hard to read at ${depth} levels deep, past the limit of ${max}.`;
|
|
24792
24704
|
const DEFAULT_MAX_DEPTH = 14;
|
|
24793
|
-
const resolveSettings$
|
|
24705
|
+
const resolveSettings$30 = (settings) => {
|
|
24794
24706
|
const reactDoctor = settings?.["react-doctor"];
|
|
24795
24707
|
return { max: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxMaxDepth ?? {} : {}).max ?? DEFAULT_MAX_DEPTH };
|
|
24796
24708
|
};
|
|
@@ -24859,7 +24771,7 @@ const jsxMaxDepth = defineRule({
|
|
|
24859
24771
|
recommendation: "Pull deeply nested JSX into smaller components so it's easier to read.",
|
|
24860
24772
|
category: "Architecture",
|
|
24861
24773
|
create: (context) => {
|
|
24862
|
-
const { max } = resolveSettings$
|
|
24774
|
+
const { max } = resolveSettings$30(context.settings);
|
|
24863
24775
|
const deepestLeafPerTree = /* @__PURE__ */ new Map();
|
|
24864
24776
|
const checkNode = (node) => {
|
|
24865
24777
|
if (!isLeafJsxNode(node)) return;
|
|
@@ -24890,7 +24802,7 @@ const jsxMaxDepth = defineRule({
|
|
|
24890
24802
|
});
|
|
24891
24803
|
//#endregion
|
|
24892
24804
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
24893
|
-
const MESSAGE$
|
|
24805
|
+
const MESSAGE$48 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
24894
24806
|
const LITERAL_TEXT_TAGS = new Set([
|
|
24895
24807
|
"code",
|
|
24896
24808
|
"pre",
|
|
@@ -24960,7 +24872,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
24960
24872
|
if (isDeliberateStyledCommentToken(node)) return;
|
|
24961
24873
|
context.report({
|
|
24962
24874
|
node,
|
|
24963
|
-
message: MESSAGE$
|
|
24875
|
+
message: MESSAGE$48
|
|
24964
24876
|
});
|
|
24965
24877
|
} })
|
|
24966
24878
|
});
|
|
@@ -24991,7 +24903,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
24991
24903
|
};
|
|
24992
24904
|
//#endregion
|
|
24993
24905
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
24994
|
-
const MESSAGE$
|
|
24906
|
+
const MESSAGE$47 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
24995
24907
|
const CONTEXT_MODULES$1 = [
|
|
24996
24908
|
"react",
|
|
24997
24909
|
"use-context-selector",
|
|
@@ -25094,7 +25006,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
25094
25006
|
if (!isConstructedValue(innerExpression)) continue;
|
|
25095
25007
|
context.report({
|
|
25096
25008
|
node: attribute,
|
|
25097
|
-
message: MESSAGE$
|
|
25009
|
+
message: MESSAGE$47
|
|
25098
25010
|
});
|
|
25099
25011
|
}
|
|
25100
25012
|
}
|
|
@@ -25960,7 +25872,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
25960
25872
|
];
|
|
25961
25873
|
//#endregion
|
|
25962
25874
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
25963
|
-
const MESSAGE$
|
|
25875
|
+
const MESSAGE$46 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
25964
25876
|
const isDataArrayPropName = (propName) => {
|
|
25965
25877
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
25966
25878
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -26047,7 +25959,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
26047
25959
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
26048
25960
|
context.report({
|
|
26049
25961
|
node,
|
|
26050
|
-
message: MESSAGE$
|
|
25962
|
+
message: MESSAGE$46
|
|
26051
25963
|
});
|
|
26052
25964
|
}
|
|
26053
25965
|
};
|
|
@@ -26305,7 +26217,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
26305
26217
|
]);
|
|
26306
26218
|
//#endregion
|
|
26307
26219
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
26308
|
-
const MESSAGE$
|
|
26220
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
26309
26221
|
const isAccessorPredicateName = (propName) => {
|
|
26310
26222
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
26311
26223
|
if (propName.length <= prefix.length) continue;
|
|
@@ -26512,7 +26424,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
26512
26424
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
26513
26425
|
context.report({
|
|
26514
26426
|
node,
|
|
26515
|
-
message: MESSAGE$
|
|
26427
|
+
message: MESSAGE$45
|
|
26516
26428
|
});
|
|
26517
26429
|
}
|
|
26518
26430
|
};
|
|
@@ -26732,7 +26644,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
26732
26644
|
];
|
|
26733
26645
|
//#endregion
|
|
26734
26646
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
26735
|
-
const MESSAGE$
|
|
26647
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
26736
26648
|
const isConfigObjectPropName = (propName) => {
|
|
26737
26649
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
26738
26650
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -26821,7 +26733,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26821
26733
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
26822
26734
|
context.report({
|
|
26823
26735
|
node,
|
|
26824
|
-
message: MESSAGE$
|
|
26736
|
+
message: MESSAGE$44
|
|
26825
26737
|
});
|
|
26826
26738
|
}
|
|
26827
26739
|
};
|
|
@@ -26829,9 +26741,9 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
26829
26741
|
});
|
|
26830
26742
|
//#endregion
|
|
26831
26743
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
26832
|
-
const MESSAGE$
|
|
26744
|
+
const MESSAGE$43 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
26833
26745
|
const JAVASCRIPT_URL_PATTERN = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
26834
|
-
const resolveSettings$
|
|
26746
|
+
const resolveSettings$29 = (settings) => {
|
|
26835
26747
|
const reactDoctor = settings?.["react-doctor"];
|
|
26836
26748
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
26837
26749
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
@@ -26853,7 +26765,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
26853
26765
|
recommendation: "Use real event handlers instead of `javascript:` URLs so injected URL text cannot execute as code.",
|
|
26854
26766
|
category: "Security",
|
|
26855
26767
|
create: (context) => {
|
|
26856
|
-
const options = resolveSettings$
|
|
26768
|
+
const options = resolveSettings$29(context.settings);
|
|
26857
26769
|
return { JSXOpeningElement(node) {
|
|
26858
26770
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26859
26771
|
const elementName = resolveJsxElementType(node);
|
|
@@ -26867,7 +26779,284 @@ const jsxNoScriptUrl = defineRule({
|
|
|
26867
26779
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
26868
26780
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
26869
26781
|
node: attribute,
|
|
26870
|
-
message: MESSAGE$
|
|
26782
|
+
message: MESSAGE$43
|
|
26783
|
+
});
|
|
26784
|
+
}
|
|
26785
|
+
} };
|
|
26786
|
+
}
|
|
26787
|
+
});
|
|
26788
|
+
//#endregion
|
|
26789
|
+
//#region src/plugin/rules/react-builtins/jsx-no-target-blank.ts
|
|
26790
|
+
const NOREFERRER_MESSAGE = "`target=\"_blank\"` without `rel=\"noreferrer\"` lets the linked page hijack your tab to a phishing site.";
|
|
26791
|
+
const NOOPENER_MESSAGE = "`target=\"_blank\"` without `rel` lets the linked page hijack your tab to a phishing site.";
|
|
26792
|
+
const SPREAD_MESSAGE = "A spread here can add `target=\"_blank\"`, letting the linked page hijack your tab to a phishing site.";
|
|
26793
|
+
const resolveSettings$28 = (settings) => {
|
|
26794
|
+
const reactDoctor = settings?.["react-doctor"];
|
|
26795
|
+
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxNoTargetBlank ?? {} : {};
|
|
26796
|
+
const reactSettings = typeof settings?.react === "object" && settings.react !== null ? settings.react : {};
|
|
26797
|
+
const linkComponents = /* @__PURE__ */ new Map();
|
|
26798
|
+
for (const entry of reactSettings.linkComponents ?? []) if (typeof entry === "string") linkComponents.set(entry, ["href"]);
|
|
26799
|
+
else if (typeof entry === "object" && entry !== null) {
|
|
26800
|
+
const linkAttribute = entry.linkAttribute ?? "href";
|
|
26801
|
+
linkComponents.set(entry.name, Array.isArray(linkAttribute) ? linkAttribute : [linkAttribute]);
|
|
26802
|
+
}
|
|
26803
|
+
const formComponents = /* @__PURE__ */ new Map();
|
|
26804
|
+
for (const entry of reactSettings.formComponents ?? []) if (typeof entry === "string") formComponents.set(entry, ["action"]);
|
|
26805
|
+
else if (typeof entry === "object" && entry !== null) {
|
|
26806
|
+
const formAttribute = entry.formAttribute ?? "action";
|
|
26807
|
+
formComponents.set(entry.name, Array.isArray(formAttribute) ? formAttribute : [formAttribute]);
|
|
26808
|
+
}
|
|
26809
|
+
return {
|
|
26810
|
+
enforceDynamicLinks: ruleSettings.enforceDynamicLinks ?? "always",
|
|
26811
|
+
warnOnSpreadAttributes: ruleSettings.warnOnSpreadAttributes ?? false,
|
|
26812
|
+
allowReferrer: ruleSettings.allowReferrer ?? false,
|
|
26813
|
+
links: ruleSettings.links ?? true,
|
|
26814
|
+
forms: ruleSettings.forms ?? false,
|
|
26815
|
+
linkComponents,
|
|
26816
|
+
formComponents
|
|
26817
|
+
};
|
|
26818
|
+
};
|
|
26819
|
+
const isExternalLink = (href) => href.includes("//");
|
|
26820
|
+
const matchHrefExpression = (expression, state) => {
|
|
26821
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") {
|
|
26822
|
+
if (isExternalLink(expression.value)) state.isExternal = true;
|
|
26823
|
+
return;
|
|
26824
|
+
}
|
|
26825
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
26826
|
+
state.isDynamic = true;
|
|
26827
|
+
return;
|
|
26828
|
+
}
|
|
26829
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26830
|
+
matchHrefExpression(expression.consequent, state);
|
|
26831
|
+
matchHrefExpression(expression.alternate, state);
|
|
26832
|
+
}
|
|
26833
|
+
};
|
|
26834
|
+
const checkHref = (attributeValue, enforceDynamicLinks) => {
|
|
26835
|
+
const state = {
|
|
26836
|
+
isExternal: false,
|
|
26837
|
+
isDynamic: false
|
|
26838
|
+
};
|
|
26839
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") state.isExternal = isExternalLink(attributeValue.value);
|
|
26840
|
+
else if (isNodeOfType(attributeValue, "JSXExpressionContainer")) matchHrefExpression(attributeValue.expression, state);
|
|
26841
|
+
if (enforceDynamicLinks === "never") return !state.isExternal || state.isDynamic;
|
|
26842
|
+
return !(state.isExternal || state.isDynamic);
|
|
26843
|
+
};
|
|
26844
|
+
const checkRelValue = (text, allowReferrer) => {
|
|
26845
|
+
const tokens = text.split(/\s+/);
|
|
26846
|
+
if (allowReferrer) return tokens.includes("noopener") || tokens.includes("noreferrer");
|
|
26847
|
+
return tokens.some((token) => token.toLowerCase() === "noreferrer");
|
|
26848
|
+
};
|
|
26849
|
+
const matchRelExpression = (expression, allowReferrer) => {
|
|
26850
|
+
const empty = {
|
|
26851
|
+
combined: false,
|
|
26852
|
+
testName: "",
|
|
26853
|
+
consequent: false,
|
|
26854
|
+
alternate: false
|
|
26855
|
+
};
|
|
26856
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
26857
|
+
combined: checkRelValue(expression.value, allowReferrer),
|
|
26858
|
+
testName: "",
|
|
26859
|
+
consequent: false,
|
|
26860
|
+
alternate: false
|
|
26861
|
+
};
|
|
26862
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26863
|
+
const consequent = matchRelExpression(expression.consequent, allowReferrer);
|
|
26864
|
+
const alternate = matchRelExpression(expression.alternate, allowReferrer);
|
|
26865
|
+
const test = expression.test;
|
|
26866
|
+
if (isNodeOfType(test, "Identifier")) return {
|
|
26867
|
+
combined: consequent.combined && alternate.combined,
|
|
26868
|
+
testName: test.name,
|
|
26869
|
+
consequent: consequent.combined,
|
|
26870
|
+
alternate: alternate.combined
|
|
26871
|
+
};
|
|
26872
|
+
return {
|
|
26873
|
+
combined: consequent.combined && alternate.combined,
|
|
26874
|
+
testName: "",
|
|
26875
|
+
consequent: consequent.combined,
|
|
26876
|
+
alternate: alternate.combined
|
|
26877
|
+
};
|
|
26878
|
+
}
|
|
26879
|
+
return empty;
|
|
26880
|
+
};
|
|
26881
|
+
const checkRel = (attributeValue, allowReferrer) => {
|
|
26882
|
+
const empty = {
|
|
26883
|
+
combined: false,
|
|
26884
|
+
testName: "",
|
|
26885
|
+
consequent: false,
|
|
26886
|
+
alternate: false
|
|
26887
|
+
};
|
|
26888
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
26889
|
+
combined: checkRelValue(attributeValue.value, allowReferrer),
|
|
26890
|
+
testName: "",
|
|
26891
|
+
consequent: false,
|
|
26892
|
+
alternate: false
|
|
26893
|
+
};
|
|
26894
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
26895
|
+
const expression = attributeValue.expression;
|
|
26896
|
+
if (expression.type === "JSXEmptyExpression") return empty;
|
|
26897
|
+
return matchRelExpression(expression, allowReferrer);
|
|
26898
|
+
}
|
|
26899
|
+
return empty;
|
|
26900
|
+
};
|
|
26901
|
+
const matchTargetExpression = (expression) => {
|
|
26902
|
+
const empty = {
|
|
26903
|
+
combined: false,
|
|
26904
|
+
testName: "",
|
|
26905
|
+
consequent: false,
|
|
26906
|
+
alternate: false
|
|
26907
|
+
};
|
|
26908
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return {
|
|
26909
|
+
combined: expression.value.toLowerCase() === "_blank",
|
|
26910
|
+
testName: "",
|
|
26911
|
+
consequent: false,
|
|
26912
|
+
alternate: false
|
|
26913
|
+
};
|
|
26914
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
26915
|
+
const consequent = matchTargetExpression(expression.consequent);
|
|
26916
|
+
const alternate = matchTargetExpression(expression.alternate);
|
|
26917
|
+
const test = expression.test;
|
|
26918
|
+
const combined = consequent.combined || alternate.combined;
|
|
26919
|
+
if (isNodeOfType(test, "Identifier")) return {
|
|
26920
|
+
combined,
|
|
26921
|
+
testName: test.name,
|
|
26922
|
+
consequent: consequent.combined,
|
|
26923
|
+
alternate: alternate.combined
|
|
26924
|
+
};
|
|
26925
|
+
return {
|
|
26926
|
+
combined,
|
|
26927
|
+
testName: "",
|
|
26928
|
+
consequent: consequent.combined,
|
|
26929
|
+
alternate: alternate.combined
|
|
26930
|
+
};
|
|
26931
|
+
}
|
|
26932
|
+
return empty;
|
|
26933
|
+
};
|
|
26934
|
+
const checkTarget = (attributeValue) => {
|
|
26935
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return {
|
|
26936
|
+
combined: attributeValue.value.toLowerCase() === "_blank",
|
|
26937
|
+
testName: "",
|
|
26938
|
+
consequent: false,
|
|
26939
|
+
alternate: false
|
|
26940
|
+
};
|
|
26941
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
26942
|
+
const expression = attributeValue.expression;
|
|
26943
|
+
if (expression.type === "JSXEmptyExpression") return {
|
|
26944
|
+
combined: false,
|
|
26945
|
+
testName: "",
|
|
26946
|
+
consequent: false,
|
|
26947
|
+
alternate: false
|
|
26948
|
+
};
|
|
26949
|
+
return matchTargetExpression(expression);
|
|
26950
|
+
}
|
|
26951
|
+
return {
|
|
26952
|
+
combined: false,
|
|
26953
|
+
testName: "",
|
|
26954
|
+
consequent: false,
|
|
26955
|
+
alternate: false
|
|
26956
|
+
};
|
|
26957
|
+
};
|
|
26958
|
+
const jsxNoTargetBlank = defineRule({
|
|
26959
|
+
id: "jsx-no-target-blank",
|
|
26960
|
+
title: "Unsafe target=_blank link",
|
|
26961
|
+
severity: "warn",
|
|
26962
|
+
recommendation: "Add `rel=\"noreferrer\"` (or `\"noopener\"`) when using `target=\"_blank\"`.",
|
|
26963
|
+
category: "Security",
|
|
26964
|
+
create: (context) => {
|
|
26965
|
+
const settings = resolveSettings$28(context.settings);
|
|
26966
|
+
const isLink = (tagName) => {
|
|
26967
|
+
if (!settings.links) return false;
|
|
26968
|
+
if (tagName === "a") return true;
|
|
26969
|
+
return settings.linkComponents.has(tagName);
|
|
26970
|
+
};
|
|
26971
|
+
const isForm = (tagName) => {
|
|
26972
|
+
if (!settings.forms) return false;
|
|
26973
|
+
if (tagName === "form") return true;
|
|
26974
|
+
return settings.formComponents.has(tagName);
|
|
26975
|
+
};
|
|
26976
|
+
return { JSXOpeningElement(node) {
|
|
26977
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
26978
|
+
const tagName = resolveJsxElementType(node);
|
|
26979
|
+
if (!tagName) return;
|
|
26980
|
+
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
26981
|
+
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
26982
|
+
const formAttributeNames = settings.formComponents.get(tagName) ?? ["action"];
|
|
26983
|
+
let targetTuple = {
|
|
26984
|
+
combined: false,
|
|
26985
|
+
testName: "",
|
|
26986
|
+
consequent: false,
|
|
26987
|
+
alternate: false
|
|
26988
|
+
};
|
|
26989
|
+
let relTuple = {
|
|
26990
|
+
combined: false,
|
|
26991
|
+
testName: "",
|
|
26992
|
+
consequent: false,
|
|
26993
|
+
alternate: false
|
|
26994
|
+
};
|
|
26995
|
+
let isHrefValid = true;
|
|
26996
|
+
let hasHrefValue = false;
|
|
26997
|
+
let warnSpread = false;
|
|
26998
|
+
let targetReportNode = node.name;
|
|
26999
|
+
let spreadReportNode = null;
|
|
27000
|
+
for (const attribute of node.attributes) {
|
|
27001
|
+
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
27002
|
+
if (settings.warnOnSpreadAttributes) {
|
|
27003
|
+
warnSpread = true;
|
|
27004
|
+
spreadReportNode = attribute;
|
|
27005
|
+
targetTuple = {
|
|
27006
|
+
combined: false,
|
|
27007
|
+
testName: "",
|
|
27008
|
+
consequent: false,
|
|
27009
|
+
alternate: false
|
|
27010
|
+
};
|
|
27011
|
+
relTuple = {
|
|
27012
|
+
combined: false,
|
|
27013
|
+
testName: "",
|
|
27014
|
+
consequent: false,
|
|
27015
|
+
alternate: false
|
|
27016
|
+
};
|
|
27017
|
+
isHrefValid = false;
|
|
27018
|
+
hasHrefValue = true;
|
|
27019
|
+
}
|
|
27020
|
+
continue;
|
|
27021
|
+
}
|
|
27022
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
27023
|
+
const attributeName = attribute.name;
|
|
27024
|
+
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
27025
|
+
const propName = attributeName.name;
|
|
27026
|
+
const value = attribute.value;
|
|
27027
|
+
if (propName === "target") {
|
|
27028
|
+
if (value) {
|
|
27029
|
+
targetTuple = checkTarget(value);
|
|
27030
|
+
targetReportNode = value;
|
|
27031
|
+
}
|
|
27032
|
+
} else if (propName === "href" || propName === "action" || linkAttributeNames.includes(propName) || formAttributeNames.includes(propName)) {
|
|
27033
|
+
if (value) {
|
|
27034
|
+
hasHrefValue = true;
|
|
27035
|
+
isHrefValid = checkHref(value, settings.enforceDynamicLinks);
|
|
27036
|
+
}
|
|
27037
|
+
} else if (propName === "rel" && value) relTuple = checkRel(value, settings.allowReferrer);
|
|
27038
|
+
}
|
|
27039
|
+
if (warnSpread) {
|
|
27040
|
+
if (hasHrefValue && isHrefValid || relTuple.combined) return;
|
|
27041
|
+
context.report({
|
|
27042
|
+
node: spreadReportNode ?? node,
|
|
27043
|
+
message: SPREAD_MESSAGE
|
|
27044
|
+
});
|
|
27045
|
+
return;
|
|
27046
|
+
}
|
|
27047
|
+
if (!isHrefValid) {
|
|
27048
|
+
if (targetTuple.testName !== "" && targetTuple.testName === relTuple.testName) {
|
|
27049
|
+
const consequentBad = targetTuple.consequent && !relTuple.consequent;
|
|
27050
|
+
const alternateBad = targetTuple.alternate && !relTuple.alternate;
|
|
27051
|
+
if (consequentBad || alternateBad) context.report({
|
|
27052
|
+
node: targetReportNode,
|
|
27053
|
+
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
27054
|
+
});
|
|
27055
|
+
return;
|
|
27056
|
+
}
|
|
27057
|
+
if (targetTuple.combined && !relTuple.combined) context.report({
|
|
27058
|
+
node: targetReportNode,
|
|
27059
|
+
message: settings.allowReferrer ? NOOPENER_MESSAGE : NOREFERRER_MESSAGE
|
|
26871
27060
|
});
|
|
26872
27061
|
}
|
|
26873
27062
|
} };
|
|
@@ -27210,7 +27399,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
27210
27399
|
});
|
|
27211
27400
|
//#endregion
|
|
27212
27401
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
27213
|
-
const MESSAGE$
|
|
27402
|
+
const MESSAGE$42 = "You can't tell what props reach this element when you spread them.";
|
|
27214
27403
|
const resolveSettings$25 = (settings) => {
|
|
27215
27404
|
const reactDoctor = settings?.["react-doctor"];
|
|
27216
27405
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -27253,7 +27442,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
27253
27442
|
}
|
|
27254
27443
|
context.report({
|
|
27255
27444
|
node: attribute,
|
|
27256
|
-
message: MESSAGE$
|
|
27445
|
+
message: MESSAGE$42
|
|
27257
27446
|
});
|
|
27258
27447
|
didReportInFile = true;
|
|
27259
27448
|
return;
|
|
@@ -27529,7 +27718,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
27529
27718
|
});
|
|
27530
27719
|
//#endregion
|
|
27531
27720
|
//#region src/plugin/rules/a11y/lang.ts
|
|
27532
|
-
const MESSAGE$
|
|
27721
|
+
const MESSAGE$41 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
27533
27722
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
27534
27723
|
"aa",
|
|
27535
27724
|
"ab",
|
|
@@ -28009,7 +28198,7 @@ const lang = defineRule({
|
|
|
28009
28198
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
28010
28199
|
context.report({
|
|
28011
28200
|
node: langAttr,
|
|
28012
|
-
message: MESSAGE$
|
|
28201
|
+
message: MESSAGE$41
|
|
28013
28202
|
});
|
|
28014
28203
|
return;
|
|
28015
28204
|
}
|
|
@@ -28018,7 +28207,7 @@ const lang = defineRule({
|
|
|
28018
28207
|
if (value === null) return;
|
|
28019
28208
|
if (!isValidLangTag(value)) context.report({
|
|
28020
28209
|
node: langAttr,
|
|
28021
|
-
message: MESSAGE$
|
|
28210
|
+
message: MESSAGE$41
|
|
28022
28211
|
});
|
|
28023
28212
|
} })
|
|
28024
28213
|
});
|
|
@@ -28063,13 +28252,13 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
28063
28252
|
recommendation: "Use a constrained compiler for untrusted content, disable expressions/raw HTML, sandbox renderers, and avoid caching attacker-controlled output across tenants.",
|
|
28064
28253
|
scan: scanByPattern({
|
|
28065
28254
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
28066
|
-
pattern: /(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo
|
|
28255
|
+
pattern: /(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,
|
|
28067
28256
|
message: "MDX/markdown rendering code may evaluate user or repository content during SSR or static generation."
|
|
28068
28257
|
})
|
|
28069
28258
|
});
|
|
28070
28259
|
//#endregion
|
|
28071
28260
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
28072
|
-
const MESSAGE$
|
|
28261
|
+
const MESSAGE$40 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
28073
28262
|
const DEFAULT_AUDIO = ["audio"];
|
|
28074
28263
|
const DEFAULT_VIDEO = ["video"];
|
|
28075
28264
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -28159,7 +28348,7 @@ const mediaHasCaption = defineRule({
|
|
|
28159
28348
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
28160
28349
|
context.report({
|
|
28161
28350
|
node: node.name,
|
|
28162
|
-
message: MESSAGE$
|
|
28351
|
+
message: MESSAGE$40
|
|
28163
28352
|
});
|
|
28164
28353
|
return;
|
|
28165
28354
|
}
|
|
@@ -28178,7 +28367,7 @@ const mediaHasCaption = defineRule({
|
|
|
28178
28367
|
return kindValue.value.toLowerCase() === "captions";
|
|
28179
28368
|
})) context.report({
|
|
28180
28369
|
node: node.name,
|
|
28181
|
-
message: MESSAGE$
|
|
28370
|
+
message: MESSAGE$40
|
|
28182
28371
|
});
|
|
28183
28372
|
} };
|
|
28184
28373
|
}
|
|
@@ -28531,30 +28720,6 @@ const hasAncestorLayoutMatching = (pageFilename, matchesLayout) => {
|
|
|
28531
28720
|
const hasAncestorMetadataLayout = (pageFilename) => hasAncestorLayoutMatching(pageFilename, (layoutPath) => METADATA_EXPORT_NAMES.some((exportName) => doesModuleExportName(layoutPath, exportName)));
|
|
28532
28721
|
//#endregion
|
|
28533
28722
|
//#region src/plugin/rules/nextjs/nextjs-missing-metadata.ts
|
|
28534
|
-
const unwrapAwaitedExpression = (node) => {
|
|
28535
|
-
let expression = stripParenExpression(node);
|
|
28536
|
-
while (isNodeOfType(expression, "AwaitExpression")) expression = stripParenExpression(expression.argument);
|
|
28537
|
-
return expression;
|
|
28538
|
-
};
|
|
28539
|
-
const isNextNavigationRedirectCall = (node, context) => {
|
|
28540
|
-
const expression = unwrapAwaitedExpression(node);
|
|
28541
|
-
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
28542
|
-
const callee = stripParenExpression(expression.callee);
|
|
28543
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
28544
|
-
if (context.scopes.symbolFor(callee)?.kind !== "import") return false;
|
|
28545
|
-
const importedName = getImportedNameFromModule(expression, callee.name, "next/navigation");
|
|
28546
|
-
return importedName === "redirect" || importedName === "permanentRedirect";
|
|
28547
|
-
};
|
|
28548
|
-
const isRedirectOnlyDefaultExport = (programNode, context) => {
|
|
28549
|
-
const defaultExport = findExportedValue(programNode, "default");
|
|
28550
|
-
if (!defaultExport || !isFunctionLike$1(defaultExport)) return false;
|
|
28551
|
-
if (!isNodeOfType(defaultExport.body, "BlockStatement")) return isNextNavigationRedirectCall(defaultExport.body, context);
|
|
28552
|
-
if (defaultExport.body.body.length !== 1) return false;
|
|
28553
|
-
const onlyStatement = defaultExport.body.body[0];
|
|
28554
|
-
if (isNodeOfType(onlyStatement, "ExpressionStatement")) return isNextNavigationRedirectCall(onlyStatement.expression, context);
|
|
28555
|
-
if (isNodeOfType(onlyStatement, "ReturnStatement") && onlyStatement.argument) return isNextNavigationRedirectCall(onlyStatement.argument, context);
|
|
28556
|
-
return false;
|
|
28557
|
-
};
|
|
28558
28723
|
const nextjsMissingMetadata = defineRule({
|
|
28559
28724
|
id: "nextjs-missing-metadata",
|
|
28560
28725
|
title: "Page missing metadata for search previews",
|
|
@@ -28574,7 +28739,6 @@ const nextjsMissingMetadata = defineRule({
|
|
|
28574
28739
|
if (isNodeOfType(declaration, "FunctionDeclaration")) return declaration.id?.name === "generateMetadata";
|
|
28575
28740
|
return (statement.specifiers ?? []).some((specifier) => isNodeOfType(specifier, "ExportSpecifier") && isNodeOfType(specifier.exported, "Identifier") && METADATA_EXPORT_NAMES.includes(specifier.exported.name));
|
|
28576
28741
|
})) return;
|
|
28577
|
-
if (isRedirectOnlyDefaultExport(programNode, context)) return;
|
|
28578
28742
|
if (hasAncestorMetadataLayout(context.filename ?? "")) return;
|
|
28579
28743
|
context.report({
|
|
28580
28744
|
node: programNode,
|
|
@@ -30231,7 +30395,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
30231
30395
|
});
|
|
30232
30396
|
//#endregion
|
|
30233
30397
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
30234
|
-
const MESSAGE$
|
|
30398
|
+
const MESSAGE$39 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
30235
30399
|
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
30236
30400
|
const noAccessKey = defineRule({
|
|
30237
30401
|
id: "no-access-key",
|
|
@@ -30250,7 +30414,7 @@ const noAccessKey = defineRule({
|
|
|
30250
30414
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
30251
30415
|
context.report({
|
|
30252
30416
|
node: accessKey,
|
|
30253
|
-
message: MESSAGE$
|
|
30417
|
+
message: MESSAGE$39
|
|
30254
30418
|
});
|
|
30255
30419
|
return;
|
|
30256
30420
|
}
|
|
@@ -30260,7 +30424,7 @@ const noAccessKey = defineRule({
|
|
|
30260
30424
|
if (isUndefinedIdentifier$1(expression)) return;
|
|
30261
30425
|
context.report({
|
|
30262
30426
|
node: accessKey,
|
|
30263
|
-
message: MESSAGE$
|
|
30427
|
+
message: MESSAGE$39
|
|
30264
30428
|
});
|
|
30265
30429
|
}
|
|
30266
30430
|
} };
|
|
@@ -32603,7 +32767,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
32603
32767
|
});
|
|
32604
32768
|
//#endregion
|
|
32605
32769
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
32606
|
-
const MESSAGE$
|
|
32770
|
+
const MESSAGE$38 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
32607
32771
|
const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
32608
32772
|
"button",
|
|
32609
32773
|
"embed",
|
|
@@ -32715,7 +32879,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
32715
32879
|
const isImplicitlyFocusable = isNativelyFocusable(tag, node);
|
|
32716
32880
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
32717
32881
|
node: ariaHidden,
|
|
32718
|
-
message: MESSAGE$
|
|
32882
|
+
message: MESSAGE$38
|
|
32719
32883
|
});
|
|
32720
32884
|
} })
|
|
32721
32885
|
});
|
|
@@ -33762,7 +33926,7 @@ const isAllLiteralArrayExpression = (node) => {
|
|
|
33762
33926
|
};
|
|
33763
33927
|
//#endregion
|
|
33764
33928
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
33765
|
-
const MESSAGE$
|
|
33929
|
+
const MESSAGE$37 = "Your users can see & submit the wrong data when this list reorders.";
|
|
33766
33930
|
const SECOND_INDEX_METHODS = new Set([
|
|
33767
33931
|
"every",
|
|
33768
33932
|
"filter",
|
|
@@ -33881,14 +34045,14 @@ const noArrayIndexKey = defineRule({
|
|
|
33881
34045
|
if (propName !== "key") continue;
|
|
33882
34046
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
33883
34047
|
node: property,
|
|
33884
|
-
message: MESSAGE$
|
|
34048
|
+
message: MESSAGE$37
|
|
33885
34049
|
});
|
|
33886
34050
|
}
|
|
33887
34051
|
} })
|
|
33888
34052
|
});
|
|
33889
34053
|
//#endregion
|
|
33890
34054
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
33891
|
-
const MESSAGE$
|
|
34055
|
+
const MESSAGE$36 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
33892
34056
|
const noAsyncEffectCallback = defineRule({
|
|
33893
34057
|
id: "no-async-effect-callback",
|
|
33894
34058
|
title: "Async effect callback",
|
|
@@ -33906,13 +34070,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
33906
34070
|
if (!callback.async) return;
|
|
33907
34071
|
context.report({
|
|
33908
34072
|
node: callback,
|
|
33909
|
-
message: MESSAGE$
|
|
34073
|
+
message: MESSAGE$36
|
|
33910
34074
|
});
|
|
33911
34075
|
} })
|
|
33912
34076
|
});
|
|
33913
34077
|
//#endregion
|
|
33914
34078
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
33915
|
-
const MESSAGE$
|
|
34079
|
+
const MESSAGE$35 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
33916
34080
|
const resolveSettings$21 = (settings) => {
|
|
33917
34081
|
const reactDoctor = settings?.["react-doctor"];
|
|
33918
34082
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -34009,7 +34173,7 @@ const noAutofocus = defineRule({
|
|
|
34009
34173
|
if (isConditionallyRendered(node)) return;
|
|
34010
34174
|
context.report({
|
|
34011
34175
|
node: autoFocusAttribute,
|
|
34012
|
-
message: MESSAGE$
|
|
34176
|
+
message: MESSAGE$35
|
|
34013
34177
|
});
|
|
34014
34178
|
} };
|
|
34015
34179
|
}
|
|
@@ -35338,7 +35502,7 @@ const noChainStateUpdates = defineRule({
|
|
|
35338
35502
|
});
|
|
35339
35503
|
//#endregion
|
|
35340
35504
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
35341
|
-
const MESSAGE$
|
|
35505
|
+
const MESSAGE$34 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
35342
35506
|
const noChildrenProp = defineRule({
|
|
35343
35507
|
id: "no-children-prop",
|
|
35344
35508
|
title: "Children passed as a prop",
|
|
@@ -35350,7 +35514,7 @@ const noChildrenProp = defineRule({
|
|
|
35350
35514
|
if (node.name.name !== "children") return;
|
|
35351
35515
|
context.report({
|
|
35352
35516
|
node: node.name,
|
|
35353
|
-
message: MESSAGE$
|
|
35517
|
+
message: MESSAGE$34
|
|
35354
35518
|
});
|
|
35355
35519
|
},
|
|
35356
35520
|
CallExpression(node) {
|
|
@@ -35363,7 +35527,7 @@ const noChildrenProp = defineRule({
|
|
|
35363
35527
|
const propertyKey = property.key;
|
|
35364
35528
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
35365
35529
|
node: propertyKey,
|
|
35366
|
-
message: MESSAGE$
|
|
35530
|
+
message: MESSAGE$34
|
|
35367
35531
|
});
|
|
35368
35532
|
}
|
|
35369
35533
|
}
|
|
@@ -35371,7 +35535,7 @@ const noChildrenProp = defineRule({
|
|
|
35371
35535
|
});
|
|
35372
35536
|
//#endregion
|
|
35373
35537
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
35374
|
-
const MESSAGE$
|
|
35538
|
+
const MESSAGE$33 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
35375
35539
|
const noCloneElement = defineRule({
|
|
35376
35540
|
id: "no-clone-element",
|
|
35377
35541
|
title: "cloneElement makes child props fragile",
|
|
@@ -35384,7 +35548,7 @@ const noCloneElement = defineRule({
|
|
|
35384
35548
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
35385
35549
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
35386
35550
|
node: callee,
|
|
35387
|
-
message: MESSAGE$
|
|
35551
|
+
message: MESSAGE$33
|
|
35388
35552
|
});
|
|
35389
35553
|
return;
|
|
35390
35554
|
}
|
|
@@ -35397,7 +35561,7 @@ const noCloneElement = defineRule({
|
|
|
35397
35561
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
35398
35562
|
context.report({
|
|
35399
35563
|
node: callee,
|
|
35400
|
-
message: MESSAGE$
|
|
35564
|
+
message: MESSAGE$33
|
|
35401
35565
|
});
|
|
35402
35566
|
}
|
|
35403
35567
|
} })
|
|
@@ -35417,7 +35581,7 @@ const getCallMethodName = (callee) => {
|
|
|
35417
35581
|
};
|
|
35418
35582
|
//#endregion
|
|
35419
35583
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
35420
|
-
const MESSAGE$
|
|
35584
|
+
const MESSAGE$32 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
35421
35585
|
const CONTEXT_MODULES = [
|
|
35422
35586
|
"react",
|
|
35423
35587
|
"use-context-selector",
|
|
@@ -35465,7 +35629,7 @@ const noCreateContextInRender = defineRule({
|
|
|
35465
35629
|
if (!componentOrHookName) return;
|
|
35466
35630
|
context.report({
|
|
35467
35631
|
node,
|
|
35468
|
-
message: `${MESSAGE$
|
|
35632
|
+
message: `${MESSAGE$32} (called inside "${componentOrHookName}")`
|
|
35469
35633
|
});
|
|
35470
35634
|
} })
|
|
35471
35635
|
});
|
|
@@ -36212,7 +36376,7 @@ const isProvenOneShotTestingLibraryComponent = (functionNode, filename, scopes)
|
|
|
36212
36376
|
};
|
|
36213
36377
|
//#endregion
|
|
36214
36378
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
36215
|
-
const MESSAGE$
|
|
36379
|
+
const MESSAGE$31 = "`createRef()` may escape or be observed beyond the render that created it, so a later render can replace the ref object and detach the observed one. Hoist a `useRef()` call to the component's unconditional top level instead.";
|
|
36216
36380
|
const isUseMemoCallbackArgument = (functionNode, scopes) => {
|
|
36217
36381
|
const parent = functionNode.parent;
|
|
36218
36382
|
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
@@ -36266,7 +36430,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
36266
36430
|
if (isCreateRefResultWriteOnly(node, context.filename, context.scopes)) return;
|
|
36267
36431
|
context.report({
|
|
36268
36432
|
node,
|
|
36269
|
-
message: MESSAGE$
|
|
36433
|
+
message: MESSAGE$31
|
|
36270
36434
|
});
|
|
36271
36435
|
} })
|
|
36272
36436
|
});
|
|
@@ -36406,7 +36570,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
36406
36570
|
});
|
|
36407
36571
|
//#endregion
|
|
36408
36572
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
36409
|
-
const MESSAGE$
|
|
36573
|
+
const MESSAGE$30 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
36410
36574
|
const noDanger = defineRule({
|
|
36411
36575
|
id: "no-danger",
|
|
36412
36576
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -36420,7 +36584,7 @@ const noDanger = defineRule({
|
|
|
36420
36584
|
if (!propAttribute) return;
|
|
36421
36585
|
context.report({
|
|
36422
36586
|
node: propAttribute.name,
|
|
36423
|
-
message: MESSAGE$
|
|
36587
|
+
message: MESSAGE$30
|
|
36424
36588
|
});
|
|
36425
36589
|
},
|
|
36426
36590
|
CallExpression(node) {
|
|
@@ -36432,7 +36596,7 @@ const noDanger = defineRule({
|
|
|
36432
36596
|
const propertyKey = property.key;
|
|
36433
36597
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
36434
36598
|
node: propertyKey,
|
|
36435
|
-
message: MESSAGE$
|
|
36599
|
+
message: MESSAGE$30
|
|
36436
36600
|
});
|
|
36437
36601
|
}
|
|
36438
36602
|
}
|
|
@@ -36455,7 +36619,7 @@ const isMeaningfulJsxChild = (child) => {
|
|
|
36455
36619
|
};
|
|
36456
36620
|
//#endregion
|
|
36457
36621
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
36458
|
-
const MESSAGE$
|
|
36622
|
+
const MESSAGE$29 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
36459
36623
|
const mergePropsShape = (target, source) => {
|
|
36460
36624
|
target.hasDangerously ||= source.hasDangerously;
|
|
36461
36625
|
target.hasChildren ||= source.hasChildren;
|
|
@@ -36530,7 +36694,7 @@ const noDangerWithChildren = defineRule({
|
|
|
36530
36694
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
36531
36695
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
36532
36696
|
node: opening,
|
|
36533
|
-
message: MESSAGE$
|
|
36697
|
+
message: MESSAGE$29
|
|
36534
36698
|
});
|
|
36535
36699
|
},
|
|
36536
36700
|
CallExpression(node) {
|
|
@@ -36543,7 +36707,7 @@ const noDangerWithChildren = defineRule({
|
|
|
36543
36707
|
const positionalChildren = node.arguments.slice(2);
|
|
36544
36708
|
if (positionalChildren.length > 1 || positionalChildren.some((argument) => !isNullishExpression(argument)) || propsShape.hasChildren) context.report({
|
|
36545
36709
|
node,
|
|
36546
|
-
message: MESSAGE$
|
|
36710
|
+
message: MESSAGE$29
|
|
36547
36711
|
});
|
|
36548
36712
|
}
|
|
36549
36713
|
})
|
|
@@ -36833,6 +36997,19 @@ const noDefaultProps = defineRule({
|
|
|
36833
36997
|
} })
|
|
36834
36998
|
});
|
|
36835
36999
|
//#endregion
|
|
37000
|
+
//#region src/plugin/utils/is-component-function.ts
|
|
37001
|
+
const isFunctionAssignedToComponent = (functionNode) => {
|
|
37002
|
+
let cursor = functionNode.parent ?? null;
|
|
37003
|
+
while (isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
37004
|
+
if (isNodeOfType(cursor, "VariableDeclarator") && isNodeOfType(cursor.id, "Identifier") && isUppercaseName(cursor.id.name)) return true;
|
|
37005
|
+
return isNodeOfType(cursor, "ExportDefaultDeclaration");
|
|
37006
|
+
};
|
|
37007
|
+
const isComponentFunction$1 = (node) => {
|
|
37008
|
+
if (!isFunctionLike$1(node)) return false;
|
|
37009
|
+
if (isNodeOfType(node, "FunctionDeclaration")) return !node.id || node.id.name === "default" || isUppercaseName(node.id.name) || isNodeOfType(node.parent, "ExportDefaultDeclaration");
|
|
37010
|
+
return isFunctionAssignedToComponent(node);
|
|
37011
|
+
};
|
|
37012
|
+
//#endregion
|
|
36836
37013
|
//#region src/plugin/utils/create-component-prop-stack-tracker.ts
|
|
36837
37014
|
const extractDestructuredPropNames = (params) => {
|
|
36838
37015
|
const propNames = /* @__PURE__ */ new Set();
|
|
@@ -37518,7 +37695,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
37518
37695
|
//#endregion
|
|
37519
37696
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
37520
37697
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
37521
|
-
const MESSAGE$
|
|
37698
|
+
const MESSAGE$28 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
37522
37699
|
const getEnclosingLifecycleFunction = (setStateCall) => {
|
|
37523
37700
|
let ancestor = setStateCall.parent;
|
|
37524
37701
|
while (ancestor) {
|
|
@@ -37645,7 +37822,7 @@ const noDidMountSetState = defineRule({
|
|
|
37645
37822
|
}
|
|
37646
37823
|
context.report({
|
|
37647
37824
|
node: node.callee,
|
|
37648
|
-
message: MESSAGE$
|
|
37825
|
+
message: MESSAGE$28
|
|
37649
37826
|
});
|
|
37650
37827
|
} };
|
|
37651
37828
|
}
|
|
@@ -37663,7 +37840,7 @@ const findEnclosingClass = (node) => {
|
|
|
37663
37840
|
//#endregion
|
|
37664
37841
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
37665
37842
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
37666
|
-
const MESSAGE$
|
|
37843
|
+
const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
37667
37844
|
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
37668
37845
|
const EQUALITY_OPERATORS = new Set([
|
|
37669
37846
|
"==",
|
|
@@ -38057,7 +38234,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
38057
38234
|
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
38058
38235
|
context.report({
|
|
38059
38236
|
node: node.callee,
|
|
38060
|
-
message: MESSAGE$
|
|
38237
|
+
message: MESSAGE$27
|
|
38061
38238
|
});
|
|
38062
38239
|
} };
|
|
38063
38240
|
}
|
|
@@ -38080,7 +38257,7 @@ const isStateMemberExpression = (node) => {
|
|
|
38080
38257
|
};
|
|
38081
38258
|
//#endregion
|
|
38082
38259
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
38083
|
-
const MESSAGE$
|
|
38260
|
+
const MESSAGE$26 = "Mutating `this.state` by hand never triggers a redraw on its own & a later setState can overwrite it, so use `this.setState` instead.";
|
|
38084
38261
|
const shouldIgnoreMutation = (node) => {
|
|
38085
38262
|
let isConstructor = false;
|
|
38086
38263
|
let isInsideCallExpression = false;
|
|
@@ -38102,7 +38279,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
38102
38279
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
38103
38280
|
context.report({
|
|
38104
38281
|
node: reportNode,
|
|
38105
|
-
message: MESSAGE$
|
|
38282
|
+
message: MESSAGE$26
|
|
38106
38283
|
});
|
|
38107
38284
|
};
|
|
38108
38285
|
const noDirectMutationState = defineRule({
|
|
@@ -38453,7 +38630,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
38453
38630
|
});
|
|
38454
38631
|
//#endregion
|
|
38455
38632
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
38456
|
-
const MESSAGE$
|
|
38633
|
+
const MESSAGE$25 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
38457
38634
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
38458
38635
|
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
38459
38636
|
const noDocumentWrite = defineRule({
|
|
@@ -38470,7 +38647,7 @@ const noDocumentWrite = defineRule({
|
|
|
38470
38647
|
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
38471
38648
|
context.report({
|
|
38472
38649
|
node,
|
|
38473
|
-
message: MESSAGE$
|
|
38650
|
+
message: MESSAGE$25
|
|
38474
38651
|
});
|
|
38475
38652
|
} })
|
|
38476
38653
|
});
|
|
@@ -41810,7 +41987,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
41810
41987
|
"ReactDOM",
|
|
41811
41988
|
"ReactDom"
|
|
41812
41989
|
]);
|
|
41813
|
-
const MESSAGE$
|
|
41990
|
+
const MESSAGE$24 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
41814
41991
|
const noFindDomNode = defineRule({
|
|
41815
41992
|
id: "no-find-dom-node",
|
|
41816
41993
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -41821,7 +41998,7 @@ const noFindDomNode = defineRule({
|
|
|
41821
41998
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
41822
41999
|
if (isImportedFromModule(node, callee.name, "react-dom")) context.report({
|
|
41823
42000
|
node: callee,
|
|
41824
|
-
message: MESSAGE$
|
|
42001
|
+
message: MESSAGE$24
|
|
41825
42002
|
});
|
|
41826
42003
|
return;
|
|
41827
42004
|
}
|
|
@@ -41832,7 +42009,7 @@ const noFindDomNode = defineRule({
|
|
|
41832
42009
|
if (callee.property.name !== "findDOMNode") return;
|
|
41833
42010
|
context.report({
|
|
41834
42011
|
node: callee.property,
|
|
41835
|
-
message: MESSAGE$
|
|
42012
|
+
message: MESSAGE$24
|
|
41836
42013
|
});
|
|
41837
42014
|
}
|
|
41838
42015
|
} })
|
|
@@ -42970,7 +43147,7 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
42970
43147
|
});
|
|
42971
43148
|
//#endregion
|
|
42972
43149
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
42973
|
-
const MESSAGE$
|
|
43150
|
+
const MESSAGE$23 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
42974
43151
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
42975
43152
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
42976
43153
|
title: "Lazy image with high fetchPriority",
|
|
@@ -42984,7 +43161,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
42984
43161
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
42985
43162
|
context.report({
|
|
42986
43163
|
node: node.name,
|
|
42987
|
-
message: MESSAGE$
|
|
43164
|
+
message: MESSAGE$23
|
|
42988
43165
|
});
|
|
42989
43166
|
} })
|
|
42990
43167
|
});
|
|
@@ -43173,7 +43350,7 @@ const noImpureStateUpdater = defineRule({
|
|
|
43173
43350
|
});
|
|
43174
43351
|
//#endregion
|
|
43175
43352
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
43176
|
-
const MESSAGE$
|
|
43353
|
+
const MESSAGE$22 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
43177
43354
|
const REACT_USE_REF_OPTIONS = {
|
|
43178
43355
|
allowGlobalReactNamespace: false,
|
|
43179
43356
|
allowUnboundBareCalls: false
|
|
@@ -43274,7 +43451,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
43274
43451
|
if (!inputTypeValues || !inputTypeValues.every((inputTypeValue) => inputTypeValue.toLowerCase() === "checkbox")) return;
|
|
43275
43452
|
context.report({
|
|
43276
43453
|
node: indeterminateAttribute,
|
|
43277
|
-
message: MESSAGE$
|
|
43454
|
+
message: MESSAGE$22
|
|
43278
43455
|
});
|
|
43279
43456
|
},
|
|
43280
43457
|
CallExpression(node) {
|
|
@@ -43283,7 +43460,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
43283
43460
|
if (!isProvenHtmlInputElement(receiver, context.scopes)) return;
|
|
43284
43461
|
context.report({
|
|
43285
43462
|
node,
|
|
43286
|
-
message: MESSAGE$
|
|
43463
|
+
message: MESSAGE$22
|
|
43287
43464
|
});
|
|
43288
43465
|
}
|
|
43289
43466
|
};
|
|
@@ -43571,7 +43748,7 @@ const noIsMounted = defineRule({
|
|
|
43571
43748
|
});
|
|
43572
43749
|
//#endregion
|
|
43573
43750
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
43574
|
-
const MESSAGE$
|
|
43751
|
+
const MESSAGE$21 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
43575
43752
|
const isJsonMethodCall = (node, method) => {
|
|
43576
43753
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
43577
43754
|
const callee = node.callee;
|
|
@@ -43635,13 +43812,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
43635
43812
|
if (isCatchParameterRoundTrip(firstArgument)) return;
|
|
43636
43813
|
context.report({
|
|
43637
43814
|
node,
|
|
43638
|
-
message: MESSAGE$
|
|
43815
|
+
message: MESSAGE$21
|
|
43639
43816
|
});
|
|
43640
43817
|
} })
|
|
43641
43818
|
});
|
|
43642
43819
|
//#endregion
|
|
43643
43820
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
43644
|
-
const MESSAGE$
|
|
43821
|
+
const MESSAGE$20 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
43645
43822
|
const isJsxElementTypeReference = (node) => {
|
|
43646
43823
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
43647
43824
|
const typeName = node.typeName;
|
|
@@ -43693,7 +43870,7 @@ const noJsxElementType = defineRule({
|
|
|
43693
43870
|
if (isJsxImported) return;
|
|
43694
43871
|
for (const typeAnnotation of flaggedAnnotations) context.report({
|
|
43695
43872
|
node: typeAnnotation,
|
|
43696
|
-
message: MESSAGE$
|
|
43873
|
+
message: MESSAGE$20
|
|
43697
43874
|
});
|
|
43698
43875
|
}
|
|
43699
43876
|
};
|
|
@@ -44974,7 +45151,7 @@ const noMoment = defineRule({
|
|
|
44974
45151
|
});
|
|
44975
45152
|
//#endregion
|
|
44976
45153
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
44977
|
-
const MESSAGE$
|
|
45154
|
+
const MESSAGE$19 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
44978
45155
|
const resolveSettings$16 = (settings) => {
|
|
44979
45156
|
const reactDoctor = settings?.["react-doctor"];
|
|
44980
45157
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -45326,7 +45503,7 @@ const noMultiComp = defineRule({
|
|
|
45326
45503
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
45327
45504
|
for (const component of flagged.slice(1)) context.report({
|
|
45328
45505
|
node: component.reportNode,
|
|
45329
|
-
message: MESSAGE$
|
|
45506
|
+
message: MESSAGE$19
|
|
45330
45507
|
});
|
|
45331
45508
|
} };
|
|
45332
45509
|
}
|
|
@@ -45539,7 +45716,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
45539
45716
|
};
|
|
45540
45717
|
//#endregion
|
|
45541
45718
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
45542
|
-
const MESSAGE$
|
|
45719
|
+
const MESSAGE$18 = "This reducer changes state in place, so your update is silently skipped.";
|
|
45543
45720
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
45544
45721
|
"copyWithin",
|
|
45545
45722
|
"fill",
|
|
@@ -45743,7 +45920,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
45743
45920
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
45744
45921
|
context.report({
|
|
45745
45922
|
node: options.crossFileConsumerCallSite,
|
|
45746
|
-
message: `${MESSAGE$
|
|
45923
|
+
message: `${MESSAGE$18} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
45747
45924
|
});
|
|
45748
45925
|
return;
|
|
45749
45926
|
}
|
|
@@ -45752,7 +45929,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
45752
45929
|
reportedNodes.add(mutation.node);
|
|
45753
45930
|
context.report({
|
|
45754
45931
|
node: mutation.node,
|
|
45755
|
-
message: MESSAGE$
|
|
45932
|
+
message: MESSAGE$18
|
|
45756
45933
|
});
|
|
45757
45934
|
}
|
|
45758
45935
|
};
|
|
@@ -46159,7 +46336,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
46159
46336
|
});
|
|
46160
46337
|
//#endregion
|
|
46161
46338
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
46162
|
-
const MESSAGE$
|
|
46339
|
+
const MESSAGE$17 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
46163
46340
|
const KEYBOARD_HANDLER_PROP_NAMES = [
|
|
46164
46341
|
"onKeyDown",
|
|
46165
46342
|
"onKeyUp",
|
|
@@ -46278,7 +46455,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46278
46455
|
if (numeric === null) {
|
|
46279
46456
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node) && !isFocusOperable(node) && !hasJsxSpreadAttribute$1(node.attributes)) context.report({
|
|
46280
46457
|
node: tabIndex,
|
|
46281
|
-
message: MESSAGE$
|
|
46458
|
+
message: MESSAGE$17
|
|
46282
46459
|
});
|
|
46283
46460
|
return;
|
|
46284
46461
|
}
|
|
@@ -46300,7 +46477,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46300
46477
|
if (!roleAttribute) {
|
|
46301
46478
|
context.report({
|
|
46302
46479
|
node: tabIndex,
|
|
46303
|
-
message: MESSAGE$
|
|
46480
|
+
message: MESSAGE$17
|
|
46304
46481
|
});
|
|
46305
46482
|
return;
|
|
46306
46483
|
}
|
|
@@ -46314,7 +46491,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
46314
46491
|
}
|
|
46315
46492
|
context.report({
|
|
46316
46493
|
node: tabIndex,
|
|
46317
|
-
message: MESSAGE$
|
|
46494
|
+
message: MESSAGE$17
|
|
46318
46495
|
});
|
|
46319
46496
|
} };
|
|
46320
46497
|
}
|
|
@@ -48520,7 +48697,7 @@ const noRandomKey = defineRule({
|
|
|
48520
48697
|
});
|
|
48521
48698
|
//#endregion
|
|
48522
48699
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
48523
|
-
const MESSAGE$
|
|
48700
|
+
const MESSAGE$16 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
48524
48701
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
48525
48702
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
48526
48703
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -48546,13 +48723,13 @@ const noReactChildren = defineRule({
|
|
|
48546
48723
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
48547
48724
|
context.report({
|
|
48548
48725
|
node: calleeOuter,
|
|
48549
|
-
message: MESSAGE$
|
|
48726
|
+
message: MESSAGE$16
|
|
48550
48727
|
});
|
|
48551
48728
|
return;
|
|
48552
48729
|
}
|
|
48553
48730
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
48554
48731
|
node: calleeOuter,
|
|
48555
|
-
message: MESSAGE$
|
|
48732
|
+
message: MESSAGE$16
|
|
48556
48733
|
});
|
|
48557
48734
|
} })
|
|
48558
48735
|
});
|
|
@@ -49434,7 +49611,7 @@ const noRenderPropChildren = defineRule({
|
|
|
49434
49611
|
});
|
|
49435
49612
|
//#endregion
|
|
49436
49613
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
49437
|
-
const MESSAGE$
|
|
49614
|
+
const MESSAGE$15 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
49438
49615
|
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
49439
49616
|
const isUsedAsReturnValue = (parent) => {
|
|
49440
49617
|
if (!parent) return false;
|
|
@@ -49452,7 +49629,7 @@ const noRenderReturnValue = defineRule({
|
|
|
49452
49629
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
49453
49630
|
context.report({
|
|
49454
49631
|
node: node.callee,
|
|
49455
|
-
message: MESSAGE$
|
|
49632
|
+
message: MESSAGE$15
|
|
49456
49633
|
});
|
|
49457
49634
|
} })
|
|
49458
49635
|
});
|
|
@@ -50879,7 +51056,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
50879
51056
|
});
|
|
50880
51057
|
//#endregion
|
|
50881
51058
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
50882
|
-
const MESSAGE$
|
|
51059
|
+
const MESSAGE$14 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
50883
51060
|
const noSetState = defineRule({
|
|
50884
51061
|
id: "no-set-state",
|
|
50885
51062
|
title: "Local class state forbidden",
|
|
@@ -50894,7 +51071,7 @@ const noSetState = defineRule({
|
|
|
50894
51071
|
if (!getParentComponent(node)) return;
|
|
50895
51072
|
context.report({
|
|
50896
51073
|
node: node.callee,
|
|
50897
|
-
message: MESSAGE$
|
|
51074
|
+
message: MESSAGE$14
|
|
50898
51075
|
});
|
|
50899
51076
|
} })
|
|
50900
51077
|
});
|
|
@@ -51264,7 +51441,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
51264
51441
|
};
|
|
51265
51442
|
//#endregion
|
|
51266
51443
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
51267
|
-
const MESSAGE$
|
|
51444
|
+
const MESSAGE$13 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
51268
51445
|
const DEFAULT_HANDLERS = [
|
|
51269
51446
|
"onClick",
|
|
51270
51447
|
"onMouseDown",
|
|
@@ -51379,7 +51556,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51379
51556
|
if (!roleAttribute || !roleAttribute.value) {
|
|
51380
51557
|
context.report({
|
|
51381
51558
|
node: node.name,
|
|
51382
|
-
message: MESSAGE$
|
|
51559
|
+
message: MESSAGE$13
|
|
51383
51560
|
});
|
|
51384
51561
|
return;
|
|
51385
51562
|
}
|
|
@@ -51389,7 +51566,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51389
51566
|
if (isRecognizedRoleString(attributeValue.value)) return;
|
|
51390
51567
|
context.report({
|
|
51391
51568
|
node: node.name,
|
|
51392
|
-
message: MESSAGE$
|
|
51569
|
+
message: MESSAGE$13
|
|
51393
51570
|
});
|
|
51394
51571
|
return;
|
|
51395
51572
|
}
|
|
@@ -51397,7 +51574,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51397
51574
|
if (!settings.allowExpressionValues) {
|
|
51398
51575
|
context.report({
|
|
51399
51576
|
node: node.name,
|
|
51400
|
-
message: MESSAGE$
|
|
51577
|
+
message: MESSAGE$13
|
|
51401
51578
|
});
|
|
51402
51579
|
return;
|
|
51403
51580
|
}
|
|
@@ -51405,7 +51582,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51405
51582
|
if (isStaticNullishExpression(expression)) {
|
|
51406
51583
|
context.report({
|
|
51407
51584
|
node: node.name,
|
|
51408
|
-
message: MESSAGE$
|
|
51585
|
+
message: MESSAGE$13
|
|
51409
51586
|
});
|
|
51410
51587
|
return;
|
|
51411
51588
|
}
|
|
@@ -51415,7 +51592,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51415
51592
|
if (branches.some((branch) => branch.role !== null && isRecognizedRoleString(branch.role))) return;
|
|
51416
51593
|
context.report({
|
|
51417
51594
|
node: node.name,
|
|
51418
|
-
message: MESSAGE$
|
|
51595
|
+
message: MESSAGE$13
|
|
51419
51596
|
});
|
|
51420
51597
|
return;
|
|
51421
51598
|
}
|
|
@@ -51423,7 +51600,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
51423
51600
|
}
|
|
51424
51601
|
context.report({
|
|
51425
51602
|
node: node.name,
|
|
51426
|
-
message: MESSAGE$
|
|
51603
|
+
message: MESSAGE$13
|
|
51427
51604
|
});
|
|
51428
51605
|
} };
|
|
51429
51606
|
}
|
|
@@ -51529,7 +51706,7 @@ const noStringRefs = defineRule({
|
|
|
51529
51706
|
});
|
|
51530
51707
|
//#endregion
|
|
51531
51708
|
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
51532
|
-
const MESSAGE$
|
|
51709
|
+
const MESSAGE$12 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
51533
51710
|
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
51534
51711
|
const PUBLIC_ASSET_PATH_PATTERN = /(?:^|\/)public\//i;
|
|
51535
51712
|
const noSyncXhr = defineRule({
|
|
@@ -51550,7 +51727,7 @@ const noSyncXhr = defineRule({
|
|
|
51550
51727
|
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
51551
51728
|
context.report({
|
|
51552
51729
|
node,
|
|
51553
|
-
message: MESSAGE$
|
|
51730
|
+
message: MESSAGE$12
|
|
51554
51731
|
});
|
|
51555
51732
|
};
|
|
51556
51733
|
return {
|
|
@@ -51575,7 +51752,7 @@ const noSyncXhr = defineRule({
|
|
|
51575
51752
|
});
|
|
51576
51753
|
//#endregion
|
|
51577
51754
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
51578
|
-
const MESSAGE$
|
|
51755
|
+
const MESSAGE$11 = "This value is `undefined` because function components have no `this`.";
|
|
51579
51756
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
51580
51757
|
let ancestor = node.parent;
|
|
51581
51758
|
while (ancestor) {
|
|
@@ -51663,7 +51840,7 @@ const noThisInSfc = defineRule({
|
|
|
51663
51840
|
if (functionHasOwnThisMemberWrite(enclosingFunction)) return;
|
|
51664
51841
|
context.report({
|
|
51665
51842
|
node,
|
|
51666
|
-
message: MESSAGE$
|
|
51843
|
+
message: MESSAGE$11
|
|
51667
51844
|
});
|
|
51668
51845
|
} };
|
|
51669
51846
|
}
|
|
@@ -54033,7 +54210,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
54033
54210
|
//#endregion
|
|
54034
54211
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
54035
54212
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
54036
|
-
const MESSAGE$
|
|
54213
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
54037
54214
|
const resolveSettings$7 = (settings) => {
|
|
54038
54215
|
const reactDoctor = settings?.["react-doctor"];
|
|
54039
54216
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -54067,7 +54244,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
54067
54244
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
54068
54245
|
context.report({
|
|
54069
54246
|
node: node.callee,
|
|
54070
|
-
message: MESSAGE$
|
|
54247
|
+
message: MESSAGE$10
|
|
54071
54248
|
});
|
|
54072
54249
|
} };
|
|
54073
54250
|
}
|
|
@@ -55867,7 +56044,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
55867
56044
|
});
|
|
55868
56045
|
//#endregion
|
|
55869
56046
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
55870
|
-
const MESSAGE$
|
|
56047
|
+
const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
55871
56048
|
const preactPreferOndblclick = defineRule({
|
|
55872
56049
|
id: "preact-prefer-ondblclick",
|
|
55873
56050
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -55882,7 +56059,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
55882
56059
|
if (!onDoubleClickAttribute) return;
|
|
55883
56060
|
context.report({
|
|
55884
56061
|
node: onDoubleClickAttribute,
|
|
55885
|
-
message: MESSAGE$
|
|
56062
|
+
message: MESSAGE$9
|
|
55886
56063
|
});
|
|
55887
56064
|
} })
|
|
55888
56065
|
});
|
|
@@ -56122,7 +56299,7 @@ const preferExplicitVariants = defineRule({
|
|
|
56122
56299
|
});
|
|
56123
56300
|
//#endregion
|
|
56124
56301
|
//#region src/plugin/rules/react-builtins/prefer-function-component.ts
|
|
56125
|
-
const MESSAGE$
|
|
56302
|
+
const MESSAGE$8 = "This class component keeps behavior in lifecycle methods, so state and effects are harder to follow than in a hook-based function component.";
|
|
56126
56303
|
const resolveSettings$4 = (settings) => {
|
|
56127
56304
|
const reactDoctor = settings?.["react-doctor"];
|
|
56128
56305
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.preferFunctionComponent ?? {} : {};
|
|
@@ -56161,7 +56338,7 @@ const preferFunctionComponent = defineRule({
|
|
|
56161
56338
|
const reportNode = node.id ?? node;
|
|
56162
56339
|
context.report({
|
|
56163
56340
|
node: reportNode,
|
|
56164
|
-
message: MESSAGE$
|
|
56341
|
+
message: MESSAGE$8
|
|
56165
56342
|
});
|
|
56166
56343
|
};
|
|
56167
56344
|
return {
|
|
@@ -57482,8 +57659,7 @@ const preferUseReducer = defineRule({
|
|
|
57482
57659
|
//#endregion
|
|
57483
57660
|
//#region src/plugin/rules/security-scan/utils/is-public-debug-artifact-path.ts
|
|
57484
57661
|
const LOCALE_DIRECTORY_PATTERN = /(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i;
|
|
57485
|
-
const
|
|
57486
|
-
const isPublicDebugArtifactPath = (relativePath) => isBrowserArtifactPath(relativePath, GENERATED_BUNDLE_FILE_PATTERN.test(relativePath)) && !LOCALE_DIRECTORY_PATTERN.test(relativePath) && PUBLIC_DEBUG_ARTIFACT_PATH_PATTERN.test(relativePath);
|
|
57662
|
+
const isPublicDebugArtifactPath = (relativePath) => isBrowserArtifactPath(relativePath, GENERATED_BUNDLE_FILE_PATTERN.test(relativePath)) && !LOCALE_DIRECTORY_PATTERN.test(relativePath) && /(?:^|\/)(?:\.env(?:\.[^/]*)?|[^/]*(?:debug|crash|trace|stack|report|dump|phpinfo)[^/]*\.(?:txt|log|json|html?)|[^/]+\.log)$/i.test(relativePath);
|
|
57487
57663
|
//#endregion
|
|
57488
57664
|
//#region src/plugin/rules/security-scan/public-debug-artifact.ts
|
|
57489
57665
|
const publicDebugArtifact = defineRule({
|
|
@@ -58362,7 +58538,7 @@ const reactCompilerNoManualMemoization = defineRule({
|
|
|
58362
58538
|
});
|
|
58363
58539
|
//#endregion
|
|
58364
58540
|
//#region src/plugin/rules/react-builtins/react-in-jsx-scope.ts
|
|
58365
|
-
const MESSAGE$
|
|
58541
|
+
const MESSAGE$7 = "This JSX crashes because `React` isn't in scope.";
|
|
58366
58542
|
const reactInJsxScope = defineRule({
|
|
58367
58543
|
id: "react-in-jsx-scope",
|
|
58368
58544
|
title: "React not in scope for JSX",
|
|
@@ -58374,21 +58550,21 @@ const reactInJsxScope = defineRule({
|
|
|
58374
58550
|
if (findVariableInitializer(node, "React")) return;
|
|
58375
58551
|
context.report({
|
|
58376
58552
|
node: node.name,
|
|
58377
|
-
message: MESSAGE$
|
|
58553
|
+
message: MESSAGE$7
|
|
58378
58554
|
});
|
|
58379
58555
|
},
|
|
58380
58556
|
JSXFragment(node) {
|
|
58381
58557
|
if (findVariableInitializer(node, "React")) return;
|
|
58382
58558
|
context.report({
|
|
58383
58559
|
node: node.openingFragment,
|
|
58384
|
-
message: MESSAGE$
|
|
58560
|
+
message: MESSAGE$7
|
|
58385
58561
|
});
|
|
58386
58562
|
}
|
|
58387
58563
|
})
|
|
58388
58564
|
});
|
|
58389
58565
|
//#endregion
|
|
58390
58566
|
//#region src/plugin/rules/security/react-markdown-unsanitized-raw-html.ts
|
|
58391
|
-
const MESSAGE$
|
|
58567
|
+
const MESSAGE$6 = "React Markdown parses dynamic raw HTML when `rehype-raw` is enabled. Add `rehype-sanitize` to `rehypePlugins` or sanitize the markdown before rendering it.";
|
|
58392
58568
|
const REACT_MARKDOWN_MODULE = "react-markdown";
|
|
58393
58569
|
const REHYPE_RAW_MODULE = "rehype-raw";
|
|
58394
58570
|
const REHYPE_SANITIZE_MODULE = "rehype-sanitize";
|
|
@@ -58540,7 +58716,7 @@ const reactMarkdownUnsanitizedRawHtml = defineRule({
|
|
|
58540
58716
|
if (pluginEntries.some((entry) => isPluginFromModule(entry, REHYPE_SANITIZE_MODULE, context.scopes, /* @__PURE__ */ new Set())) || !hasDynamicUnsanitizedChildren(node, context.scopes)) return;
|
|
58541
58717
|
context.report({
|
|
58542
58718
|
node,
|
|
58543
|
-
message: MESSAGE$
|
|
58719
|
+
message: MESSAGE$6
|
|
58544
58720
|
});
|
|
58545
58721
|
} }))
|
|
58546
58722
|
});
|
|
@@ -58695,7 +58871,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
58695
58871
|
});
|
|
58696
58872
|
//#endregion
|
|
58697
58873
|
//#region src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts
|
|
58698
|
-
const MESSAGE$
|
|
58874
|
+
const MESSAGE$5 = "Your component re-renders on every dispatched action when useSelector returns a new object or array.";
|
|
58699
58875
|
const isConciseBodyReturningCollection = (functionNode) => {
|
|
58700
58876
|
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
58701
58877
|
const rawBody = functionNode.body;
|
|
@@ -58730,7 +58906,7 @@ const reduxUseselectorReturnsNewCollection = defineRule({
|
|
|
58730
58906
|
if (!isConciseBodyReturningCollection(selectorArgument)) return;
|
|
58731
58907
|
context.report({
|
|
58732
58908
|
node,
|
|
58733
|
-
message: MESSAGE$
|
|
58909
|
+
message: MESSAGE$5
|
|
58734
58910
|
});
|
|
58735
58911
|
}
|
|
58736
58912
|
};
|
|
@@ -59785,7 +59961,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
59785
59961
|
const isRepositorySecretFilePath = (relativePath) => DOTENV_FILE_PATTERN.test(relativePath) || /(?:^|\/)\.npmrc$/.test(relativePath) || /(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(relativePath);
|
|
59786
59962
|
//#endregion
|
|
59787
59963
|
//#region src/plugin/rules/security-scan/repository-secret-file.ts
|
|
59788
|
-
const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env(
|
|
59964
|
+
const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
|
|
59789
59965
|
const repositorySecretFile = defineRule({
|
|
59790
59966
|
id: "repository-secret-file",
|
|
59791
59967
|
title: "Secret file checked into repository",
|
|
@@ -59857,7 +60033,7 @@ const functionBodyHasReturnWithValue = (functionNode) => {
|
|
|
59857
60033
|
//#endregion
|
|
59858
60034
|
//#region src/plugin/rules/react-builtins/require-render-return.ts
|
|
59859
60035
|
const RENDER_METHOD_NAME = "render";
|
|
59860
|
-
const MESSAGE$
|
|
60036
|
+
const MESSAGE$4 = "Your users see nothing because this `render` method returns nothing.";
|
|
59861
60037
|
const isStaticRenderKey = (key) => {
|
|
59862
60038
|
if (isNodeOfType(key, "Identifier")) return key.name === RENDER_METHOD_NAME;
|
|
59863
60039
|
if (isNodeOfType(key, "Literal")) return key.value === RENDER_METHOD_NAME;
|
|
@@ -59917,7 +60093,7 @@ const requireRenderReturn = defineRule({
|
|
|
59917
60093
|
if (functionBodyHasReturnWithValue(functionNode)) return;
|
|
59918
60094
|
context.report({
|
|
59919
60095
|
node: host.reportNode,
|
|
59920
|
-
message: MESSAGE$
|
|
60096
|
+
message: MESSAGE$4
|
|
59921
60097
|
});
|
|
59922
60098
|
};
|
|
59923
60099
|
return {
|
|
@@ -68469,7 +68645,6 @@ const countOwnScopeHookCalls = (functionNode, scopes, settings, countedFunctionN
|
|
|
68469
68645
|
};
|
|
68470
68646
|
const MIN_HOOK_CALLS_FOR_RENDER_SCOPE = 2;
|
|
68471
68647
|
const RENDER_SCOPE_FACTORY_NAME_PATTERN = /^_?(?:init|create|make|build)(?:[A-Z0-9_]|$)/;
|
|
68472
|
-
const HOOK_NAMESPACE_NAME_PATTERN = /Hooks?$/;
|
|
68473
68648
|
const isLocalNonHookFunctionCallee = (call, scopes, settings, countedFunctionNodes) => {
|
|
68474
68649
|
const callee = call.callee;
|
|
68475
68650
|
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
@@ -68494,15 +68669,6 @@ const isPackageImportedNonReactHookCallee = (call) => {
|
|
|
68494
68669
|
if (PATH_ALIAS_IMPORT_PATTERN.test(importSource)) return false;
|
|
68495
68670
|
return !isReactEcosystemImportSource(importSource);
|
|
68496
68671
|
};
|
|
68497
|
-
const isDefaultImportedClassApiMemberCallee = (call) => {
|
|
68498
|
-
const callee = call.callee;
|
|
68499
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
68500
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
68501
|
-
if (HOOK_NAMESPACE_NAME_PATTERN.test(callee.object.name)) return false;
|
|
68502
|
-
const importBinding = getImportBindingForName(call, callee.object.name);
|
|
68503
|
-
if (!importBinding || importBinding.exportedName !== "default") return false;
|
|
68504
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importBinding.source);
|
|
68505
|
-
};
|
|
68506
68672
|
const isProjectOwnedMdxComponentsGetter = (call) => {
|
|
68507
68673
|
const callee = call.callee;
|
|
68508
68674
|
if (!isNodeOfType(callee, "Identifier") || callee.name !== "useMDXComponents") return false;
|
|
@@ -68652,7 +68818,6 @@ const rulesOfHooks = defineRule({
|
|
|
68652
68818
|
return;
|
|
68653
68819
|
}
|
|
68654
68820
|
if (isInsideClassComponent(node)) {
|
|
68655
|
-
if (isPackageImportedNonReactHookCallee(node) || isDefaultImportedClassApiMemberCallee(node)) return;
|
|
68656
68821
|
context.report({
|
|
68657
68822
|
node: node.callee,
|
|
68658
68823
|
message: buildClassComponentMessage(hookName)
|
|
@@ -68759,7 +68924,7 @@ const rulesOfHooks = defineRule({
|
|
|
68759
68924
|
});
|
|
68760
68925
|
//#endregion
|
|
68761
68926
|
//#region src/plugin/rules/a11y/scope.ts
|
|
68762
|
-
const MESSAGE$
|
|
68927
|
+
const MESSAGE$3 = "The `scope` attribute only works on `<th>` cells, so screen readers get no table-header help from it here.";
|
|
68763
68928
|
const scope = defineRule({
|
|
68764
68929
|
id: "scope",
|
|
68765
68930
|
title: "scope attribute on non-th element",
|
|
@@ -68774,7 +68939,7 @@ const scope = defineRule({
|
|
|
68774
68939
|
if (!HTML_TAGS.has(tag)) return;
|
|
68775
68940
|
if (tag !== "th") context.report({
|
|
68776
68941
|
node: scopeAttribute,
|
|
68777
|
-
message: MESSAGE$
|
|
68942
|
+
message: MESSAGE$3
|
|
68778
68943
|
});
|
|
68779
68944
|
} })
|
|
68780
68945
|
});
|
|
@@ -68785,13 +68950,13 @@ const secretInFallback = defineRule({
|
|
|
68785
68950
|
recommendation: "Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",
|
|
68786
68951
|
scan: scanByPattern({
|
|
68787
68952
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
68788
|
-
pattern: /\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!
|
|
68953
|
+
pattern: /\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
68789
68954
|
message: "A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset."
|
|
68790
68955
|
})
|
|
68791
68956
|
});
|
|
68792
68957
|
//#endregion
|
|
68793
68958
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
68794
|
-
const MESSAGE$
|
|
68959
|
+
const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
68795
68960
|
const resolveSettings$2 = (settings) => {
|
|
68796
68961
|
const reactDoctor = settings?.["react-doctor"];
|
|
68797
68962
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.selfClosingComp ?? {} : {};
|
|
@@ -68830,7 +68995,7 @@ const selfClosingComp = defineRule({
|
|
|
68830
68995
|
if (isHtmlTag && !settings.html) return;
|
|
68831
68996
|
context.report({
|
|
68832
68997
|
node: node.openingElement,
|
|
68833
|
-
message: MESSAGE$
|
|
68998
|
+
message: MESSAGE$2
|
|
68834
68999
|
});
|
|
68835
69000
|
} };
|
|
68836
69001
|
}
|
|
@@ -69055,30 +69220,7 @@ const isAuthGuardName = (calleeName) => {
|
|
|
69055
69220
|
//#endregion
|
|
69056
69221
|
//#region src/plugin/utils/is-non-privileged-server-action.ts
|
|
69057
69222
|
const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
|
|
69058
|
-
const
|
|
69059
|
-
const LOCALE_PREFERENCE_NOUN_TOKENS = new Set(["language", "locale"]);
|
|
69060
|
-
const COOKIE_DELETION_METHOD_NAME = "delete";
|
|
69061
|
-
const isCacheOrNavigationCall = (node) => {
|
|
69062
|
-
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier")) return false;
|
|
69063
|
-
return NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name) && getImportSourceForName(node, node.callee.name) !== null;
|
|
69064
|
-
};
|
|
69065
|
-
const isCallerScopedLocalePreferenceCall = (node) => {
|
|
69066
|
-
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier") || node.arguments?.length !== 1) return false;
|
|
69067
|
-
const nameTokens = tokenizeIdentifierWords(node.callee.name);
|
|
69068
|
-
if (!nameTokens.some((token) => LOCALE_PREFERENCE_VERB_TOKENS.has(token))) return false;
|
|
69069
|
-
if (!nameTokens.some((token) => LOCALE_PREFERENCE_NOUN_TOKENS.has(token))) return false;
|
|
69070
|
-
const importSource = getImportSourceForName(node, node.callee.name)?.toLowerCase();
|
|
69071
|
-
return Boolean(importSource?.includes("i18n") || importSource?.includes("locale"));
|
|
69072
|
-
};
|
|
69073
|
-
const isCallerScopedCookieCall = (node, cookieBindingNames) => {
|
|
69074
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
69075
|
-
if (isNodeOfType(node.callee, "Identifier")) return node.callee.name === "cookies" && getImportSourceForName(node, node.callee.name) === "next/headers";
|
|
69076
|
-
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== COOKIE_DELETION_METHOD_NAME) return false;
|
|
69077
|
-
const receiver = stripParenExpression(node.callee.object);
|
|
69078
|
-
if (isNodeOfType(receiver, "Identifier")) return cookieBindingNames.has(receiver.name);
|
|
69079
|
-
return isCookiesOrAwaitedCookiesCall(receiver) && getImportSourceForName(receiver, "cookies") === "next/headers";
|
|
69080
|
-
};
|
|
69081
|
-
const isKnownNonDataEffectCall = (node, cookieBindingNames) => isCacheOrNavigationCall(node) || isCallerScopedLocalePreferenceCall(node) || isCallerScopedCookieCall(node, cookieBindingNames);
|
|
69223
|
+
const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name) && getImportSourceForName(node, node.callee.name) !== null;
|
|
69082
69224
|
const unwrapExpression = (node) => {
|
|
69083
69225
|
let current = node;
|
|
69084
69226
|
while (current) {
|
|
@@ -69094,10 +69236,10 @@ const unwrapExpression = (node) => {
|
|
|
69094
69236
|
}
|
|
69095
69237
|
return null;
|
|
69096
69238
|
};
|
|
69097
|
-
const isDataExposingValue = (node
|
|
69239
|
+
const isDataExposingValue = (node) => {
|
|
69098
69240
|
const value = unwrapExpression(node);
|
|
69099
69241
|
if (!value) return false;
|
|
69100
|
-
if (
|
|
69242
|
+
if (isCacheOrNavigationCall(value)) return false;
|
|
69101
69243
|
return !isLiteralOnlyExpression(value);
|
|
69102
69244
|
};
|
|
69103
69245
|
const isLiteralOnlyExpression = (node) => {
|
|
@@ -69115,23 +69257,22 @@ const getReturnedOrThrownArgument = (node) => {
|
|
|
69115
69257
|
if (isNodeOfType(node, "ThrowStatement")) return node.argument ?? null;
|
|
69116
69258
|
return null;
|
|
69117
69259
|
};
|
|
69118
|
-
const isDataExposingReturnOrThrow = (node
|
|
69119
|
-
const isPrivilegedEffect = (node
|
|
69260
|
+
const isDataExposingReturnOrThrow = (node) => isDataExposingValue(getReturnedOrThrownArgument(node));
|
|
69261
|
+
const isPrivilegedEffect = (node) => isNodeOfType(node, "CallExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" || isDataExposingReturnOrThrow(node);
|
|
69120
69262
|
const isNonPrivilegedServerAction = (functionNode) => {
|
|
69121
69263
|
const functionBody = functionNode.body;
|
|
69122
69264
|
if (!functionBody) return false;
|
|
69123
|
-
|
|
69124
|
-
if (!isNodeOfType(functionBody, "BlockStatement") && isDataExposingValue(functionBody, cookieBindingNames)) return false;
|
|
69265
|
+
if (!isNodeOfType(functionBody, "BlockStatement") && isDataExposingValue(functionBody)) return false;
|
|
69125
69266
|
let hasNonDataEffectCall = false;
|
|
69126
69267
|
let hasPrivilegedEffect = false;
|
|
69127
69268
|
walkAst(functionBody, (child) => {
|
|
69128
69269
|
if (hasPrivilegedEffect) return false;
|
|
69129
69270
|
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
69130
|
-
if (
|
|
69271
|
+
if (isCacheOrNavigationCall(child)) {
|
|
69131
69272
|
hasNonDataEffectCall = true;
|
|
69132
69273
|
return;
|
|
69133
69274
|
}
|
|
69134
|
-
if (isPrivilegedEffect(child
|
|
69275
|
+
if (isPrivilegedEffect(child)) {
|
|
69135
69276
|
hasPrivilegedEffect = true;
|
|
69136
69277
|
return false;
|
|
69137
69278
|
}
|
|
@@ -69210,34 +69351,6 @@ const getAuthScanRoots = (functionNode) => {
|
|
|
69210
69351
|
if (isNodeOfType(bodyNode, "BlockStatement")) return (bodyNode.body ?? []).slice(0, 10);
|
|
69211
69352
|
return [bodyNode];
|
|
69212
69353
|
};
|
|
69213
|
-
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
69214
|
-
const TEST_APP_SOURCE_PATH_PATTERN = /(?:^|[/\\])test[/\\](?:app|src)[/\\]/;
|
|
69215
|
-
const containsJsxOutsideNestedFunctions = (rootNode) => {
|
|
69216
|
-
let containsJsx = false;
|
|
69217
|
-
walkAst(rootNode, (child) => {
|
|
69218
|
-
if (containsJsx) return false;
|
|
69219
|
-
if (child !== rootNode && isFunctionLike$1(child)) return false;
|
|
69220
|
-
if (isNodeOfType(child, "JSXElement") || isNodeOfType(child, "JSXFragment")) {
|
|
69221
|
-
containsJsx = true;
|
|
69222
|
-
return false;
|
|
69223
|
-
}
|
|
69224
|
-
});
|
|
69225
|
-
return containsJsx;
|
|
69226
|
-
};
|
|
69227
|
-
const isComponentLikeServerExport = (candidate) => {
|
|
69228
|
-
if (!COMPONENT_NAME_PATTERN.test(candidate.displayName)) return false;
|
|
69229
|
-
const functionBody = candidate.functionNode.body;
|
|
69230
|
-
if (!isNodeOfType(functionBody, "BlockStatement")) return containsJsxOutsideNestedFunctions(functionBody);
|
|
69231
|
-
let hasReturnedJsx = false;
|
|
69232
|
-
walkAst(functionBody, (child) => {
|
|
69233
|
-
if (hasReturnedJsx) return false;
|
|
69234
|
-
if (child !== functionBody && isFunctionLike$1(child)) return false;
|
|
69235
|
-
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
69236
|
-
hasReturnedJsx = containsJsxOutsideNestedFunctions(child.argument);
|
|
69237
|
-
return false;
|
|
69238
|
-
});
|
|
69239
|
-
return hasReturnedJsx;
|
|
69240
|
-
};
|
|
69241
69354
|
const CREDENTIAL_MERGE_TAIL_TOKENS = {
|
|
69242
69355
|
sign: new Set([
|
|
69243
69356
|
"in",
|
|
@@ -69302,7 +69415,6 @@ const isCredentialEstablishingActionName = (actionName) => {
|
|
|
69302
69415
|
const hasPublicNameToken = (actionName) => tokenizeIdentifierWords(actionName).includes("public");
|
|
69303
69416
|
const inspectServerAction = (candidate, fileHasUseServerDirective, allowedFunctionNames, context) => {
|
|
69304
69417
|
if (!(fileHasUseServerDirective || hasUseServerDirective(candidate.functionNode))) return;
|
|
69305
|
-
if (isComponentLikeServerExport(candidate)) return;
|
|
69306
69418
|
if (isCredentialEstablishingActionName(candidate.displayName)) return;
|
|
69307
69419
|
if (hasPublicNameToken(candidate.displayName)) return;
|
|
69308
69420
|
if (containsAuthCheck(getAuthScanRoots(candidate.functionNode), allowedFunctionNames, GENERIC_AUTH_METHOD_NAMES)) return;
|
|
@@ -69340,42 +69452,18 @@ const serverAuthActions = defineRule({
|
|
|
69340
69452
|
title: "Unauthenticated server action can be called directly",
|
|
69341
69453
|
severity: "error",
|
|
69342
69454
|
recommendation: "Check auth before touching data because exported server actions can be called directly by unauthenticated clients.",
|
|
69343
|
-
create:
|
|
69344
|
-
const shouldSkipTestAppSource = Boolean(context.filename && TEST_APP_SOURCE_PATH_PATTERN.test(context.filename));
|
|
69455
|
+
create: (context) => {
|
|
69345
69456
|
let fileHasUseServerDirective = false;
|
|
69346
|
-
let programNode = null;
|
|
69347
|
-
const inspectedFunctions = /* @__PURE__ */ new Set();
|
|
69348
69457
|
const customAuthFunctionNames = getReactDoctorStringArraySetting(context.settings, "serverAuthFunctionNames");
|
|
69349
69458
|
const allowedFunctionNames = customAuthFunctionNames.length > 0 ? new Set([...AUTH_FUNCTION_NAMES, ...customAuthFunctionNames]) : AUTH_FUNCTION_NAMES;
|
|
69350
|
-
const inspect = (candidate) =>
|
|
69351
|
-
if (inspectedFunctions.has(candidate.functionNode)) return;
|
|
69352
|
-
inspectedFunctions.add(candidate.functionNode);
|
|
69353
|
-
inspectServerAction(candidate, fileHasUseServerDirective, allowedFunctionNames, context);
|
|
69354
|
-
};
|
|
69459
|
+
const inspect = (candidate) => inspectServerAction(candidate, fileHasUseServerDirective, allowedFunctionNames, context);
|
|
69355
69460
|
return {
|
|
69356
|
-
Program(
|
|
69357
|
-
|
|
69358
|
-
fileHasUseServerDirective = hasDirective(currentProgramNode, "use server");
|
|
69461
|
+
Program(programNode) {
|
|
69462
|
+
fileHasUseServerDirective = hasDirective(programNode, "use server");
|
|
69359
69463
|
},
|
|
69360
69464
|
ExportNamedDeclaration(node) {
|
|
69361
|
-
if (shouldSkipTestAppSource) return;
|
|
69362
69465
|
const declaration = node.declaration;
|
|
69363
|
-
if (!declaration)
|
|
69364
|
-
if (!programNode || node.source || node.exportKind === "type") return;
|
|
69365
|
-
for (const specifier of node.specifiers ?? []) {
|
|
69366
|
-
if (!isNodeOfType(specifier, "ExportSpecifier") || specifier.exportKind === "type") continue;
|
|
69367
|
-
const exportedName = isNodeOfType(specifier.exported, "Identifier") ? specifier.exported.name : isNodeOfType(specifier.exported, "Literal") && typeof specifier.exported.value === "string" ? specifier.exported.value : null;
|
|
69368
|
-
if (!exportedName) continue;
|
|
69369
|
-
const exportedValue = findExportedValue(programNode, exportedName);
|
|
69370
|
-
if (!isAsyncFunctionLikeNode(exportedValue)) continue;
|
|
69371
|
-
inspect({
|
|
69372
|
-
functionNode: exportedValue,
|
|
69373
|
-
displayName: isNodeOfType(specifier.local, "Identifier") ? specifier.local.name : exportedName,
|
|
69374
|
-
reportNode: specifier.local ?? specifier
|
|
69375
|
-
});
|
|
69376
|
-
}
|
|
69377
|
-
return;
|
|
69378
|
-
}
|
|
69466
|
+
if (!declaration) return;
|
|
69379
69467
|
if (isAsyncFunctionLikeNode(declaration)) {
|
|
69380
69468
|
if (!isNodeOfType(declaration, "FunctionDeclaration")) return;
|
|
69381
69469
|
inspect({
|
|
@@ -69388,23 +69476,11 @@ const serverAuthActions = defineRule({
|
|
|
69388
69476
|
if (isNodeOfType(declaration, "VariableDeclaration")) for (const candidate of collectCandidatesFromVariableDeclaration(declaration)) inspect(candidate);
|
|
69389
69477
|
},
|
|
69390
69478
|
ExportDefaultDeclaration(node) {
|
|
69391
|
-
|
|
69392
|
-
const directCandidate = getCandidateFromDefaultDeclaration(node);
|
|
69393
|
-
if (directCandidate) {
|
|
69394
|
-
inspect(directCandidate);
|
|
69395
|
-
return;
|
|
69396
|
-
}
|
|
69397
|
-
if (!programNode) return;
|
|
69398
|
-
const resolvedDefaultExport = findExportedValue(programNode, "default");
|
|
69399
|
-
const candidate = isAsyncFunctionLikeNode(resolvedDefaultExport) ? {
|
|
69400
|
-
functionNode: resolvedDefaultExport,
|
|
69401
|
-
displayName: isNodeOfType(node.declaration, "Identifier") ? node.declaration.name : "default",
|
|
69402
|
-
reportNode: node.declaration ?? node
|
|
69403
|
-
} : null;
|
|
69479
|
+
const candidate = getCandidateFromDefaultDeclaration(node);
|
|
69404
69480
|
if (candidate) inspect(candidate);
|
|
69405
69481
|
}
|
|
69406
69482
|
};
|
|
69407
|
-
}
|
|
69483
|
+
}
|
|
69408
69484
|
});
|
|
69409
69485
|
//#endregion
|
|
69410
69486
|
//#region src/plugin/rules/server/server-cache-with-object-literal.ts
|
|
@@ -69484,7 +69560,6 @@ const serverDedupProps = defineRule({
|
|
|
69484
69560
|
for (const attr of node.attributes ?? []) {
|
|
69485
69561
|
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
69486
69562
|
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
69487
|
-
if (attr.name.name === "key") continue;
|
|
69488
69563
|
if (!isNodeOfType(attr.value, "JSXExpressionContainer")) continue;
|
|
69489
69564
|
const expression = attr.value.expression;
|
|
69490
69565
|
if (!expression) continue;
|
|
@@ -70214,7 +70289,7 @@ const collectJsxRuntimeImports = (program) => {
|
|
|
70214
70289
|
};
|
|
70215
70290
|
//#endregion
|
|
70216
70291
|
//#region src/plugin/rules/react-builtins/style-prop-object.ts
|
|
70217
|
-
const MESSAGE$
|
|
70292
|
+
const MESSAGE$1 = "Your styles don't render because you passed the `style` prop a string instead of an object.";
|
|
70218
70293
|
const resolveSettings = (settings) => {
|
|
70219
70294
|
const reactDoctor = settings?.["react-doctor"];
|
|
70220
70295
|
return { allow: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.stylePropObject ?? {} : {}).allow ?? [] };
|
|
@@ -70321,7 +70396,7 @@ const stylePropObject = defineRule({
|
|
|
70321
70396
|
if (isNodeOfType(value, "Literal") && typeof value.value === "string") {
|
|
70322
70397
|
context.report({
|
|
70323
70398
|
node: attribute,
|
|
70324
|
-
message: MESSAGE$
|
|
70399
|
+
message: MESSAGE$1
|
|
70325
70400
|
});
|
|
70326
70401
|
return;
|
|
70327
70402
|
}
|
|
@@ -70329,7 +70404,7 @@ const stylePropObject = defineRule({
|
|
|
70329
70404
|
const innerExpression = value.expression;
|
|
70330
70405
|
if (innerExpression && innerExpression.type !== "JSXEmptyExpression" && isInvalidStyleExpression(innerExpression)) context.report({
|
|
70331
70406
|
node: attribute,
|
|
70332
|
-
message: MESSAGE$
|
|
70407
|
+
message: MESSAGE$1
|
|
70333
70408
|
});
|
|
70334
70409
|
}
|
|
70335
70410
|
}
|
|
@@ -70352,7 +70427,7 @@ const stylePropObject = defineRule({
|
|
|
70352
70427
|
if (!(isNodeOfType(key, "Identifier") && key.name === "style" || isNodeOfType(key, "Literal") && key.value === "style")) continue;
|
|
70353
70428
|
if (isInvalidStyleExpression(property.value)) context.report({
|
|
70354
70429
|
node: property,
|
|
70355
|
-
message: MESSAGE$
|
|
70430
|
+
message: MESSAGE$1
|
|
70356
70431
|
});
|
|
70357
70432
|
}
|
|
70358
70433
|
}
|
|
@@ -70688,7 +70763,7 @@ const svgFilterClickjackingRisk = defineRule({
|
|
|
70688
70763
|
});
|
|
70689
70764
|
//#endregion
|
|
70690
70765
|
//#region src/plugin/rules/a11y/tabindex-no-positive.ts
|
|
70691
|
-
const MESSAGE
|
|
70766
|
+
const MESSAGE = "Keyboard users get jumped out of the normal order by a positive `tabIndex`, so use `0` or `-1`.";
|
|
70692
70767
|
const tabindexNoPositive = defineRule({
|
|
70693
70768
|
id: "tabindex-no-positive",
|
|
70694
70769
|
title: "Positive tabindex value",
|
|
@@ -70708,7 +70783,7 @@ const tabindexNoPositive = defineRule({
|
|
|
70708
70783
|
} else if (tabIndex.value && isNodeOfType(tabIndex.value, "JSXExpressionContainer")) numericValue = parseJsxValue(tabIndex.value);
|
|
70709
70784
|
if (numericValue !== null && numericValue > 0) context.report({
|
|
70710
70785
|
node: tabIndex,
|
|
70711
|
-
message: MESSAGE
|
|
70786
|
+
message: MESSAGE
|
|
70712
70787
|
});
|
|
70713
70788
|
} })
|
|
70714
70789
|
});
|
|
@@ -71614,248 +71689,6 @@ const useLazyMotion = defineRule({
|
|
|
71614
71689
|
} })
|
|
71615
71690
|
});
|
|
71616
71691
|
//#endregion
|
|
71617
|
-
//#region src/plugin/rules/valtio/valtio-no-proxy-read-in-render.ts
|
|
71618
|
-
const VALTIO_REACT_MODULE_SOURCES = new Set(["valtio", "valtio/react"]);
|
|
71619
|
-
const REACT_DEPENDENCY_ARRAY_HOOK_NAMES = new Set([
|
|
71620
|
-
"useCallback",
|
|
71621
|
-
"useEffect",
|
|
71622
|
-
"useInsertionEffect",
|
|
71623
|
-
"useLayoutEffect",
|
|
71624
|
-
"useMemo"
|
|
71625
|
-
]);
|
|
71626
|
-
const MESSAGE = "Read this Valtio value from the useSnapshot result during render. Keep proxy reads for callbacks so render uses the tracked, consistent snapshot.";
|
|
71627
|
-
const isValtioImportSymbol = (symbol) => {
|
|
71628
|
-
if (symbol.kind !== "import") return false;
|
|
71629
|
-
const importDeclaration = symbol.declarationNode.parent;
|
|
71630
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && VALTIO_REACT_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
71631
|
-
};
|
|
71632
|
-
const isValtioUseSnapshotCall = (node, scopes) => {
|
|
71633
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
71634
|
-
const callee = stripParenExpression(node.callee);
|
|
71635
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
71636
|
-
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
71637
|
-
return Boolean(symbol && isValtioImportSymbol(symbol) && getImportedName(symbol.declarationNode) === "useSnapshot");
|
|
71638
|
-
}
|
|
71639
|
-
if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyName(callee) !== "useSnapshot") return false;
|
|
71640
|
-
const receiver = stripParenExpression(callee.object);
|
|
71641
|
-
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
71642
|
-
const namespaceSymbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
71643
|
-
return Boolean(namespaceSymbol && isValtioImportSymbol(namespaceSymbol) && isNodeOfType(namespaceSymbol.declarationNode, "ImportNamespaceSpecifier"));
|
|
71644
|
-
};
|
|
71645
|
-
const collectPatternBindingIdentifiers = (pattern) => {
|
|
71646
|
-
if (isNodeOfType(pattern, "Identifier")) return [pattern];
|
|
71647
|
-
if (isNodeOfType(pattern, "AssignmentPattern")) return collectPatternBindingIdentifiers(pattern.left);
|
|
71648
|
-
if (isNodeOfType(pattern, "RestElement")) return collectPatternBindingIdentifiers(pattern.argument);
|
|
71649
|
-
if (isNodeOfType(pattern, "ArrayPattern")) return pattern.elements.flatMap((element) => element ? collectPatternBindingIdentifiers(element) : []);
|
|
71650
|
-
if (isNodeOfType(pattern, "ObjectPattern")) return pattern.properties.flatMap((property) => {
|
|
71651
|
-
if (isNodeOfType(property, "Property")) return collectPatternBindingIdentifiers(property.value);
|
|
71652
|
-
return isNodeOfType(property, "RestElement") ? collectPatternBindingIdentifiers(property.argument) : [];
|
|
71653
|
-
});
|
|
71654
|
-
return [];
|
|
71655
|
-
};
|
|
71656
|
-
const collectAssignmentWriteTargets = (target) => {
|
|
71657
|
-
const candidate = stripParenExpression(target);
|
|
71658
|
-
if (isNodeOfType(candidate, "AssignmentPattern")) return collectAssignmentWriteTargets(candidate.left);
|
|
71659
|
-
if (isNodeOfType(candidate, "RestElement")) return collectAssignmentWriteTargets(candidate.argument);
|
|
71660
|
-
if (isNodeOfType(candidate, "ArrayPattern")) return candidate.elements.flatMap((element) => element ? collectAssignmentWriteTargets(element) : []);
|
|
71661
|
-
if (isNodeOfType(candidate, "ObjectPattern")) return candidate.properties.flatMap((property) => isNodeOfType(property, "Property") ? collectAssignmentWriteTargets(property.value) : collectAssignmentWriteTargets(property.argument));
|
|
71662
|
-
return [candidate];
|
|
71663
|
-
};
|
|
71664
|
-
const findPatternPathToBinding = (pattern, bindingIdentifier) => {
|
|
71665
|
-
if (pattern === bindingIdentifier) return [];
|
|
71666
|
-
if (isNodeOfType(pattern, "AssignmentPattern")) return findPatternPathToBinding(pattern.left, bindingIdentifier);
|
|
71667
|
-
if (isNodeOfType(pattern, "RestElement")) return null;
|
|
71668
|
-
if (isNodeOfType(pattern, "ArrayPattern")) {
|
|
71669
|
-
for (const [elementIndex, element] of pattern.elements.entries()) {
|
|
71670
|
-
if (!element) continue;
|
|
71671
|
-
const nestedPath = findPatternPathToBinding(element, bindingIdentifier);
|
|
71672
|
-
if (nestedPath) return [String(elementIndex), ...nestedPath];
|
|
71673
|
-
}
|
|
71674
|
-
return null;
|
|
71675
|
-
}
|
|
71676
|
-
if (!isNodeOfType(pattern, "ObjectPattern")) return null;
|
|
71677
|
-
for (const property of pattern.properties) {
|
|
71678
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
71679
|
-
const nestedPath = findPatternPathToBinding(property.value, bindingIdentifier);
|
|
71680
|
-
if (!nestedPath) continue;
|
|
71681
|
-
const propertyName = getStaticPropertyKeyName(property);
|
|
71682
|
-
return propertyName === null ? null : [propertyName, ...nestedPath];
|
|
71683
|
-
}
|
|
71684
|
-
return null;
|
|
71685
|
-
};
|
|
71686
|
-
const resolveProxyPath = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set(), allowedAliasSymbolIds = null, collectedAliasCaptures = null) => {
|
|
71687
|
-
const candidate = stripParenExpression(expression);
|
|
71688
|
-
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
71689
|
-
const receiverPath = resolveProxyPath(candidate.object, scopes, visitedSymbolIds, allowedAliasSymbolIds, collectedAliasCaptures);
|
|
71690
|
-
if (!receiverPath) return null;
|
|
71691
|
-
return {
|
|
71692
|
-
...receiverPath,
|
|
71693
|
-
properties: [...receiverPath.properties, getStaticPropertyName(candidate)]
|
|
71694
|
-
};
|
|
71695
|
-
}
|
|
71696
|
-
if (!isNodeOfType(candidate, "Identifier")) return null;
|
|
71697
|
-
const symbol = scopes.symbolFor(candidate);
|
|
71698
|
-
if (!symbol) return scopes.isGlobalReference(candidate) ? {
|
|
71699
|
-
rootKey: `global:${candidate.name}`,
|
|
71700
|
-
properties: []
|
|
71701
|
-
} : null;
|
|
71702
|
-
if (visitedSymbolIds.has(symbol.id)) return null;
|
|
71703
|
-
if (symbol.kind === "const" && symbol.initializer && isNodeOfType(symbol.declarationNode, "VariableDeclarator")) {
|
|
71704
|
-
const directAlias = symbol.declarationNode.id === symbol.bindingIdentifier;
|
|
71705
|
-
const initializerNode = directAlias ? symbol.initializer : symbol.declarationNode.init;
|
|
71706
|
-
if (!initializerNode) return {
|
|
71707
|
-
rootKey: `symbol:${symbol.id}`,
|
|
71708
|
-
properties: []
|
|
71709
|
-
};
|
|
71710
|
-
const initializer = stripParenExpression(initializerNode);
|
|
71711
|
-
const canResolveDirectAlias = directAlias && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"));
|
|
71712
|
-
const patternPath = directAlias ? null : findPatternPathToBinding(symbol.declarationNode.id, symbol.bindingIdentifier);
|
|
71713
|
-
if ((canResolveDirectAlias || patternPath) && (allowedAliasSymbolIds === null || allowedAliasSymbolIds.has(symbol.id))) {
|
|
71714
|
-
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
71715
|
-
nextVisitedSymbolIds.add(symbol.id);
|
|
71716
|
-
const initializerPath = resolveProxyPath(initializer, scopes, nextVisitedSymbolIds, allowedAliasSymbolIds, collectedAliasCaptures);
|
|
71717
|
-
if (initializerPath) {
|
|
71718
|
-
const aliasPath = {
|
|
71719
|
-
...initializerPath,
|
|
71720
|
-
properties: [...initializerPath.properties, ...patternPath ?? []]
|
|
71721
|
-
};
|
|
71722
|
-
const declarationEnd = symbol.declarationNode.range?.[1];
|
|
71723
|
-
if (typeof declarationEnd === "number") collectedAliasCaptures?.set(symbol.id, {
|
|
71724
|
-
declarationEnd,
|
|
71725
|
-
path: aliasPath
|
|
71726
|
-
});
|
|
71727
|
-
return aliasPath;
|
|
71728
|
-
}
|
|
71729
|
-
}
|
|
71730
|
-
}
|
|
71731
|
-
return {
|
|
71732
|
-
rootKey: `symbol:${symbol.id}`,
|
|
71733
|
-
properties: []
|
|
71734
|
-
};
|
|
71735
|
-
};
|
|
71736
|
-
const isScopeWithin = (candidateScope, ancestorScope) => {
|
|
71737
|
-
let currentScope = candidateScope;
|
|
71738
|
-
while (currentScope) {
|
|
71739
|
-
if (currentScope === ancestorScope) return true;
|
|
71740
|
-
currentScope = currentScope.parent;
|
|
71741
|
-
}
|
|
71742
|
-
return false;
|
|
71743
|
-
};
|
|
71744
|
-
const isPathPrefix = (prefix, candidate) => {
|
|
71745
|
-
if (prefix.rootKey !== candidate.rootKey || prefix.properties.length > candidate.properties.length) return false;
|
|
71746
|
-
return prefix.properties.every((propertyName, propertyIndex) => propertyName !== null && propertyName === candidate.properties[propertyIndex]);
|
|
71747
|
-
};
|
|
71748
|
-
const findOutermostMemberRead = (identifier) => {
|
|
71749
|
-
let currentExpression = findTransparentExpressionRoot(identifier);
|
|
71750
|
-
for (;;) {
|
|
71751
|
-
const parent = currentExpression.parent;
|
|
71752
|
-
if (!isNodeOfType(parent, "MemberExpression") || stripParenExpression(parent.object) !== stripParenExpression(currentExpression)) return currentExpression;
|
|
71753
|
-
currentExpression = findTransparentExpressionRoot(parent);
|
|
71754
|
-
}
|
|
71755
|
-
};
|
|
71756
|
-
const isReadPositionWithinAssignmentTarget = (expression) => {
|
|
71757
|
-
let currentNode = expression;
|
|
71758
|
-
let parentNode = currentNode.parent;
|
|
71759
|
-
while (parentNode) {
|
|
71760
|
-
if (isNodeOfType(parentNode, "MemberExpression") && parentNode.computed && parentNode.property === currentNode || isNodeOfType(parentNode, "Property") && parentNode.computed && parentNode.key === currentNode || isNodeOfType(parentNode, "AssignmentPattern") && parentNode.right === currentNode) return true;
|
|
71761
|
-
if (isNodeOfType(parentNode, "AssignmentExpression") || isNodeOfType(parentNode, "UpdateExpression") || isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "delete" || isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return false;
|
|
71762
|
-
currentNode = parentNode;
|
|
71763
|
-
parentNode = currentNode.parent;
|
|
71764
|
-
}
|
|
71765
|
-
return false;
|
|
71766
|
-
};
|
|
71767
|
-
const isSnapshotArgument = (expression, scopes) => {
|
|
71768
|
-
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
71769
|
-
const parent = expressionRoot.parent;
|
|
71770
|
-
return Boolean(parent && isNodeOfType(parent, "CallExpression") && isValtioUseSnapshotCall(parent, scopes) && parent.arguments[0] === expressionRoot);
|
|
71771
|
-
};
|
|
71772
|
-
const isStableProxyDependency = (expression, readPath, matchingTarget, scopes) => {
|
|
71773
|
-
if (readPath.properties.length !== matchingTarget.path.properties.length || !isPathPrefix(matchingTarget.path, readPath)) return false;
|
|
71774
|
-
const dependencyArray = findTransparentExpressionRoot(expression).parent;
|
|
71775
|
-
if (!isNodeOfType(dependencyArray, "ArrayExpression")) return false;
|
|
71776
|
-
const hookCall = dependencyArray.parent;
|
|
71777
|
-
return Boolean(isNodeOfType(hookCall, "CallExpression") && hookCall.arguments[1] === dependencyArray && isReactApiCall(hookCall, REACT_DEPENDENCY_ARRAY_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }));
|
|
71778
|
-
};
|
|
71779
|
-
const getSnapshotTarget = (callExpression, context) => {
|
|
71780
|
-
if (!isValtioUseSnapshotCall(callExpression, context.scopes)) return null;
|
|
71781
|
-
const proxyArgument = callExpression.arguments[0];
|
|
71782
|
-
if (!proxyArgument || isNodeOfType(proxyArgument, "SpreadElement")) return null;
|
|
71783
|
-
const path = resolveProxyPath(proxyArgument, context.scopes);
|
|
71784
|
-
if (!path || path.properties.some((propertyName) => propertyName === null)) return null;
|
|
71785
|
-
const callExpressionRoot = findTransparentExpressionRoot(callExpression);
|
|
71786
|
-
const declarator = callExpressionRoot.parent;
|
|
71787
|
-
if (!isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== callExpressionRoot || !isNodeOfType(declarator.parent, "VariableDeclaration")) return null;
|
|
71788
|
-
const ownerFunction = findRenderPhaseComponentOrHook(callExpression, context.scopes);
|
|
71789
|
-
if (!ownerFunction) return null;
|
|
71790
|
-
const snapshotBindingScopes = collectPatternBindingIdentifiers(declarator.id).flatMap((bindingIdentifier) => {
|
|
71791
|
-
const bindingSymbol = context.scopes.symbolFor(bindingIdentifier);
|
|
71792
|
-
return bindingSymbol ? [bindingSymbol.scope] : [];
|
|
71793
|
-
});
|
|
71794
|
-
const declarationEnd = declarator.range?.[1];
|
|
71795
|
-
if (snapshotBindingScopes.length === 0 || typeof declarationEnd !== "number") return null;
|
|
71796
|
-
return {
|
|
71797
|
-
declarationEnd,
|
|
71798
|
-
ownerFunction,
|
|
71799
|
-
path,
|
|
71800
|
-
snapshotBindingScopes
|
|
71801
|
-
};
|
|
71802
|
-
};
|
|
71803
|
-
const wasTargetReplacedBeforeRead = (target, readPosition, writeTargets, readAliasCaptures, context) => writeTargets.some((writeTarget) => {
|
|
71804
|
-
const writeNode = findTransparentExpressionRoot(writeTarget).parent;
|
|
71805
|
-
const writePosition = writeNode?.range?.[0];
|
|
71806
|
-
if (!writeNode || typeof writePosition !== "number" || writePosition <= target.declarationEnd || writePosition >= readPosition || findRenderPhaseComponentOrHook(writeNode, context.scopes) !== target.ownerFunction) return false;
|
|
71807
|
-
const writePath = resolveProxyPath(writeTarget, context.scopes, /* @__PURE__ */ new Set(), null);
|
|
71808
|
-
if (!writePath || !isPathPrefix(writePath, target.path)) return false;
|
|
71809
|
-
return ![...readAliasCaptures.values()].some((aliasCapture) => aliasCapture.declarationEnd < writePosition && isPathPrefix(writePath, aliasCapture.path));
|
|
71810
|
-
});
|
|
71811
|
-
const valtioNoProxyReadInRender = defineRule({
|
|
71812
|
-
id: "valtio-no-proxy-read-in-render",
|
|
71813
|
-
title: "Valtio proxy read during render",
|
|
71814
|
-
severity: "warn",
|
|
71815
|
-
recommendation: "Read reactive render values from useSnapshot. Reserve the mutable proxy for event handlers, effects, and other callbacks that need the latest value.",
|
|
71816
|
-
requires: ["valtio:1"],
|
|
71817
|
-
create: (context) => {
|
|
71818
|
-
const snapshotTargets = [];
|
|
71819
|
-
const identifierCandidates = [];
|
|
71820
|
-
const writeTargets = [];
|
|
71821
|
-
return {
|
|
71822
|
-
CallExpression(node) {
|
|
71823
|
-
const snapshotTarget = getSnapshotTarget(node, context);
|
|
71824
|
-
if (snapshotTarget) snapshotTargets.push(snapshotTarget);
|
|
71825
|
-
},
|
|
71826
|
-
Identifier(node) {
|
|
71827
|
-
if (context.scopes.referenceFor(node)) identifierCandidates.push(node);
|
|
71828
|
-
},
|
|
71829
|
-
AssignmentExpression(node) {
|
|
71830
|
-
writeTargets.push(...collectAssignmentWriteTargets(node.left));
|
|
71831
|
-
},
|
|
71832
|
-
UpdateExpression(node) {
|
|
71833
|
-
writeTargets.push(node.argument);
|
|
71834
|
-
},
|
|
71835
|
-
"Program:exit"() {
|
|
71836
|
-
const reportedExpressions = /* @__PURE__ */ new Set();
|
|
71837
|
-
for (const identifier of identifierCandidates) {
|
|
71838
|
-
const readExpression = findOutermostMemberRead(identifier);
|
|
71839
|
-
if (reportedExpressions.has(readExpression) || isWithinAssignmentTarget(readExpression) && !isReadPositionWithinAssignmentTarget(readExpression) || isSnapshotArgument(readExpression, context.scopes)) continue;
|
|
71840
|
-
const readAliasCaptures = /* @__PURE__ */ new Map();
|
|
71841
|
-
const readPath = resolveProxyPath(readExpression, context.scopes, /* @__PURE__ */ new Set(), null, readAliasCaptures);
|
|
71842
|
-
const readPosition = readExpression.range?.[0];
|
|
71843
|
-
const ownerFunction = findRenderPhaseComponentOrHook(readExpression, context.scopes);
|
|
71844
|
-
if (!readPath || typeof readPosition !== "number" || !ownerFunction) continue;
|
|
71845
|
-
const readScope = context.scopes.scopeFor(readExpression);
|
|
71846
|
-
const matchingTarget = snapshotTargets.find((target) => target.ownerFunction === ownerFunction && target.declarationEnd < readPosition && target.snapshotBindingScopes.some((bindingScope) => isScopeWithin(readScope, bindingScope)) && isPathPrefix(target.path, readPath) && (readAliasCaptures.size === 0 || [...readAliasCaptures.values()].some((aliasCapture) => isPathPrefix(aliasCapture.path, target.path) || isPathPrefix(target.path, aliasCapture.path) && readPath.properties.length > aliasCapture.path.properties.length)) && !wasTargetReplacedBeforeRead(target, readPosition, writeTargets, readAliasCaptures, context));
|
|
71847
|
-
if (!matchingTarget || isStableProxyDependency(readExpression, readPath, matchingTarget, context.scopes)) continue;
|
|
71848
|
-
reportedExpressions.add(readExpression);
|
|
71849
|
-
context.report({
|
|
71850
|
-
node: readExpression,
|
|
71851
|
-
message: MESSAGE
|
|
71852
|
-
});
|
|
71853
|
-
}
|
|
71854
|
-
}
|
|
71855
|
-
};
|
|
71856
|
-
}
|
|
71857
|
-
});
|
|
71858
|
-
//#endregion
|
|
71859
71692
|
//#region src/plugin/rules/react-builtins/void-dom-elements-no-children.ts
|
|
71860
71693
|
const VOID_DOM_ELEMENTS = new Set([
|
|
71861
71694
|
"area",
|
|
@@ -73547,6 +73380,18 @@ const reactDoctorRules = [
|
|
|
73547
73380
|
requires: [...new Set(["react", ...jsxNoScriptUrl.requires ?? []])]
|
|
73548
73381
|
}
|
|
73549
73382
|
},
|
|
73383
|
+
{
|
|
73384
|
+
key: "react-doctor/jsx-no-target-blank",
|
|
73385
|
+
id: "jsx-no-target-blank",
|
|
73386
|
+
source: "react-doctor",
|
|
73387
|
+
originallyExternal: true,
|
|
73388
|
+
rule: {
|
|
73389
|
+
...jsxNoTargetBlank,
|
|
73390
|
+
framework: "global",
|
|
73391
|
+
category: "Security",
|
|
73392
|
+
requires: [...new Set(["react", ...jsxNoTargetBlank.requires ?? []])]
|
|
73393
|
+
}
|
|
73394
|
+
},
|
|
73550
73395
|
{
|
|
73551
73396
|
key: "react-doctor/jsx-no-undef",
|
|
73552
73397
|
id: "jsx-no-undef",
|
|
@@ -77008,18 +76853,6 @@ const reactDoctorRules = [
|
|
|
77008
76853
|
category: "Performance"
|
|
77009
76854
|
}
|
|
77010
76855
|
},
|
|
77011
|
-
{
|
|
77012
|
-
key: "react-doctor/valtio-no-proxy-read-in-render",
|
|
77013
|
-
id: "valtio-no-proxy-read-in-render",
|
|
77014
|
-
source: "react-doctor",
|
|
77015
|
-
originallyExternal: false,
|
|
77016
|
-
rule: {
|
|
77017
|
-
...valtioNoProxyReadInRender,
|
|
77018
|
-
framework: "global",
|
|
77019
|
-
category: "Bugs",
|
|
77020
|
-
requires: [...new Set(["react", ...valtioNoProxyReadInRender.requires ?? []])]
|
|
77021
|
-
}
|
|
77022
|
-
},
|
|
77023
76856
|
{
|
|
77024
76857
|
key: "react-doctor/void-dom-elements-no-children",
|
|
77025
76858
|
id: "void-dom-elements-no-children",
|