oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.f45cb29
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 +294 -0
- package/dist/index.js +436 -140
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1861,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1861
|
});
|
|
1862
1862
|
//#endregion
|
|
1863
1863
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1864
|
+
const MESSAGE$57 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1865
|
const anchorHasContent = defineRule({
|
|
1866
1866
|
id: "anchor-has-content",
|
|
1867
1867
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1877,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1877
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1878
|
context.report({
|
|
1879
1879
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1880
|
+
message: MESSAGE$57
|
|
1881
1881
|
});
|
|
1882
1882
|
} })
|
|
1883
1883
|
});
|
|
@@ -2271,7 +2271,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2271
|
};
|
|
2272
2272
|
//#endregion
|
|
2273
2273
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2274
|
+
const MESSAGE$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2275
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2276
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2277
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2289
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2290
|
context.report({
|
|
2291
2291
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2292
|
+
message: MESSAGE$56
|
|
2293
2293
|
});
|
|
2294
2294
|
return;
|
|
2295
2295
|
}
|
|
2296
2296
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2297
|
context.report({
|
|
2298
2298
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2299
|
+
message: MESSAGE$56
|
|
2300
2300
|
});
|
|
2301
2301
|
} })
|
|
2302
2302
|
});
|
|
@@ -4201,6 +4201,58 @@ const asyncParallel = defineRule({
|
|
|
4201
4201
|
}
|
|
4202
4202
|
});
|
|
4203
4203
|
//#endregion
|
|
4204
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4205
|
+
const MESSAGE$55 = "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.";
|
|
4206
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4207
|
+
const STORAGE_GLOBALS = new Set([
|
|
4208
|
+
"window",
|
|
4209
|
+
"globalThis",
|
|
4210
|
+
"self"
|
|
4211
|
+
]);
|
|
4212
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4213
|
+
const isWebStorageObject = (node) => {
|
|
4214
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4215
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4216
|
+
return false;
|
|
4217
|
+
};
|
|
4218
|
+
const staticMemberName = (member) => {
|
|
4219
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4220
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4221
|
+
return null;
|
|
4222
|
+
};
|
|
4223
|
+
const authTokenInWebStorage = defineRule({
|
|
4224
|
+
id: "auth-token-in-web-storage",
|
|
4225
|
+
title: "Auth token in web storage",
|
|
4226
|
+
severity: "warn",
|
|
4227
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4228
|
+
create: (context) => ({
|
|
4229
|
+
CallExpression(node) {
|
|
4230
|
+
const callee = node.callee;
|
|
4231
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4232
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4233
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4234
|
+
const keyArgument = node.arguments?.[0];
|
|
4235
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4236
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4237
|
+
context.report({
|
|
4238
|
+
node,
|
|
4239
|
+
message: MESSAGE$55
|
|
4240
|
+
});
|
|
4241
|
+
},
|
|
4242
|
+
AssignmentExpression(node) {
|
|
4243
|
+
const target = node.left;
|
|
4244
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4245
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4246
|
+
const propertyName = staticMemberName(target);
|
|
4247
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4248
|
+
context.report({
|
|
4249
|
+
node: target,
|
|
4250
|
+
message: MESSAGE$55
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
})
|
|
4254
|
+
});
|
|
4255
|
+
//#endregion
|
|
4204
4256
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4205
4257
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4206
4258
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4572,7 +4624,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4572
4624
|
//#endregion
|
|
4573
4625
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
4626
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4575
|
-
const MESSAGE$
|
|
4627
|
+
const MESSAGE$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4576
4628
|
const KEY_HANDLERS = [
|
|
4577
4629
|
"onKeyUp",
|
|
4578
4630
|
"onKeyDown",
|
|
@@ -4604,7 +4656,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4604
4656
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4657
|
context.report({
|
|
4606
4658
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4659
|
+
message: MESSAGE$54
|
|
4608
4660
|
});
|
|
4609
4661
|
} };
|
|
4610
4662
|
}
|
|
@@ -4719,7 +4771,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4771
|
};
|
|
4720
4772
|
//#endregion
|
|
4721
4773
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
const MESSAGE$
|
|
4774
|
+
const MESSAGE$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4723
4775
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4776
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4777
|
"alt",
|
|
@@ -4880,7 +4932,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
4932
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
4933
|
context.report({
|
|
4882
4934
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
4935
|
+
message: MESSAGE$53
|
|
4884
4936
|
});
|
|
4885
4937
|
} };
|
|
4886
4938
|
}
|
|
@@ -5306,6 +5358,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5358
|
} })
|
|
5307
5359
|
});
|
|
5308
5360
|
//#endregion
|
|
5361
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5362
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5363
|
+
//#endregion
|
|
5364
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5365
|
+
const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5366
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5367
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5368
|
+
"aria-label",
|
|
5369
|
+
"aria-labelledby",
|
|
5370
|
+
"title"
|
|
5371
|
+
];
|
|
5372
|
+
const dialogHasAccessibleName = defineRule({
|
|
5373
|
+
id: "dialog-has-accessible-name",
|
|
5374
|
+
title: "Dialog without accessible name",
|
|
5375
|
+
severity: "warn",
|
|
5376
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5377
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5378
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5379
|
+
const tagName = node.name.name;
|
|
5380
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5381
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5382
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5383
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5384
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5385
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5386
|
+
context.report({
|
|
5387
|
+
node: node.name,
|
|
5388
|
+
message: MESSAGE$52
|
|
5389
|
+
});
|
|
5390
|
+
} })
|
|
5391
|
+
});
|
|
5392
|
+
//#endregion
|
|
5309
5393
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5394
|
const PRAGMA$2 = "React";
|
|
5311
5395
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5424,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5424
|
};
|
|
5341
5425
|
//#endregion
|
|
5342
5426
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5427
|
+
const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5428
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5429
|
"observer",
|
|
5346
5430
|
"lazy",
|
|
@@ -5543,7 +5627,7 @@ const displayName = defineRule({
|
|
|
5543
5627
|
const reportAt = (node) => {
|
|
5544
5628
|
context.report({
|
|
5545
5629
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5630
|
+
message: MESSAGE$51
|
|
5547
5631
|
});
|
|
5548
5632
|
};
|
|
5549
5633
|
return {
|
|
@@ -7691,7 +7775,7 @@ const forbidElements = defineRule({
|
|
|
7691
7775
|
});
|
|
7692
7776
|
//#endregion
|
|
7693
7777
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7778
|
+
const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7779
|
const forwardRefUsesRef = defineRule({
|
|
7696
7780
|
id: "forward-ref-uses-ref",
|
|
7697
7781
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7795,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7795
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7796
|
context.report({
|
|
7713
7797
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7798
|
+
message: MESSAGE$50
|
|
7715
7799
|
});
|
|
7716
7800
|
} })
|
|
7717
7801
|
});
|
|
@@ -7748,7 +7832,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7832
|
});
|
|
7749
7833
|
//#endregion
|
|
7750
7834
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
const MESSAGE$
|
|
7835
|
+
const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7752
7836
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7837
|
"h1",
|
|
7754
7838
|
"h2",
|
|
@@ -7781,7 +7865,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7865
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7866
|
context.report({
|
|
7783
7867
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7868
|
+
message: MESSAGE$49
|
|
7785
7869
|
});
|
|
7786
7870
|
} };
|
|
7787
7871
|
}
|
|
@@ -7919,7 +8003,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8003
|
});
|
|
7920
8004
|
//#endregion
|
|
7921
8005
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
const MESSAGE$
|
|
8006
|
+
const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7923
8007
|
const resolveSettings$38 = (settings) => {
|
|
7924
8008
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8009
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7967,7 +8051,7 @@ const htmlHasLang = defineRule({
|
|
|
7967
8051
|
if (!lang) {
|
|
7968
8052
|
context.report({
|
|
7969
8053
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8054
|
+
message: MESSAGE$48
|
|
7971
8055
|
});
|
|
7972
8056
|
return;
|
|
7973
8057
|
}
|
|
@@ -7975,13 +8059,13 @@ const htmlHasLang = defineRule({
|
|
|
7975
8059
|
if (verdict === "missing" || verdict === "empty") {
|
|
7976
8060
|
context.report({
|
|
7977
8061
|
node: lang,
|
|
7978
|
-
message: MESSAGE$
|
|
8062
|
+
message: MESSAGE$48
|
|
7979
8063
|
});
|
|
7980
8064
|
return;
|
|
7981
8065
|
}
|
|
7982
8066
|
if (hasSpread && !lang) context.report({
|
|
7983
8067
|
node: node.name,
|
|
7984
|
-
message: MESSAGE$
|
|
8068
|
+
message: MESSAGE$48
|
|
7985
8069
|
});
|
|
7986
8070
|
} };
|
|
7987
8071
|
}
|
|
@@ -8195,7 +8279,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8279
|
});
|
|
8196
8280
|
//#endregion
|
|
8197
8281
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
const MESSAGE$
|
|
8282
|
+
const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8199
8283
|
const evaluateTitleValue = (value) => {
|
|
8200
8284
|
if (!value) return "missing";
|
|
8201
8285
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8319,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8319
|
if (!titleAttr) {
|
|
8236
8320
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8321
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8322
|
+
message: MESSAGE$47
|
|
8239
8323
|
});
|
|
8240
8324
|
return;
|
|
8241
8325
|
}
|
|
8242
8326
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8327
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8328
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8329
|
+
message: MESSAGE$47
|
|
8246
8330
|
});
|
|
8247
8331
|
} })
|
|
8248
8332
|
});
|
|
@@ -8346,7 +8430,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8430
|
});
|
|
8347
8431
|
//#endregion
|
|
8348
8432
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
const MESSAGE$
|
|
8433
|
+
const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8350
8434
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8435
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8436
|
"image",
|
|
@@ -8411,7 +8495,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8495
|
if (!altAttribute) return;
|
|
8412
8496
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8497
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8498
|
+
message: MESSAGE$46
|
|
8415
8499
|
});
|
|
8416
8500
|
} };
|
|
8417
8501
|
}
|
|
@@ -10768,7 +10852,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10852
|
});
|
|
10769
10853
|
//#endregion
|
|
10770
10854
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10855
|
+
const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10856
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10857
|
"code",
|
|
10774
10858
|
"pre",
|
|
@@ -10804,7 +10888,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
10888
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
10889
|
context.report({
|
|
10806
10890
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
10891
|
+
message: MESSAGE$45
|
|
10808
10892
|
});
|
|
10809
10893
|
} })
|
|
10810
10894
|
});
|
|
@@ -10835,7 +10919,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
10919
|
};
|
|
10836
10920
|
//#endregion
|
|
10837
10921
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
10922
|
+
const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
10923
|
const CONTEXT_MODULES$1 = [
|
|
10840
10924
|
"react",
|
|
10841
10925
|
"use-context-selector",
|
|
@@ -10933,7 +11017,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11017
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11018
|
context.report({
|
|
10935
11019
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11020
|
+
message: MESSAGE$44
|
|
10937
11021
|
});
|
|
10938
11022
|
}
|
|
10939
11023
|
}
|
|
@@ -11019,7 +11103,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11103
|
};
|
|
11020
11104
|
//#endregion
|
|
11021
11105
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11106
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11107
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11108
|
"icon",
|
|
11025
11109
|
"Icon",
|
|
@@ -11288,7 +11372,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11372
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11373
|
context.report({
|
|
11290
11374
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11375
|
+
message: MESSAGE$43
|
|
11292
11376
|
});
|
|
11293
11377
|
}
|
|
11294
11378
|
};
|
|
@@ -11576,7 +11660,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11660
|
];
|
|
11577
11661
|
//#endregion
|
|
11578
11662
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11663
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11664
|
const isDataArrayPropName = (propName) => {
|
|
11581
11665
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11666
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11744,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11744
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11745
|
context.report({
|
|
11662
11746
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11747
|
+
message: MESSAGE$42
|
|
11664
11748
|
});
|
|
11665
11749
|
}
|
|
11666
11750
|
};
|
|
@@ -11918,7 +12002,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12002
|
]);
|
|
11919
12003
|
//#endregion
|
|
11920
12004
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12005
|
+
const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12006
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12007
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12008
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12208,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12208
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12209
|
context.report({
|
|
12126
12210
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12211
|
+
message: MESSAGE$41
|
|
12128
12212
|
});
|
|
12129
12213
|
}
|
|
12130
12214
|
};
|
|
@@ -12344,7 +12428,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12428
|
];
|
|
12345
12429
|
//#endregion
|
|
12346
12430
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12431
|
+
const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12432
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12433
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12434
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12516,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12516
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12517
|
context.report({
|
|
12434
12518
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12519
|
+
message: MESSAGE$40
|
|
12436
12520
|
});
|
|
12437
12521
|
}
|
|
12438
12522
|
};
|
|
@@ -12440,7 +12524,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12524
|
});
|
|
12441
12525
|
//#endregion
|
|
12442
12526
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12527
|
+
const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
12528
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12445
12529
|
const resolveSettings$28 = (settings) => {
|
|
12446
12530
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12565,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12565
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12566
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12567
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12568
|
+
message: MESSAGE$39
|
|
12485
12569
|
});
|
|
12486
12570
|
}
|
|
12487
12571
|
} };
|
|
@@ -12796,7 +12880,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
12880
|
});
|
|
12797
12881
|
//#endregion
|
|
12798
12882
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
12883
|
+
const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
|
|
12800
12884
|
const resolveSettings$25 = (settings) => {
|
|
12801
12885
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
12886
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +12921,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
12921
|
}
|
|
12838
12922
|
context.report({
|
|
12839
12923
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
12924
|
+
message: MESSAGE$38
|
|
12841
12925
|
});
|
|
12842
12926
|
}
|
|
12843
12927
|
} };
|
|
@@ -13065,7 +13149,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13149
|
});
|
|
13066
13150
|
//#endregion
|
|
13067
13151
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
const MESSAGE$
|
|
13152
|
+
const MESSAGE$37 = "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`.";
|
|
13069
13153
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13154
|
"aa",
|
|
13071
13155
|
"ab",
|
|
@@ -13277,7 +13361,7 @@ const lang = defineRule({
|
|
|
13277
13361
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13362
|
context.report({
|
|
13279
13363
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13364
|
+
message: MESSAGE$37
|
|
13281
13365
|
});
|
|
13282
13366
|
return;
|
|
13283
13367
|
}
|
|
@@ -13286,7 +13370,7 @@ const lang = defineRule({
|
|
|
13286
13370
|
if (value === null) return;
|
|
13287
13371
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13372
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13373
|
+
message: MESSAGE$37
|
|
13290
13374
|
});
|
|
13291
13375
|
} })
|
|
13292
13376
|
});
|
|
@@ -13330,7 +13414,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13414
|
});
|
|
13331
13415
|
//#endregion
|
|
13332
13416
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
const MESSAGE$
|
|
13417
|
+
const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13334
13418
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13419
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13420
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13455,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13455
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13456
|
context.report({
|
|
13373
13457
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13458
|
+
message: MESSAGE$36
|
|
13375
13459
|
});
|
|
13376
13460
|
return;
|
|
13377
13461
|
}
|
|
@@ -13388,7 +13472,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13472
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13473
|
})) context.report({
|
|
13390
13474
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13475
|
+
message: MESSAGE$36
|
|
13392
13476
|
});
|
|
13393
13477
|
} };
|
|
13394
13478
|
}
|
|
@@ -15189,7 +15273,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15273
|
});
|
|
15190
15274
|
//#endregion
|
|
15191
15275
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15276
|
+
const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15277
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15278
|
const noAccessKey = defineRule({
|
|
15195
15279
|
id: "no-access-key",
|
|
@@ -15206,7 +15290,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15290
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15291
|
context.report({
|
|
15208
15292
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15293
|
+
message: MESSAGE$35
|
|
15210
15294
|
});
|
|
15211
15295
|
return;
|
|
15212
15296
|
}
|
|
@@ -15216,7 +15300,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15300
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15301
|
context.report({
|
|
15218
15302
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15303
|
+
message: MESSAGE$35
|
|
15220
15304
|
});
|
|
15221
15305
|
}
|
|
15222
15306
|
} })
|
|
@@ -15699,7 +15783,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15699
15783
|
});
|
|
15700
15784
|
//#endregion
|
|
15701
15785
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
const MESSAGE$
|
|
15786
|
+
const MESSAGE$34 = "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.";
|
|
15703
15787
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15788
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15789
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15810,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15810
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15811
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15812
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15813
|
+
message: MESSAGE$34
|
|
15730
15814
|
});
|
|
15731
15815
|
} })
|
|
15732
15816
|
});
|
|
@@ -16094,7 +16178,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16178
|
});
|
|
16095
16179
|
//#endregion
|
|
16096
16180
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16181
|
+
const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16182
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16183
|
"every",
|
|
16100
16184
|
"filter",
|
|
@@ -16298,7 +16382,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16382
|
}
|
|
16299
16383
|
context.report({
|
|
16300
16384
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16385
|
+
message: MESSAGE$33
|
|
16302
16386
|
});
|
|
16303
16387
|
},
|
|
16304
16388
|
CallExpression(node) {
|
|
@@ -16318,15 +16402,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16402
|
if (propName !== "key") continue;
|
|
16319
16403
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16404
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16405
|
+
message: MESSAGE$33
|
|
16322
16406
|
});
|
|
16323
16407
|
}
|
|
16324
16408
|
}
|
|
16325
16409
|
})
|
|
16326
16410
|
});
|
|
16327
16411
|
//#endregion
|
|
16412
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16413
|
+
const MESSAGE$32 = "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.";
|
|
16414
|
+
const noAsyncEffectCallback = defineRule({
|
|
16415
|
+
id: "no-async-effect-callback",
|
|
16416
|
+
title: "Async effect callback",
|
|
16417
|
+
severity: "warn",
|
|
16418
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16419
|
+
create: (context) => ({ CallExpression(node) {
|
|
16420
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16421
|
+
const callback = getEffectCallback(node);
|
|
16422
|
+
if (!callback) return;
|
|
16423
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16424
|
+
if (!callback.async) return;
|
|
16425
|
+
context.report({
|
|
16426
|
+
node: callback,
|
|
16427
|
+
message: MESSAGE$32
|
|
16428
|
+
});
|
|
16429
|
+
} })
|
|
16430
|
+
});
|
|
16431
|
+
//#endregion
|
|
16328
16432
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
const MESSAGE$
|
|
16433
|
+
const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16330
16434
|
const resolveSettings$21 = (settings) => {
|
|
16331
16435
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16436
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,7 +16486,7 @@ const noAutofocus = defineRule({
|
|
|
16382
16486
|
}
|
|
16383
16487
|
context.report({
|
|
16384
16488
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16489
|
+
message: MESSAGE$31
|
|
16386
16490
|
});
|
|
16387
16491
|
} };
|
|
16388
16492
|
}
|
|
@@ -16632,6 +16736,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16736
|
}
|
|
16633
16737
|
});
|
|
16634
16738
|
//#endregion
|
|
16739
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16740
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16741
|
+
"FunctionDeclaration",
|
|
16742
|
+
"FunctionExpression",
|
|
16743
|
+
"ArrowFunctionExpression",
|
|
16744
|
+
"ClassDeclaration",
|
|
16745
|
+
"ClassExpression"
|
|
16746
|
+
]);
|
|
16747
|
+
const isReactImport$1 = (symbol) => {
|
|
16748
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16749
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16750
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16751
|
+
return importDeclaration.source.value === "react";
|
|
16752
|
+
};
|
|
16753
|
+
const getImportedName = (symbol) => {
|
|
16754
|
+
if (symbol.kind !== "import") return null;
|
|
16755
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16756
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16757
|
+
};
|
|
16758
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16759
|
+
if (symbol.kind !== "import") return false;
|
|
16760
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16761
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16762
|
+
};
|
|
16763
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16764
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16765
|
+
const symbol = scopes.symbolFor(callee);
|
|
16766
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16767
|
+
};
|
|
16768
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16769
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16770
|
+
if (callee.computed) return false;
|
|
16771
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16772
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16773
|
+
if (callee.property.name !== "createElement") return false;
|
|
16774
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16775
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16776
|
+
};
|
|
16777
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16778
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16779
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16780
|
+
};
|
|
16781
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16782
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16783
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16784
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16785
|
+
const nodeRecord = node;
|
|
16786
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16787
|
+
if (key === "parent") continue;
|
|
16788
|
+
const child = nodeRecord[key];
|
|
16789
|
+
if (Array.isArray(child)) {
|
|
16790
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16791
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16792
|
+
}
|
|
16793
|
+
return false;
|
|
16794
|
+
};
|
|
16795
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16796
|
+
//#endregion
|
|
16797
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16798
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16799
|
+
//#endregion
|
|
16800
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16801
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16802
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16803
|
+
const declaration = symbol.declarationNode;
|
|
16804
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16805
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16806
|
+
return false;
|
|
16807
|
+
};
|
|
16808
|
+
const noCallComponentAsFunction = defineRule({
|
|
16809
|
+
id: "no-call-component-as-function",
|
|
16810
|
+
title: "Component called as a function",
|
|
16811
|
+
severity: "warn",
|
|
16812
|
+
tags: ["test-noise"],
|
|
16813
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16814
|
+
create: (context) => {
|
|
16815
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16816
|
+
const candidateCalls = [];
|
|
16817
|
+
return {
|
|
16818
|
+
JSXOpeningElement(node) {
|
|
16819
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16820
|
+
},
|
|
16821
|
+
CallExpression(node) {
|
|
16822
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16823
|
+
node,
|
|
16824
|
+
callee: node.callee,
|
|
16825
|
+
name: node.callee.name
|
|
16826
|
+
});
|
|
16827
|
+
},
|
|
16828
|
+
"Program:exit"() {
|
|
16829
|
+
for (const candidate of candidateCalls) {
|
|
16830
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16831
|
+
if (!symbol) continue;
|
|
16832
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16833
|
+
node: candidate.node,
|
|
16834
|
+
message: message(candidate.name)
|
|
16835
|
+
});
|
|
16836
|
+
}
|
|
16837
|
+
}
|
|
16838
|
+
};
|
|
16839
|
+
}
|
|
16840
|
+
});
|
|
16841
|
+
//#endregion
|
|
16635
16842
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
16843
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
16844
|
//#endregion
|
|
@@ -16783,7 +16990,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
16990
|
});
|
|
16784
16991
|
//#endregion
|
|
16785
16992
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
const MESSAGE$
|
|
16993
|
+
const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16787
16994
|
const noChildrenProp = defineRule({
|
|
16788
16995
|
id: "no-children-prop",
|
|
16789
16996
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17002,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17002
|
if (node.name.name !== "children") return;
|
|
16796
17003
|
context.report({
|
|
16797
17004
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17005
|
+
message: MESSAGE$30
|
|
16799
17006
|
});
|
|
16800
17007
|
},
|
|
16801
17008
|
CallExpression(node) {
|
|
@@ -16808,7 +17015,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17015
|
const propertyKey = property.key;
|
|
16809
17016
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17017
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17018
|
+
message: MESSAGE$30
|
|
16812
17019
|
});
|
|
16813
17020
|
}
|
|
16814
17021
|
}
|
|
@@ -16816,7 +17023,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17023
|
});
|
|
16817
17024
|
//#endregion
|
|
16818
17025
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
const MESSAGE$
|
|
17026
|
+
const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16820
17027
|
const noCloneElement = defineRule({
|
|
16821
17028
|
id: "no-clone-element",
|
|
16822
17029
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17036,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17036
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17037
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17038
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17039
|
+
message: MESSAGE$29
|
|
16833
17040
|
});
|
|
16834
17041
|
return;
|
|
16835
17042
|
}
|
|
@@ -16842,7 +17049,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17049
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17050
|
context.report({
|
|
16844
17051
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17052
|
+
message: MESSAGE$29
|
|
16846
17053
|
});
|
|
16847
17054
|
}
|
|
16848
17055
|
} })
|
|
@@ -16891,7 +17098,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17098
|
};
|
|
16892
17099
|
//#endregion
|
|
16893
17100
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17101
|
+
const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17102
|
const CONTEXT_MODULES = [
|
|
16896
17103
|
"react",
|
|
16897
17104
|
"use-context-selector",
|
|
@@ -16927,7 +17134,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17134
|
if (!componentOrHookName) return;
|
|
16928
17135
|
context.report({
|
|
16929
17136
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17137
|
+
message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
|
|
17138
|
+
});
|
|
17139
|
+
} })
|
|
17140
|
+
});
|
|
17141
|
+
//#endregion
|
|
17142
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17143
|
+
const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17144
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17145
|
+
id: "no-create-ref-in-function-component",
|
|
17146
|
+
title: "createRef in function component",
|
|
17147
|
+
severity: "warn",
|
|
17148
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17149
|
+
create: (context) => ({ CallExpression(node) {
|
|
17150
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17151
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17152
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17153
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17154
|
+
}
|
|
17155
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17156
|
+
if (!enclosingFunction) return;
|
|
17157
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17158
|
+
if (!displayName) return;
|
|
17159
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17160
|
+
context.report({
|
|
17161
|
+
node,
|
|
17162
|
+
message: MESSAGE$27
|
|
16931
17163
|
});
|
|
16932
17164
|
} })
|
|
16933
17165
|
});
|
|
@@ -17067,7 +17299,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17299
|
});
|
|
17068
17300
|
//#endregion
|
|
17069
17301
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17302
|
+
const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17303
|
const noDanger = defineRule({
|
|
17072
17304
|
id: "no-danger",
|
|
17073
17305
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17080,7 +17312,7 @@ const noDanger = defineRule({
|
|
|
17080
17312
|
if (!propAttribute) return;
|
|
17081
17313
|
context.report({
|
|
17082
17314
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17315
|
+
message: MESSAGE$26
|
|
17084
17316
|
});
|
|
17085
17317
|
},
|
|
17086
17318
|
CallExpression(node) {
|
|
@@ -17092,7 +17324,7 @@ const noDanger = defineRule({
|
|
|
17092
17324
|
const propertyKey = property.key;
|
|
17093
17325
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17326
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17327
|
+
message: MESSAGE$26
|
|
17096
17328
|
});
|
|
17097
17329
|
}
|
|
17098
17330
|
}
|
|
@@ -17100,7 +17332,7 @@ const noDanger = defineRule({
|
|
|
17100
17332
|
});
|
|
17101
17333
|
//#endregion
|
|
17102
17334
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17335
|
+
const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17336
|
const isLineBreak = (child) => {
|
|
17105
17337
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17338
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17402,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17402
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17403
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17404
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17405
|
+
message: MESSAGE$25
|
|
17174
17406
|
});
|
|
17175
17407
|
},
|
|
17176
17408
|
CallExpression(node) {
|
|
@@ -17182,7 +17414,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17414
|
if (!propsShape.hasDangerously) return;
|
|
17183
17415
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17416
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17417
|
+
message: MESSAGE$25
|
|
17186
17418
|
});
|
|
17187
17419
|
}
|
|
17188
17420
|
})
|
|
@@ -17759,7 +17991,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
17991
|
//#endregion
|
|
17760
17992
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
17993
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
17994
|
+
const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
17995
|
const resolveSettings$20 = (settings) => {
|
|
17764
17996
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
17997
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18010,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18010
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18011
|
context.report({
|
|
17780
18012
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18013
|
+
message: MESSAGE$24
|
|
17782
18014
|
});
|
|
17783
18015
|
} };
|
|
17784
18016
|
}
|
|
@@ -17786,7 +18018,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18018
|
//#endregion
|
|
17787
18019
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18020
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18021
|
+
const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18022
|
const resolveSettings$19 = (settings) => {
|
|
17791
18023
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18024
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18037,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18037
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18038
|
context.report({
|
|
17807
18039
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18040
|
+
message: MESSAGE$23
|
|
17809
18041
|
});
|
|
17810
18042
|
} };
|
|
17811
18043
|
}
|
|
@@ -17828,7 +18060,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18060
|
};
|
|
17829
18061
|
//#endregion
|
|
17830
18062
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18063
|
+
const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18064
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18065
|
let isConstructor = false;
|
|
17834
18066
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18082,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18082
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18083
|
context.report({
|
|
17852
18084
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18085
|
+
message: MESSAGE$22
|
|
17854
18086
|
});
|
|
17855
18087
|
};
|
|
17856
18088
|
const noDirectMutationState = defineRule({
|
|
@@ -19438,7 +19670,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19670
|
"ReactDOM",
|
|
19439
19671
|
"ReactDom"
|
|
19440
19672
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19673
|
+
const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19674
|
const noFindDomNode = defineRule({
|
|
19443
19675
|
id: "no-find-dom-node",
|
|
19444
19676
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19681,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19681
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19682
|
context.report({
|
|
19451
19683
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19684
|
+
message: MESSAGE$21
|
|
19453
19685
|
});
|
|
19454
19686
|
return;
|
|
19455
19687
|
}
|
|
@@ -19460,7 +19692,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19692
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19693
|
context.report({
|
|
19462
19694
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19695
|
+
message: MESSAGE$21
|
|
19464
19696
|
});
|
|
19465
19697
|
}
|
|
19466
19698
|
} })
|
|
@@ -19523,64 +19755,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
19755
|
} })
|
|
19524
19756
|
});
|
|
19525
19757
|
//#endregion
|
|
19526
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19527
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19528
|
-
"FunctionDeclaration",
|
|
19529
|
-
"FunctionExpression",
|
|
19530
|
-
"ArrowFunctionExpression",
|
|
19531
|
-
"ClassDeclaration",
|
|
19532
|
-
"ClassExpression"
|
|
19533
|
-
]);
|
|
19534
|
-
const isReactImport$1 = (symbol) => {
|
|
19535
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19536
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19537
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19538
|
-
return importDeclaration.source.value === "react";
|
|
19539
|
-
};
|
|
19540
|
-
const getImportedName = (symbol) => {
|
|
19541
|
-
if (symbol.kind !== "import") return null;
|
|
19542
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19543
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19544
|
-
};
|
|
19545
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19546
|
-
if (symbol.kind !== "import") return false;
|
|
19547
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19548
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19549
|
-
};
|
|
19550
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19551
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19552
|
-
const symbol = scopes.symbolFor(callee);
|
|
19553
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19554
|
-
};
|
|
19555
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19556
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19557
|
-
if (callee.computed) return false;
|
|
19558
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19559
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19560
|
-
if (callee.property.name !== "createElement") return false;
|
|
19561
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19562
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19563
|
-
};
|
|
19564
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19565
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19566
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19567
|
-
};
|
|
19568
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19569
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19570
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19571
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19572
|
-
const nodeRecord = node;
|
|
19573
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19574
|
-
if (key === "parent") continue;
|
|
19575
|
-
const child = nodeRecord[key];
|
|
19576
|
-
if (Array.isArray(child)) {
|
|
19577
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19578
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19579
|
-
}
|
|
19580
|
-
return false;
|
|
19581
|
-
};
|
|
19582
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19583
|
-
//#endregion
|
|
19584
19758
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
19759
|
const noGiantComponent = defineRule({
|
|
19586
19760
|
id: "no-giant-component",
|
|
@@ -19759,6 +19933,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
19933
|
} })
|
|
19760
19934
|
});
|
|
19761
19935
|
//#endregion
|
|
19936
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
19937
|
+
const MESSAGE$20 = "`<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.";
|
|
19938
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
19939
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
19940
|
+
title: "Lazy image with high fetchPriority",
|
|
19941
|
+
severity: "warn",
|
|
19942
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
19943
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
19944
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
19945
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
19946
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
19947
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
19948
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
19949
|
+
context.report({
|
|
19950
|
+
node: node.name,
|
|
19951
|
+
message: MESSAGE$20
|
|
19952
|
+
});
|
|
19953
|
+
} })
|
|
19954
|
+
});
|
|
19955
|
+
//#endregion
|
|
19762
19956
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
19957
|
const noInitializeState = defineRule({
|
|
19764
19958
|
id: "no-initialize-state",
|
|
@@ -19988,6 +20182,29 @@ const noIsMounted = defineRule({
|
|
|
19988
20182
|
} })
|
|
19989
20183
|
});
|
|
19990
20184
|
//#endregion
|
|
20185
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20186
|
+
const MESSAGE$19 = "`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)`.";
|
|
20187
|
+
const isJsonMethodCall = (node, method) => {
|
|
20188
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20189
|
+
const callee = node.callee;
|
|
20190
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20191
|
+
};
|
|
20192
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20193
|
+
id: "no-json-parse-stringify-clone",
|
|
20194
|
+
title: "JSON parse/stringify deep clone",
|
|
20195
|
+
severity: "warn",
|
|
20196
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20197
|
+
create: (context) => ({ CallExpression(node) {
|
|
20198
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20199
|
+
const firstArgument = node.arguments?.[0];
|
|
20200
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20201
|
+
context.report({
|
|
20202
|
+
node,
|
|
20203
|
+
message: MESSAGE$19
|
|
20204
|
+
});
|
|
20205
|
+
} })
|
|
20206
|
+
});
|
|
20207
|
+
//#endregion
|
|
19991
20208
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
20209
|
const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19993
20210
|
const isJsxElementTypeReference = (node) => {
|
|
@@ -20310,9 +20527,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20310
20527
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20528
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20529
|
//#endregion
|
|
20313
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20314
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20315
|
-
//#endregion
|
|
20316
20530
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20531
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20532
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -37144,6 +37358,17 @@ const reactDoctorRules = [
|
|
|
37144
37358
|
category: "Performance"
|
|
37145
37359
|
}
|
|
37146
37360
|
},
|
|
37361
|
+
{
|
|
37362
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37363
|
+
id: "auth-token-in-web-storage",
|
|
37364
|
+
source: "react-doctor",
|
|
37365
|
+
originallyExternal: false,
|
|
37366
|
+
rule: {
|
|
37367
|
+
...authTokenInWebStorage,
|
|
37368
|
+
framework: "global",
|
|
37369
|
+
category: "Security"
|
|
37370
|
+
}
|
|
37371
|
+
},
|
|
37147
37372
|
{
|
|
37148
37373
|
key: "react-doctor/autocomplete-valid",
|
|
37149
37374
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +37585,18 @@ const reactDoctorRules = [
|
|
|
37360
37585
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
37586
|
}
|
|
37362
37587
|
},
|
|
37588
|
+
{
|
|
37589
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37590
|
+
id: "dialog-has-accessible-name",
|
|
37591
|
+
source: "react-doctor",
|
|
37592
|
+
originallyExternal: false,
|
|
37593
|
+
rule: {
|
|
37594
|
+
...dialogHasAccessibleName,
|
|
37595
|
+
framework: "global",
|
|
37596
|
+
category: "Accessibility",
|
|
37597
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37598
|
+
}
|
|
37599
|
+
},
|
|
37363
37600
|
{
|
|
37364
37601
|
key: "react-doctor/display-name",
|
|
37365
37602
|
id: "display-name",
|
|
@@ -38519,6 +38756,18 @@ const reactDoctorRules = [
|
|
|
38519
38756
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
38757
|
}
|
|
38521
38758
|
},
|
|
38759
|
+
{
|
|
38760
|
+
key: "react-doctor/no-async-effect-callback",
|
|
38761
|
+
id: "no-async-effect-callback",
|
|
38762
|
+
source: "react-doctor",
|
|
38763
|
+
originallyExternal: false,
|
|
38764
|
+
rule: {
|
|
38765
|
+
...noAsyncEffectCallback,
|
|
38766
|
+
framework: "global",
|
|
38767
|
+
category: "Bugs",
|
|
38768
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
38769
|
+
}
|
|
38770
|
+
},
|
|
38522
38771
|
{
|
|
38523
38772
|
key: "react-doctor/no-autofocus",
|
|
38524
38773
|
id: "no-autofocus",
|
|
@@ -38542,6 +38791,18 @@ const reactDoctorRules = [
|
|
|
38542
38791
|
category: "Performance"
|
|
38543
38792
|
}
|
|
38544
38793
|
},
|
|
38794
|
+
{
|
|
38795
|
+
key: "react-doctor/no-call-component-as-function",
|
|
38796
|
+
id: "no-call-component-as-function",
|
|
38797
|
+
source: "react-doctor",
|
|
38798
|
+
originallyExternal: false,
|
|
38799
|
+
rule: {
|
|
38800
|
+
...noCallComponentAsFunction,
|
|
38801
|
+
framework: "global",
|
|
38802
|
+
category: "Bugs",
|
|
38803
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
38804
|
+
}
|
|
38805
|
+
},
|
|
38545
38806
|
{
|
|
38546
38807
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
38808
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +38863,18 @@ const reactDoctorRules = [
|
|
|
38602
38863
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
38864
|
}
|
|
38604
38865
|
},
|
|
38866
|
+
{
|
|
38867
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
38868
|
+
id: "no-create-ref-in-function-component",
|
|
38869
|
+
source: "react-doctor",
|
|
38870
|
+
originallyExternal: false,
|
|
38871
|
+
rule: {
|
|
38872
|
+
...noCreateRefInFunctionComponent,
|
|
38873
|
+
framework: "global",
|
|
38874
|
+
category: "Bugs",
|
|
38875
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
38876
|
+
}
|
|
38877
|
+
},
|
|
38605
38878
|
{
|
|
38606
38879
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
38880
|
id: "no-create-store-in-render",
|
|
@@ -38976,6 +39249,18 @@ const reactDoctorRules = [
|
|
|
38976
39249
|
category: "Accessibility"
|
|
38977
39250
|
}
|
|
38978
39251
|
},
|
|
39252
|
+
{
|
|
39253
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39254
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39255
|
+
source: "react-doctor",
|
|
39256
|
+
originallyExternal: false,
|
|
39257
|
+
rule: {
|
|
39258
|
+
...noImgLazyWithHighFetchpriority,
|
|
39259
|
+
framework: "global",
|
|
39260
|
+
category: "Performance",
|
|
39261
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39262
|
+
}
|
|
39263
|
+
},
|
|
38979
39264
|
{
|
|
38980
39265
|
key: "react-doctor/no-initialize-state",
|
|
38981
39266
|
id: "no-initialize-state",
|
|
@@ -39046,6 +39331,17 @@ const reactDoctorRules = [
|
|
|
39046
39331
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
39332
|
}
|
|
39048
39333
|
},
|
|
39334
|
+
{
|
|
39335
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39336
|
+
id: "no-json-parse-stringify-clone",
|
|
39337
|
+
source: "react-doctor",
|
|
39338
|
+
originallyExternal: false,
|
|
39339
|
+
rule: {
|
|
39340
|
+
...noJsonParseStringifyClone,
|
|
39341
|
+
framework: "global",
|
|
39342
|
+
category: "Performance"
|
|
39343
|
+
}
|
|
39344
|
+
},
|
|
39049
39345
|
{
|
|
39050
39346
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
39347
|
id: "no-jsx-element-type",
|