oxlint-plugin-react-doctor 0.5.6-dev.424d8f9 → 0.5.6-dev.44db3e0
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 +0 -505
- package/dist/index.js +225 -944
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1915,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1915
1915
|
});
|
|
1916
1916
|
//#endregion
|
|
1917
1917
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1918
|
-
const MESSAGE$
|
|
1918
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1919
1919
|
const anchorHasContent = defineRule({
|
|
1920
1920
|
id: "anchor-has-content",
|
|
1921
1921
|
title: "Anchor has no content",
|
|
@@ -1931,7 +1931,7 @@ const anchorHasContent = defineRule({
|
|
|
1931
1931
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1932
1932
|
context.report({
|
|
1933
1933
|
node: opening.name,
|
|
1934
|
-
message: MESSAGE$
|
|
1934
|
+
message: MESSAGE$59
|
|
1935
1935
|
});
|
|
1936
1936
|
} })
|
|
1937
1937
|
});
|
|
@@ -2325,7 +2325,7 @@ const parseJsxValue = (value) => {
|
|
|
2325
2325
|
};
|
|
2326
2326
|
//#endregion
|
|
2327
2327
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2328
|
-
const MESSAGE$
|
|
2328
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2329
2329
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2330
2330
|
id: "aria-activedescendant-has-tabindex",
|
|
2331
2331
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2343,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2343
2343
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2344
2344
|
context.report({
|
|
2345
2345
|
node: node.name,
|
|
2346
|
-
message: MESSAGE$
|
|
2346
|
+
message: MESSAGE$58
|
|
2347
2347
|
});
|
|
2348
2348
|
return;
|
|
2349
2349
|
}
|
|
2350
2350
|
if (isInteractiveElement(tag, node)) return;
|
|
2351
2351
|
context.report({
|
|
2352
2352
|
node: node.name,
|
|
2353
|
-
message: MESSAGE$
|
|
2353
|
+
message: MESSAGE$58
|
|
2354
2354
|
});
|
|
2355
2355
|
} })
|
|
2356
2356
|
});
|
|
@@ -3019,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
|
|
|
3019
3019
|
"widget",
|
|
3020
3020
|
"window"
|
|
3021
3021
|
]);
|
|
3022
|
-
const PRESENTATION_ROLES$
|
|
3022
|
+
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
3023
3023
|
//#endregion
|
|
3024
3024
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
3025
3025
|
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}`;
|
|
@@ -4326,7 +4326,7 @@ const asyncParallel = defineRule({
|
|
|
4326
4326
|
});
|
|
4327
4327
|
//#endregion
|
|
4328
4328
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4329
|
-
const MESSAGE$
|
|
4329
|
+
const MESSAGE$57 = "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.";
|
|
4330
4330
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4331
4331
|
const STORAGE_GLOBALS = new Set([
|
|
4332
4332
|
"window",
|
|
@@ -4360,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4360
4360
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4361
4361
|
context.report({
|
|
4362
4362
|
node,
|
|
4363
|
-
message: MESSAGE$
|
|
4363
|
+
message: MESSAGE$57
|
|
4364
4364
|
});
|
|
4365
4365
|
},
|
|
4366
4366
|
AssignmentExpression(node) {
|
|
@@ -4371,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4371
4371
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4372
4372
|
context.report({
|
|
4373
4373
|
node: target,
|
|
4374
|
-
message: MESSAGE$
|
|
4374
|
+
message: MESSAGE$57
|
|
4375
4375
|
});
|
|
4376
4376
|
}
|
|
4377
4377
|
})
|
|
@@ -4710,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
4710
4710
|
}
|
|
4711
4711
|
});
|
|
4712
4712
|
//#endregion
|
|
4713
|
+
//#region src/plugin/utils/is-presentation-role.ts
|
|
4714
|
+
const isPresentationRole = (openingElement) => {
|
|
4715
|
+
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
4716
|
+
if (!roleAttribute) return false;
|
|
4717
|
+
const value = getJsxPropStringValue(roleAttribute);
|
|
4718
|
+
return value !== null && PRESENTATION_ROLES$1.has(value);
|
|
4719
|
+
};
|
|
4720
|
+
//#endregion
|
|
4713
4721
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
4714
4722
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
4715
4723
|
"stopPropagation",
|
|
@@ -4747,8 +4755,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4747
4755
|
};
|
|
4748
4756
|
//#endregion
|
|
4749
4757
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4750
|
-
const
|
|
4751
|
-
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4758
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4752
4759
|
const KEY_HANDLERS = [
|
|
4753
4760
|
"onKeyUp",
|
|
4754
4761
|
"onKeyDown",
|
|
@@ -4772,15 +4779,11 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4772
4779
|
if (!onClick) return;
|
|
4773
4780
|
if (isPureEventBlockerHandler(onClick)) return;
|
|
4774
4781
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
4775
|
-
|
|
4776
|
-
if (roleAttribute) {
|
|
4777
|
-
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
4778
|
-
if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
|
|
4779
|
-
}
|
|
4782
|
+
if (isPresentationRole(node)) return;
|
|
4780
4783
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4781
4784
|
context.report({
|
|
4782
4785
|
node: node.name,
|
|
4783
|
-
message: MESSAGE$
|
|
4786
|
+
message: MESSAGE$56
|
|
4784
4787
|
});
|
|
4785
4788
|
} };
|
|
4786
4789
|
}
|
|
@@ -4895,7 +4898,7 @@ const isReactComponentName = (name) => {
|
|
|
4895
4898
|
};
|
|
4896
4899
|
//#endregion
|
|
4897
4900
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4898
|
-
const MESSAGE$
|
|
4901
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4899
4902
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4900
4903
|
const DEFAULT_LABELLING_PROPS = [
|
|
4901
4904
|
"alt",
|
|
@@ -5056,7 +5059,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5056
5059
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5057
5060
|
context.report({
|
|
5058
5061
|
node: opening,
|
|
5059
|
-
message: MESSAGE$
|
|
5062
|
+
message: MESSAGE$55
|
|
5060
5063
|
});
|
|
5061
5064
|
} };
|
|
5062
5065
|
}
|
|
@@ -5185,7 +5188,6 @@ const dangerousHtmlSink = defineRule({
|
|
|
5185
5188
|
return findings;
|
|
5186
5189
|
}
|
|
5187
5190
|
});
|
|
5188
|
-
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5189
5191
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5190
5192
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5191
5193
|
"continue",
|
|
@@ -5484,10 +5486,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5484
5486
|
});
|
|
5485
5487
|
//#endregion
|
|
5486
5488
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5487
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5489
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5488
5490
|
//#endregion
|
|
5489
5491
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5490
|
-
const MESSAGE$
|
|
5492
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5491
5493
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5492
5494
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5493
5495
|
"aria-label",
|
|
@@ -5506,11 +5508,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5506
5508
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5507
5509
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5508
5510
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5509
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5511
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5510
5512
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5511
5513
|
context.report({
|
|
5512
5514
|
node: node.name,
|
|
5513
|
-
message: MESSAGE$
|
|
5515
|
+
message: MESSAGE$54
|
|
5514
5516
|
});
|
|
5515
5517
|
} })
|
|
5516
5518
|
});
|
|
@@ -5549,7 +5551,7 @@ const isEs6Component = (node) => {
|
|
|
5549
5551
|
};
|
|
5550
5552
|
//#endregion
|
|
5551
5553
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5552
|
-
const MESSAGE$
|
|
5554
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5553
5555
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5554
5556
|
"observer",
|
|
5555
5557
|
"lazy",
|
|
@@ -5748,11 +5750,11 @@ const displayName = defineRule({
|
|
|
5748
5750
|
category: "Architecture",
|
|
5749
5751
|
create: (context) => {
|
|
5750
5752
|
const settings = resolveSettings$44(context.settings);
|
|
5751
|
-
const ignoreNamed = settings.ignoreTranspilerName
|
|
5753
|
+
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
5752
5754
|
const reportAt = (node) => {
|
|
5753
5755
|
context.report({
|
|
5754
5756
|
node,
|
|
5755
|
-
message: MESSAGE$
|
|
5757
|
+
message: MESSAGE$53
|
|
5756
5758
|
});
|
|
5757
5759
|
};
|
|
5758
5760
|
return {
|
|
@@ -7900,7 +7902,7 @@ const forbidElements = defineRule({
|
|
|
7900
7902
|
});
|
|
7901
7903
|
//#endregion
|
|
7902
7904
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7903
|
-
const MESSAGE$
|
|
7905
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7904
7906
|
const forwardRefUsesRef = defineRule({
|
|
7905
7907
|
id: "forward-ref-uses-ref",
|
|
7906
7908
|
title: "forwardRef without ref parameter",
|
|
@@ -7920,7 +7922,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7920
7922
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7921
7923
|
context.report({
|
|
7922
7924
|
node: inner,
|
|
7923
|
-
message: MESSAGE$
|
|
7925
|
+
message: MESSAGE$52
|
|
7924
7926
|
});
|
|
7925
7927
|
} })
|
|
7926
7928
|
});
|
|
@@ -7957,7 +7959,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7957
7959
|
});
|
|
7958
7960
|
//#endregion
|
|
7959
7961
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7960
|
-
const MESSAGE$
|
|
7962
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7961
7963
|
const DEFAULT_HEADING_TAGS = [
|
|
7962
7964
|
"h1",
|
|
7963
7965
|
"h2",
|
|
@@ -7990,7 +7992,7 @@ const headingHasContent = defineRule({
|
|
|
7990
7992
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7991
7993
|
context.report({
|
|
7992
7994
|
node,
|
|
7993
|
-
message: MESSAGE$
|
|
7995
|
+
message: MESSAGE$51
|
|
7994
7996
|
});
|
|
7995
7997
|
} };
|
|
7996
7998
|
}
|
|
@@ -8128,7 +8130,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8128
8130
|
});
|
|
8129
8131
|
//#endregion
|
|
8130
8132
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8131
|
-
const MESSAGE$
|
|
8133
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8132
8134
|
const resolveSettings$38 = (settings) => {
|
|
8133
8135
|
const reactDoctor = settings?.["react-doctor"];
|
|
8134
8136
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8171,26 +8173,17 @@ const htmlHasLang = defineRule({
|
|
|
8171
8173
|
return { JSXOpeningElement(node) {
|
|
8172
8174
|
const tag = getElementType(node, context.settings);
|
|
8173
8175
|
if (!tagSet.has(tag)) return;
|
|
8174
|
-
const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
8175
8176
|
const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
|
|
8176
8177
|
if (!lang) {
|
|
8177
8178
|
context.report({
|
|
8178
8179
|
node: node.name,
|
|
8179
|
-
message: MESSAGE$
|
|
8180
|
-
});
|
|
8181
|
-
return;
|
|
8182
|
-
}
|
|
8183
|
-
const verdict = evaluateLang(lang.value);
|
|
8184
|
-
if (verdict === "missing" || verdict === "empty") {
|
|
8185
|
-
context.report({
|
|
8186
|
-
node: lang,
|
|
8187
|
-
message: MESSAGE$55
|
|
8180
|
+
message: MESSAGE$50
|
|
8188
8181
|
});
|
|
8189
8182
|
return;
|
|
8190
8183
|
}
|
|
8191
|
-
if (
|
|
8192
|
-
node:
|
|
8193
|
-
message: MESSAGE$
|
|
8184
|
+
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8185
|
+
node: lang,
|
|
8186
|
+
message: MESSAGE$50
|
|
8194
8187
|
});
|
|
8195
8188
|
} };
|
|
8196
8189
|
}
|
|
@@ -8404,7 +8397,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8404
8397
|
});
|
|
8405
8398
|
//#endregion
|
|
8406
8399
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8407
|
-
const MESSAGE$
|
|
8400
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8408
8401
|
const evaluateTitleValue = (value) => {
|
|
8409
8402
|
if (!value) return "missing";
|
|
8410
8403
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8444,14 +8437,14 @@ const iframeHasTitle = defineRule({
|
|
|
8444
8437
|
if (!titleAttr) {
|
|
8445
8438
|
if (hasSpread || tag === "iframe") context.report({
|
|
8446
8439
|
node: node.name,
|
|
8447
|
-
message: MESSAGE$
|
|
8440
|
+
message: MESSAGE$49
|
|
8448
8441
|
});
|
|
8449
8442
|
return;
|
|
8450
8443
|
}
|
|
8451
8444
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8452
8445
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8453
8446
|
node: titleAttr,
|
|
8454
|
-
message: MESSAGE$
|
|
8447
|
+
message: MESSAGE$49
|
|
8455
8448
|
});
|
|
8456
8449
|
} })
|
|
8457
8450
|
});
|
|
@@ -8555,7 +8548,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8555
8548
|
});
|
|
8556
8549
|
//#endregion
|
|
8557
8550
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8558
|
-
const MESSAGE$
|
|
8551
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8559
8552
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8560
8553
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8561
8554
|
"image",
|
|
@@ -8620,7 +8613,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8620
8613
|
if (!altAttribute) return;
|
|
8621
8614
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8622
8615
|
node: altAttribute,
|
|
8623
|
-
message: MESSAGE$
|
|
8616
|
+
message: MESSAGE$48
|
|
8624
8617
|
});
|
|
8625
8618
|
} };
|
|
8626
8619
|
}
|
|
@@ -8903,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
|
|
|
8903
8896
|
//#region src/plugin/utils/is-non-interactive-role.ts
|
|
8904
8897
|
const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
|
|
8905
8898
|
//#endregion
|
|
8906
|
-
//#region src/plugin/utils/is-presentation-role.ts
|
|
8907
|
-
const isPresentationRole = (openingElement) => {
|
|
8908
|
-
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
8909
|
-
if (!roleAttribute) return false;
|
|
8910
|
-
const value = getJsxPropStringValue(roleAttribute);
|
|
8911
|
-
return value !== null && PRESENTATION_ROLES$2.has(value);
|
|
8912
|
-
};
|
|
8913
|
-
//#endregion
|
|
8914
8899
|
//#region src/plugin/rules/a11y/interactive-supports-focus.ts
|
|
8915
8900
|
const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
|
|
8916
8901
|
const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
|
|
@@ -10685,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10685
10670
|
return false;
|
|
10686
10671
|
};
|
|
10687
10672
|
//#endregion
|
|
10673
|
+
//#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
|
|
10674
|
+
const ascendThroughJsxValueWrappers = (node) => {
|
|
10675
|
+
let current = node;
|
|
10676
|
+
while (current.parent) {
|
|
10677
|
+
const parent = current.parent;
|
|
10678
|
+
if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
|
|
10679
|
+
current = parent;
|
|
10680
|
+
}
|
|
10681
|
+
return current;
|
|
10682
|
+
};
|
|
10683
|
+
const isNonChildrenJsxAttributeValue = (node) => {
|
|
10684
|
+
const container = ascendThroughJsxValueWrappers(node).parent;
|
|
10685
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
|
|
10686
|
+
const attribute = container.parent;
|
|
10687
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
|
|
10688
|
+
return getJsxAttributeName(attribute.name) !== "children";
|
|
10689
|
+
};
|
|
10690
|
+
//#endregion
|
|
10688
10691
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10689
10692
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10690
10693
|
"map",
|
|
@@ -10723,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10723
10726
|
const arrayParent = parent.parent;
|
|
10724
10727
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10725
10728
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10729
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10726
10730
|
return { kind: "array" };
|
|
10727
10731
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10728
10732
|
const callee = parent.callee;
|
|
@@ -10735,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10735
10739
|
if (!targetArg) return null;
|
|
10736
10740
|
let walker = current;
|
|
10737
10741
|
while (walker && walker !== parent) {
|
|
10738
|
-
if (walker === targetArg)
|
|
10739
|
-
|
|
10740
|
-
|
|
10741
|
-
|
|
10742
|
+
if (walker === targetArg) {
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
kind: "iterator",
|
|
10746
|
+
callExpression: parent
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10742
10749
|
walker = walker.parent ?? null;
|
|
10743
10750
|
}
|
|
10744
10751
|
return null;
|
|
@@ -10977,7 +10984,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10977
10984
|
});
|
|
10978
10985
|
//#endregion
|
|
10979
10986
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10980
|
-
const MESSAGE$
|
|
10987
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10981
10988
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10982
10989
|
"code",
|
|
10983
10990
|
"pre",
|
|
@@ -11013,7 +11020,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
11013
11020
|
if (isInsideLiteralTextTag(node)) return;
|
|
11014
11021
|
context.report({
|
|
11015
11022
|
node,
|
|
11016
|
-
message: MESSAGE$
|
|
11023
|
+
message: MESSAGE$47
|
|
11017
11024
|
});
|
|
11018
11025
|
} })
|
|
11019
11026
|
});
|
|
@@ -11044,7 +11051,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
11044
11051
|
};
|
|
11045
11052
|
//#endregion
|
|
11046
11053
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
11047
|
-
const MESSAGE$
|
|
11054
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
11048
11055
|
const CONTEXT_MODULES$1 = [
|
|
11049
11056
|
"react",
|
|
11050
11057
|
"use-context-selector",
|
|
@@ -11142,7 +11149,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11142
11149
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11143
11150
|
context.report({
|
|
11144
11151
|
node: attribute,
|
|
11145
|
-
message: MESSAGE$
|
|
11152
|
+
message: MESSAGE$46
|
|
11146
11153
|
});
|
|
11147
11154
|
}
|
|
11148
11155
|
}
|
|
@@ -11228,7 +11235,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11228
11235
|
};
|
|
11229
11236
|
//#endregion
|
|
11230
11237
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11231
|
-
const MESSAGE$
|
|
11238
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11232
11239
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11233
11240
|
"icon",
|
|
11234
11241
|
"Icon",
|
|
@@ -11497,7 +11504,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11497
11504
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11498
11505
|
context.report({
|
|
11499
11506
|
node,
|
|
11500
|
-
message: MESSAGE$
|
|
11507
|
+
message: MESSAGE$45
|
|
11501
11508
|
});
|
|
11502
11509
|
}
|
|
11503
11510
|
};
|
|
@@ -11785,7 +11792,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11785
11792
|
];
|
|
11786
11793
|
//#endregion
|
|
11787
11794
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11788
|
-
const MESSAGE$
|
|
11795
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11789
11796
|
const isDataArrayPropName = (propName) => {
|
|
11790
11797
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11791
11798
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11869,7 +11876,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11869
11876
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11870
11877
|
context.report({
|
|
11871
11878
|
node,
|
|
11872
|
-
message: MESSAGE$
|
|
11879
|
+
message: MESSAGE$44
|
|
11873
11880
|
});
|
|
11874
11881
|
}
|
|
11875
11882
|
};
|
|
@@ -12127,7 +12134,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12127
12134
|
]);
|
|
12128
12135
|
//#endregion
|
|
12129
12136
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12130
|
-
const MESSAGE$
|
|
12137
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12131
12138
|
const isAccessorPredicateName = (propName) => {
|
|
12132
12139
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12133
12140
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12333,7 +12340,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12333
12340
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12334
12341
|
context.report({
|
|
12335
12342
|
node,
|
|
12336
|
-
message: MESSAGE$
|
|
12343
|
+
message: MESSAGE$43
|
|
12337
12344
|
});
|
|
12338
12345
|
}
|
|
12339
12346
|
};
|
|
@@ -12553,7 +12560,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12553
12560
|
];
|
|
12554
12561
|
//#endregion
|
|
12555
12562
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12556
|
-
const MESSAGE$
|
|
12563
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12557
12564
|
const isConfigObjectPropName = (propName) => {
|
|
12558
12565
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12559
12566
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12641,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12641
12648
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12642
12649
|
context.report({
|
|
12643
12650
|
node,
|
|
12644
|
-
message: MESSAGE$
|
|
12651
|
+
message: MESSAGE$42
|
|
12645
12652
|
});
|
|
12646
12653
|
}
|
|
12647
12654
|
};
|
|
@@ -12649,7 +12656,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12649
12656
|
});
|
|
12650
12657
|
//#endregion
|
|
12651
12658
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12652
|
-
const MESSAGE$
|
|
12659
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12653
12660
|
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;
|
|
12654
12661
|
const resolveSettings$28 = (settings) => {
|
|
12655
12662
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12690,7 +12697,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12690
12697
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12691
12698
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12692
12699
|
node: attribute,
|
|
12693
|
-
message: MESSAGE$
|
|
12700
|
+
message: MESSAGE$41
|
|
12694
12701
|
});
|
|
12695
12702
|
}
|
|
12696
12703
|
} };
|
|
@@ -13005,7 +13012,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
13005
13012
|
});
|
|
13006
13013
|
//#endregion
|
|
13007
13014
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
13008
|
-
const MESSAGE$
|
|
13015
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
13009
13016
|
const resolveSettings$25 = (settings) => {
|
|
13010
13017
|
const reactDoctor = settings?.["react-doctor"];
|
|
13011
13018
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -13046,7 +13053,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
13046
13053
|
}
|
|
13047
13054
|
context.report({
|
|
13048
13055
|
node: attribute,
|
|
13049
|
-
message: MESSAGE$
|
|
13056
|
+
message: MESSAGE$40
|
|
13050
13057
|
});
|
|
13051
13058
|
}
|
|
13052
13059
|
} };
|
|
@@ -13274,7 +13281,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13274
13281
|
});
|
|
13275
13282
|
//#endregion
|
|
13276
13283
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13277
|
-
const MESSAGE$
|
|
13284
|
+
const MESSAGE$39 = "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`.";
|
|
13278
13285
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13279
13286
|
"aa",
|
|
13280
13287
|
"ab",
|
|
@@ -13486,7 +13493,7 @@ const lang = defineRule({
|
|
|
13486
13493
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13487
13494
|
context.report({
|
|
13488
13495
|
node: langAttr,
|
|
13489
|
-
message: MESSAGE$
|
|
13496
|
+
message: MESSAGE$39
|
|
13490
13497
|
});
|
|
13491
13498
|
return;
|
|
13492
13499
|
}
|
|
@@ -13495,7 +13502,7 @@ const lang = defineRule({
|
|
|
13495
13502
|
if (value === null) return;
|
|
13496
13503
|
if (!isValidLangTag(value)) context.report({
|
|
13497
13504
|
node: langAttr,
|
|
13498
|
-
message: MESSAGE$
|
|
13505
|
+
message: MESSAGE$39
|
|
13499
13506
|
});
|
|
13500
13507
|
} })
|
|
13501
13508
|
});
|
|
@@ -13540,7 +13547,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13540
13547
|
});
|
|
13541
13548
|
//#endregion
|
|
13542
13549
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13543
|
-
const MESSAGE$
|
|
13550
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13544
13551
|
const DEFAULT_AUDIO = ["audio"];
|
|
13545
13552
|
const DEFAULT_VIDEO = ["video"];
|
|
13546
13553
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13581,7 +13588,7 @@ const mediaHasCaption = defineRule({
|
|
|
13581
13588
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13582
13589
|
context.report({
|
|
13583
13590
|
node: node.name,
|
|
13584
|
-
message: MESSAGE$
|
|
13591
|
+
message: MESSAGE$38
|
|
13585
13592
|
});
|
|
13586
13593
|
return;
|
|
13587
13594
|
}
|
|
@@ -13598,7 +13605,7 @@ const mediaHasCaption = defineRule({
|
|
|
13598
13605
|
return kindValue.value.toLowerCase() === "captions";
|
|
13599
13606
|
})) context.report({
|
|
13600
13607
|
node: node.name,
|
|
13601
|
-
message: MESSAGE$
|
|
13608
|
+
message: MESSAGE$38
|
|
13602
13609
|
});
|
|
13603
13610
|
} };
|
|
13604
13611
|
}
|
|
@@ -15399,7 +15406,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15399
15406
|
});
|
|
15400
15407
|
//#endregion
|
|
15401
15408
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15402
|
-
const MESSAGE$
|
|
15409
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15403
15410
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15404
15411
|
const noAccessKey = defineRule({
|
|
15405
15412
|
id: "no-access-key",
|
|
@@ -15416,7 +15423,7 @@ const noAccessKey = defineRule({
|
|
|
15416
15423
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15417
15424
|
context.report({
|
|
15418
15425
|
node: accessKey,
|
|
15419
|
-
message: MESSAGE$
|
|
15426
|
+
message: MESSAGE$37
|
|
15420
15427
|
});
|
|
15421
15428
|
return;
|
|
15422
15429
|
}
|
|
@@ -15426,7 +15433,7 @@ const noAccessKey = defineRule({
|
|
|
15426
15433
|
if (isUndefinedIdentifier(expression)) return;
|
|
15427
15434
|
context.report({
|
|
15428
15435
|
node: accessKey,
|
|
15429
|
-
message: MESSAGE$
|
|
15436
|
+
message: MESSAGE$37
|
|
15430
15437
|
});
|
|
15431
15438
|
}
|
|
15432
15439
|
} })
|
|
@@ -15908,41 +15915,8 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15908
15915
|
} })
|
|
15909
15916
|
});
|
|
15910
15917
|
//#endregion
|
|
15911
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15912
|
-
const getStringFromClassNameAttr = (node) => {
|
|
15913
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15914
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15915
|
-
if (!classAttr?.value) return null;
|
|
15916
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15917
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15918
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15919
|
-
return null;
|
|
15920
|
-
};
|
|
15921
|
-
//#endregion
|
|
15922
|
-
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15923
|
-
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15924
|
-
const noArbitraryPxFontSize = defineRule({
|
|
15925
|
-
id: "no-arbitrary-px-font-size",
|
|
15926
|
-
title: "Pixel arbitrary font size",
|
|
15927
|
-
tags: ["design", "test-noise"],
|
|
15928
|
-
severity: "warn",
|
|
15929
|
-
category: "Accessibility",
|
|
15930
|
-
recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
|
|
15931
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
15932
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
15933
|
-
if (!classNameValue) return;
|
|
15934
|
-
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15935
|
-
const rem = parseFloat(match[1]) / 16;
|
|
15936
|
-
context.report({
|
|
15937
|
-
node,
|
|
15938
|
-
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15939
|
-
});
|
|
15940
|
-
}
|
|
15941
|
-
} })
|
|
15942
|
-
});
|
|
15943
|
-
//#endregion
|
|
15944
15918
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15945
|
-
const MESSAGE$
|
|
15919
|
+
const MESSAGE$36 = "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.";
|
|
15946
15920
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15947
15921
|
id: "no-aria-hidden-on-focusable",
|
|
15948
15922
|
title: "aria-hidden on focusable element",
|
|
@@ -15969,7 +15943,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15969
15943
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15970
15944
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15971
15945
|
node: ariaHidden,
|
|
15972
|
-
message: MESSAGE$
|
|
15946
|
+
message: MESSAGE$36
|
|
15973
15947
|
});
|
|
15974
15948
|
} })
|
|
15975
15949
|
});
|
|
@@ -16337,7 +16311,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16337
16311
|
});
|
|
16338
16312
|
//#endregion
|
|
16339
16313
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16340
|
-
const MESSAGE$
|
|
16314
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16341
16315
|
const SECOND_INDEX_METHODS = new Set([
|
|
16342
16316
|
"every",
|
|
16343
16317
|
"filter",
|
|
@@ -16541,7 +16515,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16541
16515
|
}
|
|
16542
16516
|
context.report({
|
|
16543
16517
|
node: keyAttribute,
|
|
16544
|
-
message: MESSAGE$
|
|
16518
|
+
message: MESSAGE$35
|
|
16545
16519
|
});
|
|
16546
16520
|
},
|
|
16547
16521
|
CallExpression(node) {
|
|
@@ -16561,7 +16535,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16561
16535
|
if (propName !== "key") continue;
|
|
16562
16536
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16563
16537
|
node: property,
|
|
16564
|
-
message: MESSAGE$
|
|
16538
|
+
message: MESSAGE$35
|
|
16565
16539
|
});
|
|
16566
16540
|
}
|
|
16567
16541
|
}
|
|
@@ -16569,7 +16543,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16569
16543
|
});
|
|
16570
16544
|
//#endregion
|
|
16571
16545
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16572
|
-
const MESSAGE$
|
|
16546
|
+
const MESSAGE$34 = "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.";
|
|
16573
16547
|
const noAsyncEffectCallback = defineRule({
|
|
16574
16548
|
id: "no-async-effect-callback",
|
|
16575
16549
|
title: "Async effect callback",
|
|
@@ -16583,13 +16557,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16583
16557
|
if (!callback.async) return;
|
|
16584
16558
|
context.report({
|
|
16585
16559
|
node: callback,
|
|
16586
|
-
message: MESSAGE$
|
|
16560
|
+
message: MESSAGE$34
|
|
16587
16561
|
});
|
|
16588
16562
|
} })
|
|
16589
16563
|
});
|
|
16590
16564
|
//#endregion
|
|
16591
16565
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16592
|
-
const MESSAGE$
|
|
16566
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16593
16567
|
const resolveSettings$21 = (settings) => {
|
|
16594
16568
|
const reactDoctor = settings?.["react-doctor"];
|
|
16595
16569
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16645,45 +16619,12 @@ const noAutofocus = defineRule({
|
|
|
16645
16619
|
}
|
|
16646
16620
|
context.report({
|
|
16647
16621
|
node: autoFocusAttribute,
|
|
16648
|
-
message: MESSAGE$
|
|
16622
|
+
message: MESSAGE$33
|
|
16649
16623
|
});
|
|
16650
16624
|
} };
|
|
16651
16625
|
}
|
|
16652
16626
|
});
|
|
16653
16627
|
//#endregion
|
|
16654
|
-
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16655
|
-
const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
|
|
16656
|
-
const resolveStaticBoolean = (attribute) => {
|
|
16657
|
-
const value = attribute.value;
|
|
16658
|
-
if (!value) return true;
|
|
16659
|
-
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16660
|
-
if (isNodeOfType(literal, "Literal")) {
|
|
16661
|
-
if (literal.value === true || literal.value === "true") return true;
|
|
16662
|
-
if (literal.value === false || literal.value === "false") return false;
|
|
16663
|
-
}
|
|
16664
|
-
return null;
|
|
16665
|
-
};
|
|
16666
|
-
const noAutoplayWithoutMuted = defineRule({
|
|
16667
|
-
id: "no-autoplay-without-muted",
|
|
16668
|
-
title: "Autoplaying media without muted",
|
|
16669
|
-
severity: "warn",
|
|
16670
|
-
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16671
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
16672
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16673
|
-
const tagName = node.name.name;
|
|
16674
|
-
if (tagName !== "video" && tagName !== "audio") return;
|
|
16675
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16676
|
-
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16677
|
-
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16678
|
-
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16679
|
-
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16680
|
-
context.report({
|
|
16681
|
-
node: node.name,
|
|
16682
|
-
message: MESSAGE$37
|
|
16683
|
-
});
|
|
16684
|
-
} })
|
|
16685
|
-
});
|
|
16686
|
-
//#endregion
|
|
16687
16628
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16688
16629
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16689
16630
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -17182,7 +17123,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17182
17123
|
});
|
|
17183
17124
|
//#endregion
|
|
17184
17125
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17185
|
-
const MESSAGE$
|
|
17126
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17186
17127
|
const noChildrenProp = defineRule({
|
|
17187
17128
|
id: "no-children-prop",
|
|
17188
17129
|
title: "Children passed as a prop",
|
|
@@ -17194,7 +17135,7 @@ const noChildrenProp = defineRule({
|
|
|
17194
17135
|
if (node.name.name !== "children") return;
|
|
17195
17136
|
context.report({
|
|
17196
17137
|
node: node.name,
|
|
17197
|
-
message: MESSAGE$
|
|
17138
|
+
message: MESSAGE$32
|
|
17198
17139
|
});
|
|
17199
17140
|
},
|
|
17200
17141
|
CallExpression(node) {
|
|
@@ -17207,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
17207
17148
|
const propertyKey = property.key;
|
|
17208
17149
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17209
17150
|
node: propertyKey,
|
|
17210
|
-
message: MESSAGE$
|
|
17151
|
+
message: MESSAGE$32
|
|
17211
17152
|
});
|
|
17212
17153
|
}
|
|
17213
17154
|
}
|
|
@@ -17215,7 +17156,7 @@ const noChildrenProp = defineRule({
|
|
|
17215
17156
|
});
|
|
17216
17157
|
//#endregion
|
|
17217
17158
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17218
|
-
const MESSAGE$
|
|
17159
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17219
17160
|
const noCloneElement = defineRule({
|
|
17220
17161
|
id: "no-clone-element",
|
|
17221
17162
|
title: "cloneElement makes child props fragile",
|
|
@@ -17228,7 +17169,7 @@ const noCloneElement = defineRule({
|
|
|
17228
17169
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17229
17170
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17230
17171
|
node: callee,
|
|
17231
|
-
message: MESSAGE$
|
|
17172
|
+
message: MESSAGE$31
|
|
17232
17173
|
});
|
|
17233
17174
|
return;
|
|
17234
17175
|
}
|
|
@@ -17241,7 +17182,7 @@ const noCloneElement = defineRule({
|
|
|
17241
17182
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17242
17183
|
context.report({
|
|
17243
17184
|
node: callee,
|
|
17244
|
-
message: MESSAGE$
|
|
17185
|
+
message: MESSAGE$31
|
|
17245
17186
|
});
|
|
17246
17187
|
}
|
|
17247
17188
|
} })
|
|
@@ -17290,7 +17231,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17290
17231
|
};
|
|
17291
17232
|
//#endregion
|
|
17292
17233
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17293
|
-
const MESSAGE$
|
|
17234
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17294
17235
|
const CONTEXT_MODULES = [
|
|
17295
17236
|
"react",
|
|
17296
17237
|
"use-context-selector",
|
|
@@ -17326,13 +17267,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17326
17267
|
if (!componentOrHookName) return;
|
|
17327
17268
|
context.report({
|
|
17328
17269
|
node,
|
|
17329
|
-
message: `${MESSAGE$
|
|
17270
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17330
17271
|
});
|
|
17331
17272
|
} })
|
|
17332
17273
|
});
|
|
17333
17274
|
//#endregion
|
|
17334
17275
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17335
|
-
const MESSAGE$
|
|
17276
|
+
const MESSAGE$29 = "`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.";
|
|
17336
17277
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17337
17278
|
id: "no-create-ref-in-function-component",
|
|
17338
17279
|
title: "createRef in function component",
|
|
@@ -17351,7 +17292,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17351
17292
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17352
17293
|
context.report({
|
|
17353
17294
|
node,
|
|
17354
|
-
message: MESSAGE$
|
|
17295
|
+
message: MESSAGE$29
|
|
17355
17296
|
});
|
|
17356
17297
|
} })
|
|
17357
17298
|
});
|
|
@@ -17491,12 +17432,13 @@ const noCreateStoreInRender = defineRule({
|
|
|
17491
17432
|
});
|
|
17492
17433
|
//#endregion
|
|
17493
17434
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17494
|
-
const MESSAGE$
|
|
17435
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17495
17436
|
const noDanger = defineRule({
|
|
17496
17437
|
id: "no-danger",
|
|
17497
17438
|
title: "Raw HTML injection can run unsafe markup",
|
|
17498
17439
|
severity: "warn",
|
|
17499
17440
|
category: "Security",
|
|
17441
|
+
defaultEnabled: false,
|
|
17500
17442
|
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
17501
17443
|
create: (context) => ({
|
|
17502
17444
|
JSXOpeningElement(node) {
|
|
@@ -17504,7 +17446,7 @@ const noDanger = defineRule({
|
|
|
17504
17446
|
if (!propAttribute) return;
|
|
17505
17447
|
context.report({
|
|
17506
17448
|
node: propAttribute.name,
|
|
17507
|
-
message: MESSAGE$
|
|
17449
|
+
message: MESSAGE$28
|
|
17508
17450
|
});
|
|
17509
17451
|
},
|
|
17510
17452
|
CallExpression(node) {
|
|
@@ -17516,7 +17458,7 @@ const noDanger = defineRule({
|
|
|
17516
17458
|
const propertyKey = property.key;
|
|
17517
17459
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17518
17460
|
node: propertyKey,
|
|
17519
|
-
message: MESSAGE$
|
|
17461
|
+
message: MESSAGE$28
|
|
17520
17462
|
});
|
|
17521
17463
|
}
|
|
17522
17464
|
}
|
|
@@ -17524,7 +17466,7 @@ const noDanger = defineRule({
|
|
|
17524
17466
|
});
|
|
17525
17467
|
//#endregion
|
|
17526
17468
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17527
|
-
const MESSAGE$
|
|
17469
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17528
17470
|
const isLineBreak = (child) => {
|
|
17529
17471
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17530
17472
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17594,7 +17536,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17594
17536
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17595
17537
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17596
17538
|
node: opening,
|
|
17597
|
-
message: MESSAGE$
|
|
17539
|
+
message: MESSAGE$27
|
|
17598
17540
|
});
|
|
17599
17541
|
},
|
|
17600
17542
|
CallExpression(node) {
|
|
@@ -17606,7 +17548,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17606
17548
|
if (!propsShape.hasDangerously) return;
|
|
17607
17549
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17608
17550
|
node,
|
|
17609
|
-
message: MESSAGE$
|
|
17551
|
+
message: MESSAGE$27
|
|
17610
17552
|
});
|
|
17611
17553
|
}
|
|
17612
17554
|
})
|
|
@@ -17771,37 +17713,6 @@ const noDefaultProps = defineRule({
|
|
|
17771
17713
|
} })
|
|
17772
17714
|
});
|
|
17773
17715
|
//#endregion
|
|
17774
|
-
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17775
|
-
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17776
|
-
//#endregion
|
|
17777
|
-
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17778
|
-
const renameDeprecatedToken = (token) => {
|
|
17779
|
-
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17780
|
-
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17781
|
-
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17782
|
-
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17783
|
-
return null;
|
|
17784
|
-
};
|
|
17785
|
-
const noDeprecatedTailwindClass = defineRule({
|
|
17786
|
-
id: "no-deprecated-tailwind-class",
|
|
17787
|
-
title: "Deprecated Tailwind v4 utility",
|
|
17788
|
-
tags: ["design", "test-noise"],
|
|
17789
|
-
severity: "warn",
|
|
17790
|
-
requires: ["tailwind:4"],
|
|
17791
|
-
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17792
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
17793
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
17794
|
-
if (!classNameValue) return;
|
|
17795
|
-
for (const token of getClassNameTokens(classNameValue)) {
|
|
17796
|
-
const replacement = renameDeprecatedToken(token);
|
|
17797
|
-
if (replacement) context.report({
|
|
17798
|
-
node,
|
|
17799
|
-
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17800
|
-
});
|
|
17801
|
-
}
|
|
17802
|
-
} })
|
|
17803
|
-
});
|
|
17804
|
-
//#endregion
|
|
17805
17716
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17806
17717
|
const isInitialOnlyPropName = (propName) => {
|
|
17807
17718
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -18214,7 +18125,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18214
18125
|
//#endregion
|
|
18215
18126
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18216
18127
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18217
|
-
const MESSAGE$
|
|
18128
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18218
18129
|
const resolveSettings$20 = (settings) => {
|
|
18219
18130
|
const reactDoctor = settings?.["react-doctor"];
|
|
18220
18131
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18233,7 +18144,7 @@ const noDidMountSetState = defineRule({
|
|
|
18233
18144
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18234
18145
|
context.report({
|
|
18235
18146
|
node: node.callee,
|
|
18236
|
-
message: MESSAGE$
|
|
18147
|
+
message: MESSAGE$26
|
|
18237
18148
|
});
|
|
18238
18149
|
} };
|
|
18239
18150
|
}
|
|
@@ -18241,7 +18152,7 @@ const noDidMountSetState = defineRule({
|
|
|
18241
18152
|
//#endregion
|
|
18242
18153
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18243
18154
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18244
|
-
const MESSAGE$
|
|
18155
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18245
18156
|
const resolveSettings$19 = (settings) => {
|
|
18246
18157
|
const reactDoctor = settings?.["react-doctor"];
|
|
18247
18158
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18260,7 +18171,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18260
18171
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18261
18172
|
context.report({
|
|
18262
18173
|
node: node.callee,
|
|
18263
|
-
message: MESSAGE$
|
|
18174
|
+
message: MESSAGE$25
|
|
18264
18175
|
});
|
|
18265
18176
|
} };
|
|
18266
18177
|
}
|
|
@@ -18283,7 +18194,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18283
18194
|
};
|
|
18284
18195
|
//#endregion
|
|
18285
18196
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18286
|
-
const MESSAGE$
|
|
18197
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18287
18198
|
const shouldIgnoreMutation = (node) => {
|
|
18288
18199
|
let isConstructor = false;
|
|
18289
18200
|
let isInsideCallExpression = false;
|
|
@@ -18305,7 +18216,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18305
18216
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18306
18217
|
context.report({
|
|
18307
18218
|
node: reportNode,
|
|
18308
|
-
message: MESSAGE$
|
|
18219
|
+
message: MESSAGE$24
|
|
18309
18220
|
});
|
|
18310
18221
|
};
|
|
18311
18222
|
const noDirectMutationState = defineRule({
|
|
@@ -18516,7 +18427,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18516
18427
|
});
|
|
18517
18428
|
//#endregion
|
|
18518
18429
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18519
|
-
const MESSAGE$
|
|
18430
|
+
const MESSAGE$23 = "`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.";
|
|
18520
18431
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18521
18432
|
const noDocumentWrite = defineRule({
|
|
18522
18433
|
id: "no-document-write",
|
|
@@ -18530,7 +18441,7 @@ const noDocumentWrite = defineRule({
|
|
|
18530
18441
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18531
18442
|
context.report({
|
|
18532
18443
|
node,
|
|
18533
|
-
message: MESSAGE$
|
|
18444
|
+
message: MESSAGE$23
|
|
18534
18445
|
});
|
|
18535
18446
|
} })
|
|
18536
18447
|
});
|
|
@@ -19913,7 +19824,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19913
19824
|
"ReactDOM",
|
|
19914
19825
|
"ReactDom"
|
|
19915
19826
|
]);
|
|
19916
|
-
const MESSAGE$
|
|
19827
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19917
19828
|
const noFindDomNode = defineRule({
|
|
19918
19829
|
id: "no-find-dom-node",
|
|
19919
19830
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19924,7 +19835,7 @@ const noFindDomNode = defineRule({
|
|
|
19924
19835
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19925
19836
|
context.report({
|
|
19926
19837
|
node: callee,
|
|
19927
|
-
message: MESSAGE$
|
|
19838
|
+
message: MESSAGE$22
|
|
19928
19839
|
});
|
|
19929
19840
|
return;
|
|
19930
19841
|
}
|
|
@@ -19935,7 +19846,7 @@ const noFindDomNode = defineRule({
|
|
|
19935
19846
|
if (callee.property.name !== "findDOMNode") return;
|
|
19936
19847
|
context.report({
|
|
19937
19848
|
node: callee.property,
|
|
19938
|
-
message: MESSAGE$
|
|
19849
|
+
message: MESSAGE$22
|
|
19939
19850
|
});
|
|
19940
19851
|
}
|
|
19941
19852
|
} })
|
|
@@ -19976,41 +19887,6 @@ const noFullLodashImport = defineRule({
|
|
|
19976
19887
|
} })
|
|
19977
19888
|
});
|
|
19978
19889
|
//#endregion
|
|
19979
|
-
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19980
|
-
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19981
|
-
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19982
|
-
const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
|
|
19983
|
-
const noFullViewportWidth = defineRule({
|
|
19984
|
-
id: "no-full-viewport-width",
|
|
19985
|
-
title: "Full viewport width causes overflow",
|
|
19986
|
-
tags: ["design", "test-noise"],
|
|
19987
|
-
severity: "warn",
|
|
19988
|
-
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19989
|
-
create: (context) => ({
|
|
19990
|
-
JSXAttribute(node) {
|
|
19991
|
-
const expression = getInlineStyleExpression(node);
|
|
19992
|
-
if (!expression) return;
|
|
19993
|
-
for (const property of expression.properties ?? []) {
|
|
19994
|
-
const key = getStylePropertyKey(property);
|
|
19995
|
-
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19996
|
-
const value = getStylePropertyStringValue(property);
|
|
19997
|
-
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19998
|
-
node: property,
|
|
19999
|
-
message: MESSAGE$25
|
|
20000
|
-
});
|
|
20001
|
-
}
|
|
20002
|
-
},
|
|
20003
|
-
JSXOpeningElement(node) {
|
|
20004
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
20005
|
-
if (!classNameValue) return;
|
|
20006
|
-
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
20007
|
-
node,
|
|
20008
|
-
message: MESSAGE$25
|
|
20009
|
-
});
|
|
20010
|
-
}
|
|
20011
|
-
})
|
|
20012
|
-
});
|
|
20013
|
-
//#endregion
|
|
20014
19890
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
20015
19891
|
const noGenericHandlerNames = defineRule({
|
|
20016
19892
|
id: "no-generic-handler-names",
|
|
@@ -20073,7 +19949,7 @@ const noGiantComponent = defineRule({
|
|
|
20073
19949
|
});
|
|
20074
19950
|
//#endregion
|
|
20075
19951
|
//#region src/plugin/constants/style.ts
|
|
20076
|
-
const LAYOUT_PROPERTIES
|
|
19952
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
20077
19953
|
"width",
|
|
20078
19954
|
"height",
|
|
20079
19955
|
"top",
|
|
@@ -20143,6 +20019,17 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
20143
20019
|
} })
|
|
20144
20020
|
});
|
|
20145
20021
|
//#endregion
|
|
20022
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
20023
|
+
const getStringFromClassNameAttr = (node) => {
|
|
20024
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
20025
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
20026
|
+
if (!classAttr?.value) return null;
|
|
20027
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
20028
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
20029
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
20030
|
+
return null;
|
|
20031
|
+
};
|
|
20032
|
+
//#endregion
|
|
20146
20033
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
20147
20034
|
const noGradientText = defineRule({
|
|
20148
20035
|
id: "no-gradient-text",
|
|
@@ -20201,7 +20088,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20201
20088
|
});
|
|
20202
20089
|
//#endregion
|
|
20203
20090
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20204
|
-
const MESSAGE$
|
|
20091
|
+
const MESSAGE$21 = "`<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.";
|
|
20205
20092
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20206
20093
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20207
20094
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20215,7 +20102,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20215
20102
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20216
20103
|
context.report({
|
|
20217
20104
|
node: node.name,
|
|
20218
|
-
message: MESSAGE$
|
|
20105
|
+
message: MESSAGE$21
|
|
20219
20106
|
});
|
|
20220
20107
|
} })
|
|
20221
20108
|
});
|
|
@@ -20312,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
20312
20199
|
severity: "warn",
|
|
20313
20200
|
tags: ["test-noise", "react-jsx-only"],
|
|
20314
20201
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
20315
|
-
create: (context) =>
|
|
20316
|
-
|
|
20317
|
-
|
|
20318
|
-
|
|
20319
|
-
|
|
20320
|
-
|
|
20321
|
-
|
|
20322
|
-
|
|
20323
|
-
|
|
20202
|
+
create: (context) => {
|
|
20203
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20204
|
+
return { JSXAttribute(node) {
|
|
20205
|
+
const expression = getInlineStyleExpression(node);
|
|
20206
|
+
if (!expression) return;
|
|
20207
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20208
|
+
if (propertyCount < 8) return;
|
|
20209
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20210
|
+
context.report({
|
|
20211
|
+
node: expression,
|
|
20212
|
+
message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
|
|
20213
|
+
});
|
|
20214
|
+
} };
|
|
20215
|
+
}
|
|
20324
20216
|
});
|
|
20325
20217
|
//#endregion
|
|
20326
20218
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -20450,7 +20342,7 @@ const noIsMounted = defineRule({
|
|
|
20450
20342
|
});
|
|
20451
20343
|
//#endregion
|
|
20452
20344
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20453
|
-
const MESSAGE$
|
|
20345
|
+
const MESSAGE$20 = "`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)`.";
|
|
20454
20346
|
const isJsonMethodCall = (node, method) => {
|
|
20455
20347
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20456
20348
|
const callee = node.callee;
|
|
@@ -20467,13 +20359,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20467
20359
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20468
20360
|
context.report({
|
|
20469
20361
|
node,
|
|
20470
|
-
message: MESSAGE$
|
|
20362
|
+
message: MESSAGE$20
|
|
20471
20363
|
});
|
|
20472
20364
|
} })
|
|
20473
20365
|
});
|
|
20474
20366
|
//#endregion
|
|
20475
20367
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20476
|
-
const MESSAGE$
|
|
20368
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20477
20369
|
const isJsxElementTypeReference = (node) => {
|
|
20478
20370
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20479
20371
|
const typeName = node.typeName;
|
|
@@ -20490,7 +20382,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20490
20382
|
if (!typeAnnotation) return;
|
|
20491
20383
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20492
20384
|
node: typeAnnotation,
|
|
20493
|
-
message: MESSAGE$
|
|
20385
|
+
message: MESSAGE$19
|
|
20494
20386
|
});
|
|
20495
20387
|
};
|
|
20496
20388
|
const noJsxElementType = defineRule({
|
|
@@ -20600,7 +20492,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20600
20492
|
let propertyName = null;
|
|
20601
20493
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20602
20494
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20603
|
-
if (propertyName && LAYOUT_PROPERTIES
|
|
20495
|
+
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20604
20496
|
node: property,
|
|
20605
20497
|
message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
|
|
20606
20498
|
});
|
|
@@ -20790,134 +20682,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20790
20682
|
} })
|
|
20791
20683
|
});
|
|
20792
20684
|
//#endregion
|
|
20793
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20794
|
-
const getStylePropertyNumberValue = (property) => {
|
|
20795
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
20796
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20797
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20798
|
-
return null;
|
|
20799
|
-
};
|
|
20800
|
-
//#endregion
|
|
20801
|
-
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20802
|
-
const linearizeChannel = (channel) => {
|
|
20803
|
-
const normalized = channel / 255;
|
|
20804
|
-
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20805
|
-
};
|
|
20806
|
-
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20807
|
-
const getWcagContrastRatio = (foreground, background) => {
|
|
20808
|
-
const foregroundLuminance = relativeLuminance(foreground);
|
|
20809
|
-
const backgroundLuminance = relativeLuminance(background);
|
|
20810
|
-
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20811
|
-
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20812
|
-
return (lighter + .05) / (darker + .05);
|
|
20813
|
-
};
|
|
20814
|
-
//#endregion
|
|
20815
|
-
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20816
|
-
const UNRESOLVABLE = new Set([
|
|
20817
|
-
"transparent",
|
|
20818
|
-
"currentcolor",
|
|
20819
|
-
"inherit",
|
|
20820
|
-
"initial",
|
|
20821
|
-
"unset",
|
|
20822
|
-
"revert",
|
|
20823
|
-
"none"
|
|
20824
|
-
]);
|
|
20825
|
-
const resolveOpaqueColor = (raw) => {
|
|
20826
|
-
const value = raw.trim().toLowerCase();
|
|
20827
|
-
if (UNRESOLVABLE.has(value)) return null;
|
|
20828
|
-
if (value === "white") return {
|
|
20829
|
-
red: 255,
|
|
20830
|
-
green: 255,
|
|
20831
|
-
blue: 255
|
|
20832
|
-
};
|
|
20833
|
-
if (value === "black") return {
|
|
20834
|
-
red: 0,
|
|
20835
|
-
green: 0,
|
|
20836
|
-
blue: 0
|
|
20837
|
-
};
|
|
20838
|
-
if (value.startsWith("var(")) return null;
|
|
20839
|
-
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20840
|
-
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20841
|
-
if (value.startsWith("rgb")) {
|
|
20842
|
-
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20843
|
-
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20844
|
-
}
|
|
20845
|
-
return parseColorToRgb(value);
|
|
20846
|
-
};
|
|
20847
|
-
const toPx = (property) => {
|
|
20848
|
-
const numberValue = getStylePropertyNumberValue(property);
|
|
20849
|
-
if (numberValue !== null) return numberValue;
|
|
20850
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20851
|
-
if (stringValue === null) return null;
|
|
20852
|
-
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20853
|
-
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20854
|
-
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20855
|
-
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20856
|
-
return null;
|
|
20857
|
-
};
|
|
20858
|
-
const isBoldWeight = (property) => {
|
|
20859
|
-
const numberValue = getStylePropertyNumberValue(property);
|
|
20860
|
-
if (numberValue !== null) return numberValue >= 700;
|
|
20861
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20862
|
-
if (stringValue === null) return false;
|
|
20863
|
-
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20864
|
-
const numericWeight = Number(stringValue);
|
|
20865
|
-
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20866
|
-
};
|
|
20867
|
-
const noLowContrastInlineStyle = defineRule({
|
|
20868
|
-
id: "no-low-contrast-inline-style",
|
|
20869
|
-
title: "Low-contrast text in inline style",
|
|
20870
|
-
tags: ["test-noise"],
|
|
20871
|
-
severity: "warn",
|
|
20872
|
-
category: "Accessibility",
|
|
20873
|
-
recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
|
|
20874
|
-
create: (context) => ({ JSXAttribute(node) {
|
|
20875
|
-
const expression = getInlineStyleExpression(node);
|
|
20876
|
-
if (!expression) return;
|
|
20877
|
-
const properties = expression.properties ?? [];
|
|
20878
|
-
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20879
|
-
let foreground = null;
|
|
20880
|
-
let backgroundColorRaw = null;
|
|
20881
|
-
let backgroundShorthandRaw = null;
|
|
20882
|
-
let backgroundIsUnknown = false;
|
|
20883
|
-
let fontSizePx = null;
|
|
20884
|
-
let isBold = false;
|
|
20885
|
-
for (const property of properties) {
|
|
20886
|
-
const key = getStylePropertyKey(property);
|
|
20887
|
-
if (!key) continue;
|
|
20888
|
-
if (key === "backgroundImage") {
|
|
20889
|
-
backgroundIsUnknown = true;
|
|
20890
|
-
continue;
|
|
20891
|
-
}
|
|
20892
|
-
if (key === "fontSize" && property.type === "Property") {
|
|
20893
|
-
fontSizePx = toPx(property);
|
|
20894
|
-
continue;
|
|
20895
|
-
}
|
|
20896
|
-
if (key === "fontWeight" && property.type === "Property") {
|
|
20897
|
-
isBold = isBoldWeight(property);
|
|
20898
|
-
continue;
|
|
20899
|
-
}
|
|
20900
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20901
|
-
if (key === "color") {
|
|
20902
|
-
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20903
|
-
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20904
|
-
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20905
|
-
else backgroundShorthandRaw = stringValue;
|
|
20906
|
-
}
|
|
20907
|
-
if (backgroundIsUnknown) return;
|
|
20908
|
-
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20909
|
-
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20910
|
-
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20911
|
-
if (!foreground || !background) return;
|
|
20912
|
-
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20913
|
-
const ratio = getWcagContrastRatio(foreground, background);
|
|
20914
|
-
if (ratio < threshold) context.report({
|
|
20915
|
-
node,
|
|
20916
|
-
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
20917
|
-
});
|
|
20918
|
-
} })
|
|
20919
|
-
});
|
|
20920
|
-
//#endregion
|
|
20921
20685
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20922
20686
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20923
20687
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -21073,7 +20837,7 @@ const noMoment = defineRule({
|
|
|
21073
20837
|
});
|
|
21074
20838
|
//#endregion
|
|
21075
20839
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
21076
|
-
const MESSAGE$
|
|
20840
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
21077
20841
|
const resolveSettings$16 = (settings) => {
|
|
21078
20842
|
const reactDoctor = settings?.["react-doctor"];
|
|
21079
20843
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21395,7 +21159,7 @@ const noMultiComp = defineRule({
|
|
|
21395
21159
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21396
21160
|
for (const component of flagged.slice(1)) context.report({
|
|
21397
21161
|
node: component.reportNode,
|
|
21398
|
-
message: MESSAGE$
|
|
21162
|
+
message: MESSAGE$18
|
|
21399
21163
|
});
|
|
21400
21164
|
} };
|
|
21401
21165
|
}
|
|
@@ -21563,7 +21327,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21563
21327
|
};
|
|
21564
21328
|
//#endregion
|
|
21565
21329
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21566
|
-
const MESSAGE$
|
|
21330
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21567
21331
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21568
21332
|
"copyWithin",
|
|
21569
21333
|
"fill",
|
|
@@ -21773,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21773
21537
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21774
21538
|
context.report({
|
|
21775
21539
|
node: options.crossFileConsumerCallSite,
|
|
21776
|
-
message: `${MESSAGE$
|
|
21540
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21777
21541
|
});
|
|
21778
21542
|
return;
|
|
21779
21543
|
}
|
|
@@ -21782,7 +21546,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21782
21546
|
reportedNodes.add(mutation.node);
|
|
21783
21547
|
context.report({
|
|
21784
21548
|
node: mutation.node,
|
|
21785
|
-
message: MESSAGE$
|
|
21549
|
+
message: MESSAGE$17
|
|
21786
21550
|
});
|
|
21787
21551
|
}
|
|
21788
21552
|
};
|
|
@@ -22054,7 +21818,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
22054
21818
|
});
|
|
22055
21819
|
//#endregion
|
|
22056
21820
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
22057
|
-
const MESSAGE$
|
|
21821
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
22058
21822
|
const resolveSettings$14 = (settings) => {
|
|
22059
21823
|
const reactDoctor = settings?.["react-doctor"];
|
|
22060
21824
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -22082,7 +21846,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22082
21846
|
if (numeric === null) {
|
|
22083
21847
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
22084
21848
|
node: tabIndex,
|
|
22085
|
-
message: MESSAGE$
|
|
21849
|
+
message: MESSAGE$16
|
|
22086
21850
|
});
|
|
22087
21851
|
return;
|
|
22088
21852
|
}
|
|
@@ -22095,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22095
21859
|
if (!roleAttribute) {
|
|
22096
21860
|
context.report({
|
|
22097
21861
|
node: tabIndex,
|
|
22098
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
22099
21863
|
});
|
|
22100
21864
|
return;
|
|
22101
21865
|
}
|
|
@@ -22109,12 +21873,20 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22109
21873
|
}
|
|
22110
21874
|
context.report({
|
|
22111
21875
|
node: tabIndex,
|
|
22112
|
-
message: MESSAGE$
|
|
21876
|
+
message: MESSAGE$16
|
|
22113
21877
|
});
|
|
22114
21878
|
} };
|
|
22115
21879
|
}
|
|
22116
21880
|
});
|
|
22117
21881
|
//#endregion
|
|
21882
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21883
|
+
const getStylePropertyNumberValue = (property) => {
|
|
21884
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
21885
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21886
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21887
|
+
return null;
|
|
21888
|
+
};
|
|
21889
|
+
//#endregion
|
|
22118
21890
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
22119
21891
|
const noOutlineNone = defineRule({
|
|
22120
21892
|
id: "no-outline-none",
|
|
@@ -22792,7 +22564,7 @@ const noRandomKey = defineRule({
|
|
|
22792
22564
|
});
|
|
22793
22565
|
//#endregion
|
|
22794
22566
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22795
|
-
const MESSAGE$
|
|
22567
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22796
22568
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22797
22569
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22798
22570
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22818,13 +22590,13 @@ const noReactChildren = defineRule({
|
|
|
22818
22590
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22819
22591
|
context.report({
|
|
22820
22592
|
node: calleeOuter,
|
|
22821
|
-
message: MESSAGE$
|
|
22593
|
+
message: MESSAGE$15
|
|
22822
22594
|
});
|
|
22823
22595
|
return;
|
|
22824
22596
|
}
|
|
22825
22597
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22826
22598
|
node: calleeOuter,
|
|
22827
|
-
message: MESSAGE$
|
|
22599
|
+
message: MESSAGE$15
|
|
22828
22600
|
});
|
|
22829
22601
|
} })
|
|
22830
22602
|
});
|
|
@@ -22935,86 +22707,6 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22935
22707
|
})
|
|
22936
22708
|
});
|
|
22937
22709
|
//#endregion
|
|
22938
|
-
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22939
|
-
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22940
|
-
"div",
|
|
22941
|
-
"p",
|
|
22942
|
-
"section",
|
|
22943
|
-
"article",
|
|
22944
|
-
"main",
|
|
22945
|
-
"header",
|
|
22946
|
-
"footer",
|
|
22947
|
-
"nav",
|
|
22948
|
-
"aside",
|
|
22949
|
-
"figure",
|
|
22950
|
-
"figcaption",
|
|
22951
|
-
"blockquote",
|
|
22952
|
-
"form",
|
|
22953
|
-
"fieldset",
|
|
22954
|
-
"address",
|
|
22955
|
-
"pre",
|
|
22956
|
-
"ul",
|
|
22957
|
-
"ol",
|
|
22958
|
-
"dl",
|
|
22959
|
-
"dt",
|
|
22960
|
-
"dd",
|
|
22961
|
-
"h1",
|
|
22962
|
-
"h2",
|
|
22963
|
-
"h3",
|
|
22964
|
-
"h4",
|
|
22965
|
-
"h5",
|
|
22966
|
-
"h6"
|
|
22967
|
-
]);
|
|
22968
|
-
const INLINE_DEFAULT_TAGS = new Set([
|
|
22969
|
-
"span",
|
|
22970
|
-
"a",
|
|
22971
|
-
"b",
|
|
22972
|
-
"i",
|
|
22973
|
-
"em",
|
|
22974
|
-
"strong",
|
|
22975
|
-
"small",
|
|
22976
|
-
"code",
|
|
22977
|
-
"abbr",
|
|
22978
|
-
"cite",
|
|
22979
|
-
"label",
|
|
22980
|
-
"mark",
|
|
22981
|
-
"q",
|
|
22982
|
-
"s",
|
|
22983
|
-
"u",
|
|
22984
|
-
"sub",
|
|
22985
|
-
"sup",
|
|
22986
|
-
"kbd",
|
|
22987
|
-
"samp",
|
|
22988
|
-
"var",
|
|
22989
|
-
"time"
|
|
22990
|
-
]);
|
|
22991
|
-
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22992
|
-
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22993
|
-
const noRedundantDisplayClass = defineRule({
|
|
22994
|
-
id: "no-redundant-display-class",
|
|
22995
|
-
title: "Redundant display utility",
|
|
22996
|
-
tags: ["design", "test-noise"],
|
|
22997
|
-
severity: "warn",
|
|
22998
|
-
recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
|
|
22999
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
23000
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23001
|
-
const tagName = node.name.name;
|
|
23002
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
23003
|
-
if (!classNameValue) return;
|
|
23004
|
-
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
23005
|
-
context.report({
|
|
23006
|
-
node,
|
|
23007
|
-
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23008
|
-
});
|
|
23009
|
-
return;
|
|
23010
|
-
}
|
|
23011
|
-
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
23012
|
-
node,
|
|
23013
|
-
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23014
|
-
});
|
|
23015
|
-
} })
|
|
23016
|
-
});
|
|
23017
|
-
//#endregion
|
|
23018
22710
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
23019
22711
|
const ELEMENT_ROLE_PAIRS = [
|
|
23020
22712
|
["a", "link"],
|
|
@@ -23227,7 +22919,7 @@ const noRenderPropChildren = defineRule({
|
|
|
23227
22919
|
});
|
|
23228
22920
|
//#endregion
|
|
23229
22921
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
23230
|
-
const MESSAGE$
|
|
22922
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
23231
22923
|
const isReactDomRenderCall = (node) => {
|
|
23232
22924
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
23233
22925
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -23251,7 +22943,7 @@ const noRenderReturnValue = defineRule({
|
|
|
23251
22943
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
23252
22944
|
context.report({
|
|
23253
22945
|
node: node.callee,
|
|
23254
|
-
message: MESSAGE$
|
|
22946
|
+
message: MESSAGE$14
|
|
23255
22947
|
});
|
|
23256
22948
|
} })
|
|
23257
22949
|
});
|
|
@@ -23949,7 +23641,7 @@ const getParentComponent = (node) => {
|
|
|
23949
23641
|
};
|
|
23950
23642
|
//#endregion
|
|
23951
23643
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23952
|
-
const MESSAGE$
|
|
23644
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23953
23645
|
const noSetState = defineRule({
|
|
23954
23646
|
id: "no-set-state",
|
|
23955
23647
|
title: "Local class state forbidden",
|
|
@@ -23964,7 +23656,7 @@ const noSetState = defineRule({
|
|
|
23964
23656
|
if (!getParentComponent(node)) return;
|
|
23965
23657
|
context.report({
|
|
23966
23658
|
node: node.callee,
|
|
23967
|
-
message: MESSAGE$
|
|
23659
|
+
message: MESSAGE$13
|
|
23968
23660
|
});
|
|
23969
23661
|
} })
|
|
23970
23662
|
});
|
|
@@ -24126,7 +23818,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
24126
23818
|
};
|
|
24127
23819
|
//#endregion
|
|
24128
23820
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
24129
|
-
const MESSAGE$
|
|
23821
|
+
const MESSAGE$12 = "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.";
|
|
24130
23822
|
const DEFAULT_HANDLERS = [
|
|
24131
23823
|
"onClick",
|
|
24132
23824
|
"onMouseDown",
|
|
@@ -24186,7 +23878,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
24186
23878
|
if (!roleAttribute || !roleAttribute.value) {
|
|
24187
23879
|
context.report({
|
|
24188
23880
|
node: node.name,
|
|
24189
|
-
message: MESSAGE$
|
|
23881
|
+
message: MESSAGE$12
|
|
24190
23882
|
});
|
|
24191
23883
|
return;
|
|
24192
23884
|
}
|
|
@@ -24196,14 +23888,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
24196
23888
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
24197
23889
|
context.report({
|
|
24198
23890
|
node: node.name,
|
|
24199
|
-
message: MESSAGE$
|
|
23891
|
+
message: MESSAGE$12
|
|
24200
23892
|
});
|
|
24201
23893
|
return;
|
|
24202
23894
|
}
|
|
24203
23895
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
24204
23896
|
context.report({
|
|
24205
23897
|
node: node.name,
|
|
24206
|
-
message: MESSAGE$
|
|
23898
|
+
message: MESSAGE$12
|
|
24207
23899
|
});
|
|
24208
23900
|
} };
|
|
24209
23901
|
}
|
|
@@ -24306,43 +23998,8 @@ const noStringRefs = defineRule({
|
|
|
24306
23998
|
}
|
|
24307
23999
|
});
|
|
24308
24000
|
//#endregion
|
|
24309
|
-
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24310
|
-
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24311
|
-
if (token.includes(":")) return false;
|
|
24312
|
-
if (!token.startsWith(prefix)) return false;
|
|
24313
|
-
const value = token.slice(prefix.length);
|
|
24314
|
-
if (value === "" || value === "current") return false;
|
|
24315
|
-
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24316
|
-
return true;
|
|
24317
|
-
});
|
|
24318
|
-
const isCurrentColor = (attribute) => {
|
|
24319
|
-
const value = getJsxPropStringValue(attribute);
|
|
24320
|
-
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24321
|
-
};
|
|
24322
|
-
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24323
|
-
id: "no-svg-currentcolor-with-fill-class",
|
|
24324
|
-
title: "currentColor fights a fill/stroke class",
|
|
24325
|
-
tags: ["design", "test-noise"],
|
|
24326
|
-
severity: "warn",
|
|
24327
|
-
recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
|
|
24328
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24329
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24330
|
-
if (!classNameValue) return;
|
|
24331
|
-
for (const paint of ["fill", "stroke"]) {
|
|
24332
|
-
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24333
|
-
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24334
|
-
context.report({
|
|
24335
|
-
node: attribute,
|
|
24336
|
-
message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
|
|
24337
|
-
});
|
|
24338
|
-
return;
|
|
24339
|
-
}
|
|
24340
|
-
}
|
|
24341
|
-
} })
|
|
24342
|
-
});
|
|
24343
|
-
//#endregion
|
|
24344
24001
|
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24345
|
-
const MESSAGE$
|
|
24002
|
+
const MESSAGE$11 = "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)`).";
|
|
24346
24003
|
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24347
24004
|
const noSyncXhr = defineRule({
|
|
24348
24005
|
id: "no-sync-xhr",
|
|
@@ -24357,103 +24014,13 @@ const noSyncXhr = defineRule({
|
|
|
24357
24014
|
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24358
24015
|
context.report({
|
|
24359
24016
|
node,
|
|
24360
|
-
message: MESSAGE$
|
|
24361
|
-
});
|
|
24362
|
-
} })
|
|
24363
|
-
});
|
|
24364
|
-
//#endregion
|
|
24365
|
-
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24366
|
-
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24367
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
24368
|
-
"width",
|
|
24369
|
-
"height",
|
|
24370
|
-
"min-width",
|
|
24371
|
-
"max-width",
|
|
24372
|
-
"min-height",
|
|
24373
|
-
"max-height",
|
|
24374
|
-
"top",
|
|
24375
|
-
"left",
|
|
24376
|
-
"right",
|
|
24377
|
-
"bottom",
|
|
24378
|
-
"inset",
|
|
24379
|
-
"inset-block",
|
|
24380
|
-
"inset-inline",
|
|
24381
|
-
"margin",
|
|
24382
|
-
"margin-top",
|
|
24383
|
-
"margin-right",
|
|
24384
|
-
"margin-bottom",
|
|
24385
|
-
"margin-left",
|
|
24386
|
-
"margin-block",
|
|
24387
|
-
"margin-inline",
|
|
24388
|
-
"padding",
|
|
24389
|
-
"padding-top",
|
|
24390
|
-
"padding-right",
|
|
24391
|
-
"padding-bottom",
|
|
24392
|
-
"padding-left",
|
|
24393
|
-
"padding-block",
|
|
24394
|
-
"padding-inline"
|
|
24395
|
-
]);
|
|
24396
|
-
const noTailwindLayoutTransition = defineRule({
|
|
24397
|
-
id: "no-tailwind-layout-transition",
|
|
24398
|
-
title: "Animating a layout property",
|
|
24399
|
-
tags: ["design", "test-noise"],
|
|
24400
|
-
severity: "warn",
|
|
24401
|
-
category: "Performance",
|
|
24402
|
-
recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
|
|
24403
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24404
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24405
|
-
if (!classNameValue) return;
|
|
24406
|
-
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24407
|
-
const animatedProperties = transitionMatch[1];
|
|
24408
|
-
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24409
|
-
if (layoutProperty) context.report({
|
|
24410
|
-
node,
|
|
24411
|
-
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24412
|
-
});
|
|
24413
|
-
}
|
|
24414
|
-
} })
|
|
24415
|
-
});
|
|
24416
|
-
//#endregion
|
|
24417
|
-
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24418
|
-
const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
|
|
24419
|
-
const targetIsBlank = (attribute) => {
|
|
24420
|
-
const stringValue = getJsxPropStringValue(attribute);
|
|
24421
|
-
if (stringValue !== null) return stringValue === "_blank";
|
|
24422
|
-
const value = attribute.value;
|
|
24423
|
-
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24424
|
-
const expression = value.expression;
|
|
24425
|
-
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24426
|
-
}
|
|
24427
|
-
return false;
|
|
24428
|
-
};
|
|
24429
|
-
const noTargetBlankWithoutRel = defineRule({
|
|
24430
|
-
id: "no-target-blank-without-rel",
|
|
24431
|
-
title: "target=_blank without rel=noopener",
|
|
24432
|
-
severity: "warn",
|
|
24433
|
-
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24434
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24435
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24436
|
-
const tagName = node.name.name;
|
|
24437
|
-
if (tagName !== "a" && tagName !== "area") return;
|
|
24438
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24439
|
-
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24440
|
-
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24441
|
-
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24442
|
-
if (relAttribute) {
|
|
24443
|
-
const relValue = getJsxPropStringValue(relAttribute);
|
|
24444
|
-
if (relValue === null) return;
|
|
24445
|
-
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24446
|
-
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24447
|
-
}
|
|
24448
|
-
context.report({
|
|
24449
|
-
node: node.name,
|
|
24450
|
-
message: MESSAGE$13
|
|
24017
|
+
message: MESSAGE$11
|
|
24451
24018
|
});
|
|
24452
24019
|
} })
|
|
24453
24020
|
});
|
|
24454
24021
|
//#endregion
|
|
24455
24022
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
24456
|
-
const MESSAGE$
|
|
24023
|
+
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
24457
24024
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
24458
24025
|
let ancestor = node.parent;
|
|
24459
24026
|
while (ancestor) {
|
|
@@ -24522,7 +24089,7 @@ const noThisInSfc = defineRule({
|
|
|
24522
24089
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
24523
24090
|
context.report({
|
|
24524
24091
|
node,
|
|
24525
|
-
message: MESSAGE$
|
|
24092
|
+
message: MESSAGE$10
|
|
24526
24093
|
});
|
|
24527
24094
|
} };
|
|
24528
24095
|
}
|
|
@@ -24560,39 +24127,26 @@ const noTinyText = defineRule({
|
|
|
24560
24127
|
});
|
|
24561
24128
|
//#endregion
|
|
24562
24129
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24563
|
-
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24564
|
-
const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
|
|
24565
24130
|
const noTransitionAll = defineRule({
|
|
24566
24131
|
id: "no-transition-all",
|
|
24567
24132
|
title: "transition: all animates everything",
|
|
24568
24133
|
tags: ["test-noise"],
|
|
24569
24134
|
severity: "warn",
|
|
24570
24135
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
24571
|
-
create: (context) => ({
|
|
24572
|
-
|
|
24573
|
-
|
|
24574
|
-
|
|
24575
|
-
|
|
24576
|
-
|
|
24577
|
-
|
|
24578
|
-
|
|
24579
|
-
|
|
24580
|
-
|
|
24581
|
-
|
|
24582
|
-
node: property,
|
|
24583
|
-
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24584
|
-
});
|
|
24585
|
-
}
|
|
24586
|
-
},
|
|
24587
|
-
JSXOpeningElement(node) {
|
|
24588
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24589
|
-
if (!classNameValue) return;
|
|
24590
|
-
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24591
|
-
node,
|
|
24592
|
-
message: TAILWIND_MESSAGE
|
|
24136
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
24137
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24138
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24139
|
+
const expression = node.value.expression;
|
|
24140
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24141
|
+
for (const property of expression.properties ?? []) {
|
|
24142
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24143
|
+
if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
|
|
24144
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
|
|
24145
|
+
node: property,
|
|
24146
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24593
24147
|
});
|
|
24594
24148
|
}
|
|
24595
|
-
})
|
|
24149
|
+
} })
|
|
24596
24150
|
});
|
|
24597
24151
|
//#endregion
|
|
24598
24152
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -24636,6 +24190,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
24636
24190
|
}
|
|
24637
24191
|
return stateNames;
|
|
24638
24192
|
};
|
|
24193
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
24639
24194
|
const noUncontrolledInput = defineRule({
|
|
24640
24195
|
id: "no-uncontrolled-input",
|
|
24641
24196
|
title: "Uncontrolled input value",
|
|
@@ -24739,38 +24294,6 @@ const noUnescapedEntities = defineRule({
|
|
|
24739
24294
|
} })
|
|
24740
24295
|
});
|
|
24741
24296
|
//#endregion
|
|
24742
|
-
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24743
|
-
const UNINFORMATIVE_LABELS = new Set([
|
|
24744
|
-
"icon",
|
|
24745
|
-
"button",
|
|
24746
|
-
"image",
|
|
24747
|
-
"img",
|
|
24748
|
-
"link",
|
|
24749
|
-
"graphic",
|
|
24750
|
-
"svg",
|
|
24751
|
-
"picture",
|
|
24752
|
-
"element",
|
|
24753
|
-
"field",
|
|
24754
|
-
"input"
|
|
24755
|
-
]);
|
|
24756
|
-
const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
|
|
24757
|
-
const noUninformativeAriaLabel = defineRule({
|
|
24758
|
-
id: "no-uninformative-aria-label",
|
|
24759
|
-
title: "Uninformative aria-label",
|
|
24760
|
-
severity: "warn",
|
|
24761
|
-
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24762
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24763
|
-
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24764
|
-
if (!ariaLabel) return;
|
|
24765
|
-
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24766
|
-
if (labelValue === null) return;
|
|
24767
|
-
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24768
|
-
node: ariaLabel,
|
|
24769
|
-
message: MESSAGE$11
|
|
24770
|
-
});
|
|
24771
|
-
} })
|
|
24772
|
-
});
|
|
24773
|
-
//#endregion
|
|
24774
24297
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24775
24298
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24776
24299
|
"activedescendant",
|
|
@@ -25905,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
25905
25428
|
visit(root);
|
|
25906
25429
|
return found;
|
|
25907
25430
|
};
|
|
25908
|
-
const classExtendsReactComponent$1 = (classNode) => {
|
|
25909
|
-
const superClass = classNode.superClass;
|
|
25910
|
-
if (!superClass) return false;
|
|
25911
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
25912
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
25913
|
-
return false;
|
|
25914
|
-
};
|
|
25915
25431
|
const isReactClassComponent = (classNode) => {
|
|
25916
|
-
if (
|
|
25432
|
+
if (isEs6Component(classNode)) return true;
|
|
25917
25433
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
25918
25434
|
};
|
|
25919
25435
|
const findEnclosingComponent = (node) => {
|
|
@@ -26073,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26073
25589
|
create: (context) => {
|
|
26074
25590
|
const settings = resolveSettings$8(context.settings);
|
|
26075
25591
|
const renderPropRegex = compileGlob(settings.propNamePattern);
|
|
26076
|
-
const reportCandidate = (candidateNode, reportNode
|
|
25592
|
+
const reportCandidate = (candidateNode, reportNode) => {
|
|
26077
25593
|
if (isFirstArgumentOfHocCall(candidateNode)) return;
|
|
26078
25594
|
if (isReturnOfMapCallback(candidateNode)) return;
|
|
26079
25595
|
const propInfo = isComponentDeclaredInProp(candidateNode);
|
|
@@ -26094,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26094
25610
|
const inferredName = inferFunctionLikeName(node);
|
|
26095
25611
|
const propInfo = isComponentDeclaredInProp(node);
|
|
26096
25612
|
if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
|
|
26097
|
-
reportCandidate(node, node
|
|
25613
|
+
reportCandidate(node, node);
|
|
26098
25614
|
};
|
|
26099
25615
|
return {
|
|
26100
25616
|
FunctionDeclaration: checkFunctionLike,
|
|
@@ -26104,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26104
25620
|
if (!node.id) return;
|
|
26105
25621
|
if (!isReactComponentName(node.id.name)) return;
|
|
26106
25622
|
if (!isReactClassComponent(node)) return;
|
|
26107
|
-
reportCandidate(node, node
|
|
25623
|
+
reportCandidate(node, node);
|
|
26108
25624
|
},
|
|
26109
25625
|
ClassExpression(node) {
|
|
26110
25626
|
const inferredName = node.id?.name ?? inferFunctionLikeName(node);
|
|
26111
25627
|
if (!inferredName || !isReactComponentName(inferredName)) return;
|
|
26112
25628
|
if (!isReactClassComponent(node)) return;
|
|
26113
|
-
reportCandidate(node, node
|
|
25629
|
+
reportCandidate(node, node);
|
|
26114
25630
|
},
|
|
26115
25631
|
CallExpression(node) {
|
|
26116
25632
|
if (!isHocCallee$1(node)) return;
|
|
26117
25633
|
if (!hocCallContainsComponent(node)) return;
|
|
26118
|
-
reportCandidate(node, node
|
|
25634
|
+
reportCandidate(node, node);
|
|
26119
25635
|
}
|
|
26120
25636
|
};
|
|
26121
25637
|
}
|
|
@@ -26242,7 +25758,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
26242
25758
|
//#endregion
|
|
26243
25759
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
26244
25760
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
26245
|
-
const MESSAGE$
|
|
25761
|
+
const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
26246
25762
|
const resolveSettings$7 = (settings) => {
|
|
26247
25763
|
const reactDoctor = settings?.["react-doctor"];
|
|
26248
25764
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -26276,7 +25792,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
26276
25792
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
26277
25793
|
context.report({
|
|
26278
25794
|
node: node.callee,
|
|
26279
|
-
message: MESSAGE$
|
|
25795
|
+
message: MESSAGE$9
|
|
26280
25796
|
});
|
|
26281
25797
|
} };
|
|
26282
25798
|
}
|
|
@@ -26555,13 +26071,6 @@ const skipTsExpression = (expression) => {
|
|
|
26555
26071
|
if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
|
|
26556
26072
|
return expression;
|
|
26557
26073
|
};
|
|
26558
|
-
const classExtendsReactComponent = (classNode) => {
|
|
26559
|
-
const superClass = classNode.superClass;
|
|
26560
|
-
if (!superClass) return false;
|
|
26561
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
26562
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
26563
|
-
return false;
|
|
26564
|
-
};
|
|
26565
26074
|
const isReactCreateContext = (initializer) => {
|
|
26566
26075
|
if (!initializer) return false;
|
|
26567
26076
|
const expression = skipTsExpression(initializer);
|
|
@@ -26752,7 +26261,7 @@ const onlyExportComponents = defineRule({
|
|
|
26752
26261
|
if (stripped.id) {
|
|
26753
26262
|
const idNode = stripped.id;
|
|
26754
26263
|
isExportedNodeIds.add(stripped);
|
|
26755
|
-
if (isReactComponentName(idNode.name) &&
|
|
26264
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
26756
26265
|
else exports.push({
|
|
26757
26266
|
kind: "non-component",
|
|
26758
26267
|
reportNode: idNode
|
|
@@ -26812,7 +26321,7 @@ const onlyExportComponents = defineRule({
|
|
|
26812
26321
|
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
26813
26322
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
26814
26323
|
isExportedNodeIds.add(declaration);
|
|
26815
|
-
if (isReactComponentName(declaration.id.name) &&
|
|
26324
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
26816
26325
|
else exports.push({
|
|
26817
26326
|
kind: "non-component",
|
|
26818
26327
|
reportNode: declaration.id
|
|
@@ -27154,7 +26663,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
27154
26663
|
});
|
|
27155
26664
|
//#endregion
|
|
27156
26665
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
27157
|
-
const MESSAGE$
|
|
26666
|
+
const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
27158
26667
|
const preactPreferOndblclick = defineRule({
|
|
27159
26668
|
id: "preact-prefer-ondblclick",
|
|
27160
26669
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -27169,7 +26678,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
27169
26678
|
if (!onDoubleClickAttribute) return;
|
|
27170
26679
|
context.report({
|
|
27171
26680
|
node: onDoubleClickAttribute,
|
|
27172
|
-
message: MESSAGE$
|
|
26681
|
+
message: MESSAGE$8
|
|
27173
26682
|
});
|
|
27174
26683
|
} })
|
|
27175
26684
|
});
|
|
@@ -27209,42 +26718,6 @@ const preactPreferOninput = defineRule({
|
|
|
27209
26718
|
} })
|
|
27210
26719
|
});
|
|
27211
26720
|
//#endregion
|
|
27212
|
-
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27213
|
-
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27214
|
-
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27215
|
-
const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
|
|
27216
|
-
const preferDvhOverVh = defineRule({
|
|
27217
|
-
id: "prefer-dvh-over-vh",
|
|
27218
|
-
title: "Use dvh instead of vh for full height",
|
|
27219
|
-
tags: ["design", "test-noise"],
|
|
27220
|
-
severity: "warn",
|
|
27221
|
-
requires: ["tailwind:3.4"],
|
|
27222
|
-
recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
|
|
27223
|
-
create: (context) => ({
|
|
27224
|
-
JSXAttribute(node) {
|
|
27225
|
-
const expression = getInlineStyleExpression(node);
|
|
27226
|
-
if (!expression) return;
|
|
27227
|
-
for (const property of expression.properties ?? []) {
|
|
27228
|
-
const key = getStylePropertyKey(property);
|
|
27229
|
-
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27230
|
-
const value = getStylePropertyStringValue(property);
|
|
27231
|
-
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27232
|
-
node: property,
|
|
27233
|
-
message: MESSAGE$8
|
|
27234
|
-
});
|
|
27235
|
-
}
|
|
27236
|
-
},
|
|
27237
|
-
JSXOpeningElement(node) {
|
|
27238
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
27239
|
-
if (!classNameValue) return;
|
|
27240
|
-
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27241
|
-
node,
|
|
27242
|
-
message: MESSAGE$8
|
|
27243
|
-
});
|
|
27244
|
-
}
|
|
27245
|
-
})
|
|
27246
|
-
});
|
|
27247
|
-
//#endregion
|
|
27248
26721
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
27249
26722
|
const preferDynamicImport = defineRule({
|
|
27250
26723
|
id: "prefer-dynamic-import",
|
|
@@ -27836,26 +27309,6 @@ const preferTagOverRole = defineRule({
|
|
|
27836
27309
|
} })
|
|
27837
27310
|
});
|
|
27838
27311
|
//#endregion
|
|
27839
|
-
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27840
|
-
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27841
|
-
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27842
|
-
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27843
|
-
const preferTruncateShorthand = defineRule({
|
|
27844
|
-
id: "prefer-truncate-shorthand",
|
|
27845
|
-
title: "Use truncate shorthand",
|
|
27846
|
-
tags: ["design", "test-noise"],
|
|
27847
|
-
severity: "warn",
|
|
27848
|
-
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27849
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
27850
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
27851
|
-
if (!classNameValue) return;
|
|
27852
|
-
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27853
|
-
node,
|
|
27854
|
-
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27855
|
-
});
|
|
27856
|
-
} })
|
|
27857
|
-
});
|
|
27858
|
-
//#endregion
|
|
27859
27312
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27860
27313
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27861
27314
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -35972,6 +35425,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
35972
35425
|
CallExpression(node) {
|
|
35973
35426
|
if (!isServerSideFile) return;
|
|
35974
35427
|
if (!isFetchCall(node)) return;
|
|
35428
|
+
if (isMutatingFetchCall(node)) return;
|
|
35975
35429
|
const optionsArg = node.arguments?.[1];
|
|
35976
35430
|
if (optionsArg && objectExpressionHasNextRevalidate(optionsArg)) return;
|
|
35977
35431
|
const urlArg = node.arguments?.[0];
|
|
@@ -39530,17 +38984,6 @@ const reactDoctorRules = [
|
|
|
39530
38984
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
39531
38985
|
}
|
|
39532
38986
|
},
|
|
39533
|
-
{
|
|
39534
|
-
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39535
|
-
id: "no-arbitrary-px-font-size",
|
|
39536
|
-
source: "react-doctor",
|
|
39537
|
-
originallyExternal: false,
|
|
39538
|
-
rule: {
|
|
39539
|
-
...noArbitraryPxFontSize,
|
|
39540
|
-
framework: "global",
|
|
39541
|
-
category: "Accessibility"
|
|
39542
|
-
}
|
|
39543
|
-
},
|
|
39544
38987
|
{
|
|
39545
38988
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
39546
38989
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -39600,18 +39043,6 @@ const reactDoctorRules = [
|
|
|
39600
39043
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
39601
39044
|
}
|
|
39602
39045
|
},
|
|
39603
|
-
{
|
|
39604
|
-
key: "react-doctor/no-autoplay-without-muted",
|
|
39605
|
-
id: "no-autoplay-without-muted",
|
|
39606
|
-
source: "react-doctor",
|
|
39607
|
-
originallyExternal: false,
|
|
39608
|
-
rule: {
|
|
39609
|
-
...noAutoplayWithoutMuted,
|
|
39610
|
-
framework: "global",
|
|
39611
|
-
category: "Accessibility",
|
|
39612
|
-
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39613
|
-
}
|
|
39614
|
-
},
|
|
39615
39046
|
{
|
|
39616
39047
|
key: "react-doctor/no-barrel-import",
|
|
39617
39048
|
id: "no-barrel-import",
|
|
@@ -39765,17 +39196,6 @@ const reactDoctorRules = [
|
|
|
39765
39196
|
category: "Maintainability"
|
|
39766
39197
|
}
|
|
39767
39198
|
},
|
|
39768
|
-
{
|
|
39769
|
-
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39770
|
-
id: "no-deprecated-tailwind-class",
|
|
39771
|
-
source: "react-doctor",
|
|
39772
|
-
originallyExternal: false,
|
|
39773
|
-
rule: {
|
|
39774
|
-
...noDeprecatedTailwindClass,
|
|
39775
|
-
framework: "global",
|
|
39776
|
-
category: "Maintainability"
|
|
39777
|
-
}
|
|
39778
|
-
},
|
|
39779
39199
|
{
|
|
39780
39200
|
key: "react-doctor/no-derived-state",
|
|
39781
39201
|
id: "no-derived-state",
|
|
@@ -40047,17 +39467,6 @@ const reactDoctorRules = [
|
|
|
40047
39467
|
category: "Performance"
|
|
40048
39468
|
}
|
|
40049
39469
|
},
|
|
40050
|
-
{
|
|
40051
|
-
key: "react-doctor/no-full-viewport-width",
|
|
40052
|
-
id: "no-full-viewport-width",
|
|
40053
|
-
source: "react-doctor",
|
|
40054
|
-
originallyExternal: false,
|
|
40055
|
-
rule: {
|
|
40056
|
-
...noFullViewportWidth,
|
|
40057
|
-
framework: "global",
|
|
40058
|
-
category: "Maintainability"
|
|
40059
|
-
}
|
|
40060
|
-
},
|
|
40061
39470
|
{
|
|
40062
39471
|
key: "react-doctor/no-generic-handler-names",
|
|
40063
39472
|
id: "no-generic-handler-names",
|
|
@@ -40297,17 +39706,6 @@ const reactDoctorRules = [
|
|
|
40297
39706
|
category: "Performance"
|
|
40298
39707
|
}
|
|
40299
39708
|
},
|
|
40300
|
-
{
|
|
40301
|
-
key: "react-doctor/no-low-contrast-inline-style",
|
|
40302
|
-
id: "no-low-contrast-inline-style",
|
|
40303
|
-
source: "react-doctor",
|
|
40304
|
-
originallyExternal: false,
|
|
40305
|
-
rule: {
|
|
40306
|
-
...noLowContrastInlineStyle,
|
|
40307
|
-
framework: "global",
|
|
40308
|
-
category: "Accessibility"
|
|
40309
|
-
}
|
|
40310
|
-
},
|
|
40311
39709
|
{
|
|
40312
39710
|
key: "react-doctor/no-many-boolean-props",
|
|
40313
39711
|
id: "no-many-boolean-props",
|
|
@@ -40585,17 +39983,6 @@ const reactDoctorRules = [
|
|
|
40585
39983
|
category: "Maintainability"
|
|
40586
39984
|
}
|
|
40587
39985
|
},
|
|
40588
|
-
{
|
|
40589
|
-
key: "react-doctor/no-redundant-display-class",
|
|
40590
|
-
id: "no-redundant-display-class",
|
|
40591
|
-
source: "react-doctor",
|
|
40592
|
-
originallyExternal: false,
|
|
40593
|
-
rule: {
|
|
40594
|
-
...noRedundantDisplayClass,
|
|
40595
|
-
framework: "global",
|
|
40596
|
-
category: "Maintainability"
|
|
40597
|
-
}
|
|
40598
|
-
},
|
|
40599
39986
|
{
|
|
40600
39987
|
key: "react-doctor/no-redundant-roles",
|
|
40601
39988
|
id: "no-redundant-roles",
|
|
@@ -40772,17 +40159,6 @@ const reactDoctorRules = [
|
|
|
40772
40159
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
40773
40160
|
}
|
|
40774
40161
|
},
|
|
40775
|
-
{
|
|
40776
|
-
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40777
|
-
id: "no-svg-currentcolor-with-fill-class",
|
|
40778
|
-
source: "react-doctor",
|
|
40779
|
-
originallyExternal: false,
|
|
40780
|
-
rule: {
|
|
40781
|
-
...noSvgCurrentcolorWithFillClass,
|
|
40782
|
-
framework: "global",
|
|
40783
|
-
category: "Maintainability"
|
|
40784
|
-
}
|
|
40785
|
-
},
|
|
40786
40162
|
{
|
|
40787
40163
|
key: "react-doctor/no-sync-xhr",
|
|
40788
40164
|
id: "no-sync-xhr",
|
|
@@ -40794,29 +40170,6 @@ const reactDoctorRules = [
|
|
|
40794
40170
|
category: "Performance"
|
|
40795
40171
|
}
|
|
40796
40172
|
},
|
|
40797
|
-
{
|
|
40798
|
-
key: "react-doctor/no-tailwind-layout-transition",
|
|
40799
|
-
id: "no-tailwind-layout-transition",
|
|
40800
|
-
source: "react-doctor",
|
|
40801
|
-
originallyExternal: false,
|
|
40802
|
-
rule: {
|
|
40803
|
-
...noTailwindLayoutTransition,
|
|
40804
|
-
framework: "global",
|
|
40805
|
-
category: "Performance"
|
|
40806
|
-
}
|
|
40807
|
-
},
|
|
40808
|
-
{
|
|
40809
|
-
key: "react-doctor/no-target-blank-without-rel",
|
|
40810
|
-
id: "no-target-blank-without-rel",
|
|
40811
|
-
source: "react-doctor",
|
|
40812
|
-
originallyExternal: false,
|
|
40813
|
-
rule: {
|
|
40814
|
-
...noTargetBlankWithoutRel,
|
|
40815
|
-
framework: "global",
|
|
40816
|
-
category: "Accessibility",
|
|
40817
|
-
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40818
|
-
}
|
|
40819
|
-
},
|
|
40820
40173
|
{
|
|
40821
40174
|
key: "react-doctor/no-this-in-sfc",
|
|
40822
40175
|
id: "no-this-in-sfc",
|
|
@@ -40886,18 +40239,6 @@ const reactDoctorRules = [
|
|
|
40886
40239
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
40887
40240
|
}
|
|
40888
40241
|
},
|
|
40889
|
-
{
|
|
40890
|
-
key: "react-doctor/no-uninformative-aria-label",
|
|
40891
|
-
id: "no-uninformative-aria-label",
|
|
40892
|
-
source: "react-doctor",
|
|
40893
|
-
originallyExternal: false,
|
|
40894
|
-
rule: {
|
|
40895
|
-
...noUninformativeAriaLabel,
|
|
40896
|
-
framework: "global",
|
|
40897
|
-
category: "Accessibility",
|
|
40898
|
-
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40899
|
-
}
|
|
40900
|
-
},
|
|
40901
40242
|
{
|
|
40902
40243
|
key: "react-doctor/no-unknown-property",
|
|
40903
40244
|
id: "no-unknown-property",
|
|
@@ -41107,17 +40448,6 @@ const reactDoctorRules = [
|
|
|
41107
40448
|
category: "Bugs"
|
|
41108
40449
|
}
|
|
41109
40450
|
},
|
|
41110
|
-
{
|
|
41111
|
-
key: "react-doctor/prefer-dvh-over-vh",
|
|
41112
|
-
id: "prefer-dvh-over-vh",
|
|
41113
|
-
source: "react-doctor",
|
|
41114
|
-
originallyExternal: false,
|
|
41115
|
-
rule: {
|
|
41116
|
-
...preferDvhOverVh,
|
|
41117
|
-
framework: "global",
|
|
41118
|
-
category: "Maintainability"
|
|
41119
|
-
}
|
|
41120
|
-
},
|
|
41121
40451
|
{
|
|
41122
40452
|
key: "react-doctor/prefer-dynamic-import",
|
|
41123
40453
|
id: "prefer-dynamic-import",
|
|
@@ -41222,17 +40552,6 @@ const reactDoctorRules = [
|
|
|
41222
40552
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
41223
40553
|
}
|
|
41224
40554
|
},
|
|
41225
|
-
{
|
|
41226
|
-
key: "react-doctor/prefer-truncate-shorthand",
|
|
41227
|
-
id: "prefer-truncate-shorthand",
|
|
41228
|
-
source: "react-doctor",
|
|
41229
|
-
originallyExternal: false,
|
|
41230
|
-
rule: {
|
|
41231
|
-
...preferTruncateShorthand,
|
|
41232
|
-
framework: "global",
|
|
41233
|
-
category: "Maintainability"
|
|
41234
|
-
}
|
|
41235
|
-
},
|
|
41236
40555
|
{
|
|
41237
40556
|
key: "react-doctor/prefer-use-effect-event",
|
|
41238
40557
|
id: "prefer-use-effect-event",
|
|
@@ -42959,32 +42278,6 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
42959
42278
|
}
|
|
42960
42279
|
return unconditional;
|
|
42961
42280
|
};
|
|
42962
|
-
const computeDominatesExit = (cfg) => {
|
|
42963
|
-
const reachableToExit = /* @__PURE__ */ new Set();
|
|
42964
|
-
const queue = [cfg.exit];
|
|
42965
|
-
while (queue.length > 0) {
|
|
42966
|
-
const block = queue.shift();
|
|
42967
|
-
if (reachableToExit.has(block)) continue;
|
|
42968
|
-
reachableToExit.add(block);
|
|
42969
|
-
for (const edge of block.predecessors) queue.push(edge.from);
|
|
42970
|
-
}
|
|
42971
|
-
const dominatesExit = /* @__PURE__ */ new Set();
|
|
42972
|
-
const visit = (block) => {
|
|
42973
|
-
if (block === cfg.exit) return true;
|
|
42974
|
-
if (dominatesExit.has(block)) return true;
|
|
42975
|
-
if (block.successors.length === 0) return false;
|
|
42976
|
-
dominatesExit.add(block);
|
|
42977
|
-
let allReach = true;
|
|
42978
|
-
for (const edge of block.successors) if (!visit(edge.to)) {
|
|
42979
|
-
allReach = false;
|
|
42980
|
-
break;
|
|
42981
|
-
}
|
|
42982
|
-
if (!allReach) dominatesExit.delete(block);
|
|
42983
|
-
return allReach;
|
|
42984
|
-
};
|
|
42985
|
-
for (const block of cfg.blocks) visit(block);
|
|
42986
|
-
return dominatesExit;
|
|
42987
|
-
};
|
|
42988
42281
|
const analyzeControlFlow = (program) => {
|
|
42989
42282
|
nextBlockId = 0;
|
|
42990
42283
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
@@ -42992,8 +42285,7 @@ const analyzeControlFlow = (program) => {
|
|
|
42992
42285
|
const cfg = buildFunctionCfg(functionNode, body);
|
|
42993
42286
|
functionCfgs.set(functionNode, {
|
|
42994
42287
|
cfg,
|
|
42995
|
-
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42996
|
-
dominatesExitSet: computeDominatesExit(cfg)
|
|
42288
|
+
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42997
42289
|
});
|
|
42998
42290
|
};
|
|
42999
42291
|
if (isNodeOfType(program, "Program")) buildFor(program, {
|
|
@@ -43036,20 +42328,10 @@ const analyzeControlFlow = (program) => {
|
|
|
43036
42328
|
if (!block) return true;
|
|
43037
42329
|
return entry.unconditionalSet.has(block);
|
|
43038
42330
|
};
|
|
43039
|
-
const dominatesExit = (node) => {
|
|
43040
|
-
const owner = enclosingFunction(node);
|
|
43041
|
-
if (!owner) return true;
|
|
43042
|
-
const entry = functionCfgs.get(owner);
|
|
43043
|
-
if (!entry) return true;
|
|
43044
|
-
const block = entry.cfg.blockOf(node);
|
|
43045
|
-
if (!block) return true;
|
|
43046
|
-
return entry.dominatesExitSet.has(block);
|
|
43047
|
-
};
|
|
43048
42331
|
return {
|
|
43049
42332
|
cfgFor,
|
|
43050
42333
|
enclosingFunction,
|
|
43051
|
-
isUnconditionalFromEntry
|
|
43052
|
-
dominatesExit
|
|
42334
|
+
isUnconditionalFromEntry
|
|
43053
42335
|
};
|
|
43054
42336
|
};
|
|
43055
42337
|
//#endregion
|
|
@@ -43074,8 +42356,7 @@ const buildFallbackScopes = () => ({
|
|
|
43074
42356
|
const FALLBACK_CFG = {
|
|
43075
42357
|
cfgFor: () => null,
|
|
43076
42358
|
enclosingFunction: () => null,
|
|
43077
|
-
isUnconditionalFromEntry: () => false
|
|
43078
|
-
dominatesExit: () => false
|
|
42359
|
+
isUnconditionalFromEntry: () => false
|
|
43079
42360
|
};
|
|
43080
42361
|
const wrapWithSemanticContext = (rule) => ({
|
|
43081
42362
|
...rule,
|