oxlint-plugin-react-doctor 0.5.6-dev.b08ca1c → 0.5.6-dev.eafac9d
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 +635 -5
- package/dist/index.js +960 -156
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1861,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1861
|
});
|
|
1862
1862
|
//#endregion
|
|
1863
1863
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1864
|
+
const MESSAGE$64 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1865
|
const anchorHasContent = defineRule({
|
|
1866
1866
|
id: "anchor-has-content",
|
|
1867
1867
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1877,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1877
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1878
|
context.report({
|
|
1879
1879
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1880
|
+
message: MESSAGE$64
|
|
1881
1881
|
});
|
|
1882
1882
|
} })
|
|
1883
1883
|
});
|
|
@@ -2271,7 +2271,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2271
|
};
|
|
2272
2272
|
//#endregion
|
|
2273
2273
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2274
|
+
const MESSAGE$63 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2275
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2276
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2277
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2289
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2290
|
context.report({
|
|
2291
2291
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2292
|
+
message: MESSAGE$63
|
|
2293
2293
|
});
|
|
2294
2294
|
return;
|
|
2295
2295
|
}
|
|
2296
2296
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2297
|
context.report({
|
|
2298
2298
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2299
|
+
message: MESSAGE$63
|
|
2300
2300
|
});
|
|
2301
2301
|
} })
|
|
2302
2302
|
});
|
|
@@ -4272,7 +4272,7 @@ const asyncParallel = defineRule({
|
|
|
4272
4272
|
});
|
|
4273
4273
|
//#endregion
|
|
4274
4274
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4275
|
-
const MESSAGE$
|
|
4275
|
+
const MESSAGE$62 = "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.";
|
|
4276
4276
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
4277
|
const STORAGE_GLOBALS = new Set([
|
|
4278
4278
|
"window",
|
|
@@ -4306,7 +4306,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4306
4306
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
4307
|
context.report({
|
|
4308
4308
|
node,
|
|
4309
|
-
message: MESSAGE$
|
|
4309
|
+
message: MESSAGE$62
|
|
4310
4310
|
});
|
|
4311
4311
|
},
|
|
4312
4312
|
AssignmentExpression(node) {
|
|
@@ -4317,7 +4317,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4317
4317
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
4318
|
context.report({
|
|
4319
4319
|
node: target,
|
|
4320
|
-
message: MESSAGE$
|
|
4320
|
+
message: MESSAGE$62
|
|
4321
4321
|
});
|
|
4322
4322
|
}
|
|
4323
4323
|
})
|
|
@@ -4694,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4694
4694
|
//#endregion
|
|
4695
4695
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
4696
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4697
|
-
const MESSAGE$
|
|
4697
|
+
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4698
|
const KEY_HANDLERS = [
|
|
4699
4699
|
"onKeyUp",
|
|
4700
4700
|
"onKeyDown",
|
|
@@ -4726,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4726
4726
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4727
|
context.report({
|
|
4728
4728
|
node: node.name,
|
|
4729
|
-
message: MESSAGE$
|
|
4729
|
+
message: MESSAGE$61
|
|
4730
4730
|
});
|
|
4731
4731
|
} };
|
|
4732
4732
|
}
|
|
@@ -4841,7 +4841,7 @@ const isReactComponentName = (name) => {
|
|
|
4841
4841
|
};
|
|
4842
4842
|
//#endregion
|
|
4843
4843
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4844
|
-
const MESSAGE$
|
|
4844
|
+
const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4845
4845
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4846
4846
|
const DEFAULT_LABELLING_PROPS = [
|
|
4847
4847
|
"alt",
|
|
@@ -5002,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5002
5002
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5003
5003
|
context.report({
|
|
5004
5004
|
node: opening,
|
|
5005
|
-
message: MESSAGE$
|
|
5005
|
+
message: MESSAGE$60
|
|
5006
5006
|
});
|
|
5007
5007
|
} };
|
|
5008
5008
|
}
|
|
@@ -5131,6 +5131,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
5131
5131
|
return findings;
|
|
5132
5132
|
}
|
|
5133
5133
|
});
|
|
5134
|
+
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5134
5135
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5135
5136
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5136
5137
|
"continue",
|
|
@@ -5429,10 +5430,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5429
5430
|
});
|
|
5430
5431
|
//#endregion
|
|
5431
5432
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5432
|
-
const hasJsxSpreadAttribute
|
|
5433
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5433
5434
|
//#endregion
|
|
5434
5435
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5435
|
-
const MESSAGE$
|
|
5436
|
+
const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5436
5437
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5437
5438
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5438
5439
|
"aria-label",
|
|
@@ -5451,11 +5452,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5451
5452
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5452
5453
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5453
5454
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5454
|
-
if (hasJsxSpreadAttribute
|
|
5455
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5455
5456
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5456
5457
|
context.report({
|
|
5457
5458
|
node: node.name,
|
|
5458
|
-
message: MESSAGE$
|
|
5459
|
+
message: MESSAGE$59
|
|
5459
5460
|
});
|
|
5460
5461
|
} })
|
|
5461
5462
|
});
|
|
@@ -5494,7 +5495,7 @@ const isEs6Component = (node) => {
|
|
|
5494
5495
|
};
|
|
5495
5496
|
//#endregion
|
|
5496
5497
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5497
|
-
const MESSAGE$
|
|
5498
|
+
const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5498
5499
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5499
5500
|
"observer",
|
|
5500
5501
|
"lazy",
|
|
@@ -5697,7 +5698,7 @@ const displayName = defineRule({
|
|
|
5697
5698
|
const reportAt = (node) => {
|
|
5698
5699
|
context.report({
|
|
5699
5700
|
node,
|
|
5700
|
-
message: MESSAGE$
|
|
5701
|
+
message: MESSAGE$58
|
|
5701
5702
|
});
|
|
5702
5703
|
};
|
|
5703
5704
|
return {
|
|
@@ -7845,7 +7846,7 @@ const forbidElements = defineRule({
|
|
|
7845
7846
|
});
|
|
7846
7847
|
//#endregion
|
|
7847
7848
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7848
|
-
const MESSAGE$
|
|
7849
|
+
const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7849
7850
|
const forwardRefUsesRef = defineRule({
|
|
7850
7851
|
id: "forward-ref-uses-ref",
|
|
7851
7852
|
title: "forwardRef without ref parameter",
|
|
@@ -7865,7 +7866,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7865
7866
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7866
7867
|
context.report({
|
|
7867
7868
|
node: inner,
|
|
7868
|
-
message: MESSAGE$
|
|
7869
|
+
message: MESSAGE$57
|
|
7869
7870
|
});
|
|
7870
7871
|
} })
|
|
7871
7872
|
});
|
|
@@ -7902,7 +7903,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7902
7903
|
});
|
|
7903
7904
|
//#endregion
|
|
7904
7905
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7905
|
-
const MESSAGE$
|
|
7906
|
+
const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7906
7907
|
const DEFAULT_HEADING_TAGS = [
|
|
7907
7908
|
"h1",
|
|
7908
7909
|
"h2",
|
|
@@ -7935,7 +7936,7 @@ const headingHasContent = defineRule({
|
|
|
7935
7936
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7936
7937
|
context.report({
|
|
7937
7938
|
node,
|
|
7938
|
-
message: MESSAGE$
|
|
7939
|
+
message: MESSAGE$56
|
|
7939
7940
|
});
|
|
7940
7941
|
} };
|
|
7941
7942
|
}
|
|
@@ -8073,7 +8074,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8073
8074
|
});
|
|
8074
8075
|
//#endregion
|
|
8075
8076
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8076
|
-
const MESSAGE$
|
|
8077
|
+
const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8077
8078
|
const resolveSettings$38 = (settings) => {
|
|
8078
8079
|
const reactDoctor = settings?.["react-doctor"];
|
|
8079
8080
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8121,7 +8122,7 @@ const htmlHasLang = defineRule({
|
|
|
8121
8122
|
if (!lang) {
|
|
8122
8123
|
context.report({
|
|
8123
8124
|
node: node.name,
|
|
8124
|
-
message: MESSAGE$
|
|
8125
|
+
message: MESSAGE$55
|
|
8125
8126
|
});
|
|
8126
8127
|
return;
|
|
8127
8128
|
}
|
|
@@ -8129,13 +8130,13 @@ const htmlHasLang = defineRule({
|
|
|
8129
8130
|
if (verdict === "missing" || verdict === "empty") {
|
|
8130
8131
|
context.report({
|
|
8131
8132
|
node: lang,
|
|
8132
|
-
message: MESSAGE$
|
|
8133
|
+
message: MESSAGE$55
|
|
8133
8134
|
});
|
|
8134
8135
|
return;
|
|
8135
8136
|
}
|
|
8136
8137
|
if (hasSpread && !lang) context.report({
|
|
8137
8138
|
node: node.name,
|
|
8138
|
-
message: MESSAGE$
|
|
8139
|
+
message: MESSAGE$55
|
|
8139
8140
|
});
|
|
8140
8141
|
} };
|
|
8141
8142
|
}
|
|
@@ -8349,7 +8350,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8349
8350
|
});
|
|
8350
8351
|
//#endregion
|
|
8351
8352
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8352
|
-
const MESSAGE$
|
|
8353
|
+
const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8353
8354
|
const evaluateTitleValue = (value) => {
|
|
8354
8355
|
if (!value) return "missing";
|
|
8355
8356
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8389,14 +8390,14 @@ const iframeHasTitle = defineRule({
|
|
|
8389
8390
|
if (!titleAttr) {
|
|
8390
8391
|
if (hasSpread || tag === "iframe") context.report({
|
|
8391
8392
|
node: node.name,
|
|
8392
|
-
message: MESSAGE$
|
|
8393
|
+
message: MESSAGE$54
|
|
8393
8394
|
});
|
|
8394
8395
|
return;
|
|
8395
8396
|
}
|
|
8396
8397
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8397
8398
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8398
8399
|
node: titleAttr,
|
|
8399
|
-
message: MESSAGE$
|
|
8400
|
+
message: MESSAGE$54
|
|
8400
8401
|
});
|
|
8401
8402
|
} })
|
|
8402
8403
|
});
|
|
@@ -8500,7 +8501,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8500
8501
|
});
|
|
8501
8502
|
//#endregion
|
|
8502
8503
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8503
|
-
const MESSAGE$
|
|
8504
|
+
const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8504
8505
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8505
8506
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8506
8507
|
"image",
|
|
@@ -8565,7 +8566,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8565
8566
|
if (!altAttribute) return;
|
|
8566
8567
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8567
8568
|
node: altAttribute,
|
|
8568
|
-
message: MESSAGE$
|
|
8569
|
+
message: MESSAGE$53
|
|
8569
8570
|
});
|
|
8570
8571
|
} };
|
|
8571
8572
|
}
|
|
@@ -10922,7 +10923,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10922
10923
|
});
|
|
10923
10924
|
//#endregion
|
|
10924
10925
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10925
|
-
const MESSAGE$
|
|
10926
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10926
10927
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10927
10928
|
"code",
|
|
10928
10929
|
"pre",
|
|
@@ -10958,7 +10959,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10958
10959
|
if (isInsideLiteralTextTag(node)) return;
|
|
10959
10960
|
context.report({
|
|
10960
10961
|
node,
|
|
10961
|
-
message: MESSAGE$
|
|
10962
|
+
message: MESSAGE$52
|
|
10962
10963
|
});
|
|
10963
10964
|
} })
|
|
10964
10965
|
});
|
|
@@ -10989,7 +10990,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10989
10990
|
};
|
|
10990
10991
|
//#endregion
|
|
10991
10992
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10992
|
-
const MESSAGE$
|
|
10993
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10993
10994
|
const CONTEXT_MODULES$1 = [
|
|
10994
10995
|
"react",
|
|
10995
10996
|
"use-context-selector",
|
|
@@ -11087,7 +11088,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11087
11088
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11088
11089
|
context.report({
|
|
11089
11090
|
node: attribute,
|
|
11090
|
-
message: MESSAGE$
|
|
11091
|
+
message: MESSAGE$51
|
|
11091
11092
|
});
|
|
11092
11093
|
}
|
|
11093
11094
|
}
|
|
@@ -11173,7 +11174,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11173
11174
|
};
|
|
11174
11175
|
//#endregion
|
|
11175
11176
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11176
|
-
const MESSAGE$
|
|
11177
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11177
11178
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11178
11179
|
"icon",
|
|
11179
11180
|
"Icon",
|
|
@@ -11442,7 +11443,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11442
11443
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11443
11444
|
context.report({
|
|
11444
11445
|
node,
|
|
11445
|
-
message: MESSAGE$
|
|
11446
|
+
message: MESSAGE$50
|
|
11446
11447
|
});
|
|
11447
11448
|
}
|
|
11448
11449
|
};
|
|
@@ -11730,7 +11731,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11730
11731
|
];
|
|
11731
11732
|
//#endregion
|
|
11732
11733
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11733
|
-
const MESSAGE$
|
|
11734
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11734
11735
|
const isDataArrayPropName = (propName) => {
|
|
11735
11736
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11736
11737
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11814,7 +11815,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11814
11815
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11815
11816
|
context.report({
|
|
11816
11817
|
node,
|
|
11817
|
-
message: MESSAGE$
|
|
11818
|
+
message: MESSAGE$49
|
|
11818
11819
|
});
|
|
11819
11820
|
}
|
|
11820
11821
|
};
|
|
@@ -12072,7 +12073,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12072
12073
|
]);
|
|
12073
12074
|
//#endregion
|
|
12074
12075
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12075
|
-
const MESSAGE$
|
|
12076
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12076
12077
|
const isAccessorPredicateName = (propName) => {
|
|
12077
12078
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12078
12079
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12278,7 +12279,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12278
12279
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12279
12280
|
context.report({
|
|
12280
12281
|
node,
|
|
12281
|
-
message: MESSAGE$
|
|
12282
|
+
message: MESSAGE$48
|
|
12282
12283
|
});
|
|
12283
12284
|
}
|
|
12284
12285
|
};
|
|
@@ -12498,7 +12499,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12498
12499
|
];
|
|
12499
12500
|
//#endregion
|
|
12500
12501
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12501
|
-
const MESSAGE$
|
|
12502
|
+
const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12502
12503
|
const isConfigObjectPropName = (propName) => {
|
|
12503
12504
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12504
12505
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12586,7 +12587,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12586
12587
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12587
12588
|
context.report({
|
|
12588
12589
|
node,
|
|
12589
|
-
message: MESSAGE$
|
|
12590
|
+
message: MESSAGE$47
|
|
12590
12591
|
});
|
|
12591
12592
|
}
|
|
12592
12593
|
};
|
|
@@ -12594,7 +12595,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12594
12595
|
});
|
|
12595
12596
|
//#endregion
|
|
12596
12597
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12597
|
-
const MESSAGE$
|
|
12598
|
+
const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12598
12599
|
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;
|
|
12599
12600
|
const resolveSettings$28 = (settings) => {
|
|
12600
12601
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12635,7 +12636,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12635
12636
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12636
12637
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12637
12638
|
node: attribute,
|
|
12638
|
-
message: MESSAGE$
|
|
12639
|
+
message: MESSAGE$46
|
|
12639
12640
|
});
|
|
12640
12641
|
}
|
|
12641
12642
|
} };
|
|
@@ -12950,7 +12951,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12950
12951
|
});
|
|
12951
12952
|
//#endregion
|
|
12952
12953
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12953
|
-
const MESSAGE$
|
|
12954
|
+
const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
|
|
12954
12955
|
const resolveSettings$25 = (settings) => {
|
|
12955
12956
|
const reactDoctor = settings?.["react-doctor"];
|
|
12956
12957
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12991,7 +12992,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12991
12992
|
}
|
|
12992
12993
|
context.report({
|
|
12993
12994
|
node: attribute,
|
|
12994
|
-
message: MESSAGE$
|
|
12995
|
+
message: MESSAGE$45
|
|
12995
12996
|
});
|
|
12996
12997
|
}
|
|
12997
12998
|
} };
|
|
@@ -13219,7 +13220,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13219
13220
|
});
|
|
13220
13221
|
//#endregion
|
|
13221
13222
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13222
|
-
const MESSAGE$
|
|
13223
|
+
const MESSAGE$44 = "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`.";
|
|
13223
13224
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13224
13225
|
"aa",
|
|
13225
13226
|
"ab",
|
|
@@ -13431,7 +13432,7 @@ const lang = defineRule({
|
|
|
13431
13432
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13432
13433
|
context.report({
|
|
13433
13434
|
node: langAttr,
|
|
13434
|
-
message: MESSAGE$
|
|
13435
|
+
message: MESSAGE$44
|
|
13435
13436
|
});
|
|
13436
13437
|
return;
|
|
13437
13438
|
}
|
|
@@ -13440,7 +13441,7 @@ const lang = defineRule({
|
|
|
13440
13441
|
if (value === null) return;
|
|
13441
13442
|
if (!isValidLangTag(value)) context.report({
|
|
13442
13443
|
node: langAttr,
|
|
13443
|
-
message: MESSAGE$
|
|
13444
|
+
message: MESSAGE$44
|
|
13444
13445
|
});
|
|
13445
13446
|
} })
|
|
13446
13447
|
});
|
|
@@ -13484,7 +13485,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13484
13485
|
});
|
|
13485
13486
|
//#endregion
|
|
13486
13487
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13487
|
-
const MESSAGE$
|
|
13488
|
+
const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13488
13489
|
const DEFAULT_AUDIO = ["audio"];
|
|
13489
13490
|
const DEFAULT_VIDEO = ["video"];
|
|
13490
13491
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13525,7 +13526,7 @@ const mediaHasCaption = defineRule({
|
|
|
13525
13526
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13526
13527
|
context.report({
|
|
13527
13528
|
node: node.name,
|
|
13528
|
-
message: MESSAGE$
|
|
13529
|
+
message: MESSAGE$43
|
|
13529
13530
|
});
|
|
13530
13531
|
return;
|
|
13531
13532
|
}
|
|
@@ -13542,7 +13543,7 @@ const mediaHasCaption = defineRule({
|
|
|
13542
13543
|
return kindValue.value.toLowerCase() === "captions";
|
|
13543
13544
|
})) context.report({
|
|
13544
13545
|
node: node.name,
|
|
13545
|
-
message: MESSAGE$
|
|
13546
|
+
message: MESSAGE$43
|
|
13546
13547
|
});
|
|
13547
13548
|
} };
|
|
13548
13549
|
}
|
|
@@ -15343,7 +15344,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15343
15344
|
});
|
|
15344
15345
|
//#endregion
|
|
15345
15346
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15346
|
-
const MESSAGE$
|
|
15347
|
+
const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15347
15348
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15348
15349
|
const noAccessKey = defineRule({
|
|
15349
15350
|
id: "no-access-key",
|
|
@@ -15360,7 +15361,7 @@ const noAccessKey = defineRule({
|
|
|
15360
15361
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15361
15362
|
context.report({
|
|
15362
15363
|
node: accessKey,
|
|
15363
|
-
message: MESSAGE$
|
|
15364
|
+
message: MESSAGE$42
|
|
15364
15365
|
});
|
|
15365
15366
|
return;
|
|
15366
15367
|
}
|
|
@@ -15370,7 +15371,7 @@ const noAccessKey = defineRule({
|
|
|
15370
15371
|
if (isUndefinedIdentifier(expression)) return;
|
|
15371
15372
|
context.report({
|
|
15372
15373
|
node: accessKey,
|
|
15373
|
-
message: MESSAGE$
|
|
15374
|
+
message: MESSAGE$42
|
|
15374
15375
|
});
|
|
15375
15376
|
}
|
|
15376
15377
|
} })
|
|
@@ -15852,8 +15853,41 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15852
15853
|
} })
|
|
15853
15854
|
});
|
|
15854
15855
|
//#endregion
|
|
15856
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15857
|
+
const getStringFromClassNameAttr = (node) => {
|
|
15858
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15859
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15860
|
+
if (!classAttr?.value) return null;
|
|
15861
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15862
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15863
|
+
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;
|
|
15864
|
+
return null;
|
|
15865
|
+
};
|
|
15866
|
+
//#endregion
|
|
15867
|
+
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15868
|
+
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15869
|
+
const noArbitraryPxFontSize = defineRule({
|
|
15870
|
+
id: "no-arbitrary-px-font-size",
|
|
15871
|
+
title: "Pixel arbitrary font size",
|
|
15872
|
+
tags: ["design", "test-noise"],
|
|
15873
|
+
severity: "warn",
|
|
15874
|
+
category: "Accessibility",
|
|
15875
|
+
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-*`.",
|
|
15876
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
15877
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
15878
|
+
if (!classNameValue) return;
|
|
15879
|
+
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15880
|
+
const rem = parseFloat(match[1]) / 16;
|
|
15881
|
+
context.report({
|
|
15882
|
+
node,
|
|
15883
|
+
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15884
|
+
});
|
|
15885
|
+
}
|
|
15886
|
+
} })
|
|
15887
|
+
});
|
|
15888
|
+
//#endregion
|
|
15855
15889
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15856
|
-
const MESSAGE$
|
|
15890
|
+
const MESSAGE$41 = "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.";
|
|
15857
15891
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15858
15892
|
id: "no-aria-hidden-on-focusable",
|
|
15859
15893
|
title: "aria-hidden on focusable element",
|
|
@@ -15880,7 +15914,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15880
15914
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15881
15915
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15882
15916
|
node: ariaHidden,
|
|
15883
|
-
message: MESSAGE$
|
|
15917
|
+
message: MESSAGE$41
|
|
15884
15918
|
});
|
|
15885
15919
|
} })
|
|
15886
15920
|
});
|
|
@@ -16248,7 +16282,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16248
16282
|
});
|
|
16249
16283
|
//#endregion
|
|
16250
16284
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16251
|
-
const MESSAGE$
|
|
16285
|
+
const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16252
16286
|
const SECOND_INDEX_METHODS = new Set([
|
|
16253
16287
|
"every",
|
|
16254
16288
|
"filter",
|
|
@@ -16452,7 +16486,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16452
16486
|
}
|
|
16453
16487
|
context.report({
|
|
16454
16488
|
node: keyAttribute,
|
|
16455
|
-
message: MESSAGE$
|
|
16489
|
+
message: MESSAGE$40
|
|
16456
16490
|
});
|
|
16457
16491
|
},
|
|
16458
16492
|
CallExpression(node) {
|
|
@@ -16472,7 +16506,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16472
16506
|
if (propName !== "key") continue;
|
|
16473
16507
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16474
16508
|
node: property,
|
|
16475
|
-
message: MESSAGE$
|
|
16509
|
+
message: MESSAGE$40
|
|
16476
16510
|
});
|
|
16477
16511
|
}
|
|
16478
16512
|
}
|
|
@@ -16480,7 +16514,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16480
16514
|
});
|
|
16481
16515
|
//#endregion
|
|
16482
16516
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16483
|
-
const MESSAGE$
|
|
16517
|
+
const MESSAGE$39 = "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.";
|
|
16484
16518
|
const noAsyncEffectCallback = defineRule({
|
|
16485
16519
|
id: "no-async-effect-callback",
|
|
16486
16520
|
title: "Async effect callback",
|
|
@@ -16494,13 +16528,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16494
16528
|
if (!callback.async) return;
|
|
16495
16529
|
context.report({
|
|
16496
16530
|
node: callback,
|
|
16497
|
-
message: MESSAGE$
|
|
16531
|
+
message: MESSAGE$39
|
|
16498
16532
|
});
|
|
16499
16533
|
} })
|
|
16500
16534
|
});
|
|
16501
16535
|
//#endregion
|
|
16502
16536
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16503
|
-
const MESSAGE$
|
|
16537
|
+
const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16504
16538
|
const resolveSettings$21 = (settings) => {
|
|
16505
16539
|
const reactDoctor = settings?.["react-doctor"];
|
|
16506
16540
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16556,12 +16590,45 @@ const noAutofocus = defineRule({
|
|
|
16556
16590
|
}
|
|
16557
16591
|
context.report({
|
|
16558
16592
|
node: autoFocusAttribute,
|
|
16559
|
-
message: MESSAGE$
|
|
16593
|
+
message: MESSAGE$38
|
|
16560
16594
|
});
|
|
16561
16595
|
} };
|
|
16562
16596
|
}
|
|
16563
16597
|
});
|
|
16564
16598
|
//#endregion
|
|
16599
|
+
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16600
|
+
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`.";
|
|
16601
|
+
const resolveStaticBoolean = (attribute) => {
|
|
16602
|
+
const value = attribute.value;
|
|
16603
|
+
if (!value) return true;
|
|
16604
|
+
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16605
|
+
if (isNodeOfType(literal, "Literal")) {
|
|
16606
|
+
if (literal.value === true || literal.value === "true") return true;
|
|
16607
|
+
if (literal.value === false || literal.value === "false") return false;
|
|
16608
|
+
}
|
|
16609
|
+
return null;
|
|
16610
|
+
};
|
|
16611
|
+
const noAutoplayWithoutMuted = defineRule({
|
|
16612
|
+
id: "no-autoplay-without-muted",
|
|
16613
|
+
title: "Autoplaying media without muted",
|
|
16614
|
+
severity: "warn",
|
|
16615
|
+
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16616
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
16617
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16618
|
+
const tagName = node.name.name;
|
|
16619
|
+
if (tagName !== "video" && tagName !== "audio") return;
|
|
16620
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16621
|
+
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16622
|
+
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16623
|
+
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16624
|
+
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16625
|
+
context.report({
|
|
16626
|
+
node: node.name,
|
|
16627
|
+
message: MESSAGE$37
|
|
16628
|
+
});
|
|
16629
|
+
} })
|
|
16630
|
+
});
|
|
16631
|
+
//#endregion
|
|
16565
16632
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16566
16633
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16567
16634
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -17060,7 +17127,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17060
17127
|
});
|
|
17061
17128
|
//#endregion
|
|
17062
17129
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17063
|
-
const MESSAGE$
|
|
17130
|
+
const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17064
17131
|
const noChildrenProp = defineRule({
|
|
17065
17132
|
id: "no-children-prop",
|
|
17066
17133
|
title: "Children passed as a prop",
|
|
@@ -17072,7 +17139,7 @@ const noChildrenProp = defineRule({
|
|
|
17072
17139
|
if (node.name.name !== "children") return;
|
|
17073
17140
|
context.report({
|
|
17074
17141
|
node: node.name,
|
|
17075
|
-
message: MESSAGE$
|
|
17142
|
+
message: MESSAGE$36
|
|
17076
17143
|
});
|
|
17077
17144
|
},
|
|
17078
17145
|
CallExpression(node) {
|
|
@@ -17085,7 +17152,7 @@ const noChildrenProp = defineRule({
|
|
|
17085
17152
|
const propertyKey = property.key;
|
|
17086
17153
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17087
17154
|
node: propertyKey,
|
|
17088
|
-
message: MESSAGE$
|
|
17155
|
+
message: MESSAGE$36
|
|
17089
17156
|
});
|
|
17090
17157
|
}
|
|
17091
17158
|
}
|
|
@@ -17093,7 +17160,7 @@ const noChildrenProp = defineRule({
|
|
|
17093
17160
|
});
|
|
17094
17161
|
//#endregion
|
|
17095
17162
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17096
|
-
const MESSAGE$
|
|
17163
|
+
const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17097
17164
|
const noCloneElement = defineRule({
|
|
17098
17165
|
id: "no-clone-element",
|
|
17099
17166
|
title: "cloneElement makes child props fragile",
|
|
@@ -17106,7 +17173,7 @@ const noCloneElement = defineRule({
|
|
|
17106
17173
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17107
17174
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17108
17175
|
node: callee,
|
|
17109
|
-
message: MESSAGE$
|
|
17176
|
+
message: MESSAGE$35
|
|
17110
17177
|
});
|
|
17111
17178
|
return;
|
|
17112
17179
|
}
|
|
@@ -17119,7 +17186,7 @@ const noCloneElement = defineRule({
|
|
|
17119
17186
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17120
17187
|
context.report({
|
|
17121
17188
|
node: callee,
|
|
17122
|
-
message: MESSAGE$
|
|
17189
|
+
message: MESSAGE$35
|
|
17123
17190
|
});
|
|
17124
17191
|
}
|
|
17125
17192
|
} })
|
|
@@ -17168,7 +17235,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17168
17235
|
};
|
|
17169
17236
|
//#endregion
|
|
17170
17237
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17171
|
-
const MESSAGE$
|
|
17238
|
+
const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17172
17239
|
const CONTEXT_MODULES = [
|
|
17173
17240
|
"react",
|
|
17174
17241
|
"use-context-selector",
|
|
@@ -17204,13 +17271,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17204
17271
|
if (!componentOrHookName) return;
|
|
17205
17272
|
context.report({
|
|
17206
17273
|
node,
|
|
17207
|
-
message: `${MESSAGE$
|
|
17274
|
+
message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
|
|
17208
17275
|
});
|
|
17209
17276
|
} })
|
|
17210
17277
|
});
|
|
17211
17278
|
//#endregion
|
|
17212
17279
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17213
|
-
const MESSAGE$
|
|
17280
|
+
const MESSAGE$33 = "`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.";
|
|
17214
17281
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17215
17282
|
id: "no-create-ref-in-function-component",
|
|
17216
17283
|
title: "createRef in function component",
|
|
@@ -17229,7 +17296,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17229
17296
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17230
17297
|
context.report({
|
|
17231
17298
|
node,
|
|
17232
|
-
message: MESSAGE$
|
|
17299
|
+
message: MESSAGE$33
|
|
17233
17300
|
});
|
|
17234
17301
|
} })
|
|
17235
17302
|
});
|
|
@@ -17369,7 +17436,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17369
17436
|
});
|
|
17370
17437
|
//#endregion
|
|
17371
17438
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17372
|
-
const MESSAGE$
|
|
17439
|
+
const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17373
17440
|
const noDanger = defineRule({
|
|
17374
17441
|
id: "no-danger",
|
|
17375
17442
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17382,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17382
17449
|
if (!propAttribute) return;
|
|
17383
17450
|
context.report({
|
|
17384
17451
|
node: propAttribute.name,
|
|
17385
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$32
|
|
17386
17453
|
});
|
|
17387
17454
|
},
|
|
17388
17455
|
CallExpression(node) {
|
|
@@ -17394,7 +17461,7 @@ const noDanger = defineRule({
|
|
|
17394
17461
|
const propertyKey = property.key;
|
|
17395
17462
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17396
17463
|
node: propertyKey,
|
|
17397
|
-
message: MESSAGE$
|
|
17464
|
+
message: MESSAGE$32
|
|
17398
17465
|
});
|
|
17399
17466
|
}
|
|
17400
17467
|
}
|
|
@@ -17402,7 +17469,7 @@ const noDanger = defineRule({
|
|
|
17402
17469
|
});
|
|
17403
17470
|
//#endregion
|
|
17404
17471
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17405
|
-
const MESSAGE$
|
|
17472
|
+
const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17406
17473
|
const isLineBreak = (child) => {
|
|
17407
17474
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17408
17475
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17472,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17472
17539
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17473
17540
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17474
17541
|
node: opening,
|
|
17475
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$31
|
|
17476
17543
|
});
|
|
17477
17544
|
},
|
|
17478
17545
|
CallExpression(node) {
|
|
@@ -17484,7 +17551,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17484
17551
|
if (!propsShape.hasDangerously) return;
|
|
17485
17552
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17486
17553
|
node,
|
|
17487
|
-
message: MESSAGE$
|
|
17554
|
+
message: MESSAGE$31
|
|
17488
17555
|
});
|
|
17489
17556
|
}
|
|
17490
17557
|
})
|
|
@@ -17649,6 +17716,37 @@ const noDefaultProps = defineRule({
|
|
|
17649
17716
|
} })
|
|
17650
17717
|
});
|
|
17651
17718
|
//#endregion
|
|
17719
|
+
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17720
|
+
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17721
|
+
//#endregion
|
|
17722
|
+
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17723
|
+
const renameDeprecatedToken = (token) => {
|
|
17724
|
+
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17725
|
+
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17726
|
+
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17727
|
+
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17728
|
+
return null;
|
|
17729
|
+
};
|
|
17730
|
+
const noDeprecatedTailwindClass = defineRule({
|
|
17731
|
+
id: "no-deprecated-tailwind-class",
|
|
17732
|
+
title: "Deprecated Tailwind v4 utility",
|
|
17733
|
+
tags: ["design", "test-noise"],
|
|
17734
|
+
severity: "warn",
|
|
17735
|
+
requires: ["tailwind:4"],
|
|
17736
|
+
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17737
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
17738
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
17739
|
+
if (!classNameValue) return;
|
|
17740
|
+
for (const token of getClassNameTokens(classNameValue)) {
|
|
17741
|
+
const replacement = renameDeprecatedToken(token);
|
|
17742
|
+
if (replacement) context.report({
|
|
17743
|
+
node,
|
|
17744
|
+
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17745
|
+
});
|
|
17746
|
+
}
|
|
17747
|
+
} })
|
|
17748
|
+
});
|
|
17749
|
+
//#endregion
|
|
17652
17750
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17653
17751
|
const isInitialOnlyPropName = (propName) => {
|
|
17654
17752
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -18061,7 +18159,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18061
18159
|
//#endregion
|
|
18062
18160
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18063
18161
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18064
|
-
const MESSAGE$
|
|
18162
|
+
const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18065
18163
|
const resolveSettings$20 = (settings) => {
|
|
18066
18164
|
const reactDoctor = settings?.["react-doctor"];
|
|
18067
18165
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18080,7 +18178,7 @@ const noDidMountSetState = defineRule({
|
|
|
18080
18178
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18081
18179
|
context.report({
|
|
18082
18180
|
node: node.callee,
|
|
18083
|
-
message: MESSAGE$
|
|
18181
|
+
message: MESSAGE$30
|
|
18084
18182
|
});
|
|
18085
18183
|
} };
|
|
18086
18184
|
}
|
|
@@ -18088,7 +18186,7 @@ const noDidMountSetState = defineRule({
|
|
|
18088
18186
|
//#endregion
|
|
18089
18187
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18090
18188
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18091
|
-
const MESSAGE$
|
|
18189
|
+
const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18092
18190
|
const resolveSettings$19 = (settings) => {
|
|
18093
18191
|
const reactDoctor = settings?.["react-doctor"];
|
|
18094
18192
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18107,7 +18205,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18107
18205
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18108
18206
|
context.report({
|
|
18109
18207
|
node: node.callee,
|
|
18110
|
-
message: MESSAGE$
|
|
18208
|
+
message: MESSAGE$29
|
|
18111
18209
|
});
|
|
18112
18210
|
} };
|
|
18113
18211
|
}
|
|
@@ -18130,7 +18228,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18130
18228
|
};
|
|
18131
18229
|
//#endregion
|
|
18132
18230
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18133
|
-
const MESSAGE$
|
|
18231
|
+
const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18134
18232
|
const shouldIgnoreMutation = (node) => {
|
|
18135
18233
|
let isConstructor = false;
|
|
18136
18234
|
let isInsideCallExpression = false;
|
|
@@ -18152,7 +18250,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18152
18250
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18153
18251
|
context.report({
|
|
18154
18252
|
node: reportNode,
|
|
18155
|
-
message: MESSAGE$
|
|
18253
|
+
message: MESSAGE$28
|
|
18156
18254
|
});
|
|
18157
18255
|
};
|
|
18158
18256
|
const noDirectMutationState = defineRule({
|
|
@@ -18362,6 +18460,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18362
18460
|
} })
|
|
18363
18461
|
});
|
|
18364
18462
|
//#endregion
|
|
18463
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18464
|
+
const MESSAGE$27 = "`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.";
|
|
18465
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18466
|
+
const noDocumentWrite = defineRule({
|
|
18467
|
+
id: "no-document-write",
|
|
18468
|
+
title: "document.write/writeln",
|
|
18469
|
+
severity: "warn",
|
|
18470
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18471
|
+
create: (context) => ({ CallExpression(node) {
|
|
18472
|
+
const callee = node.callee;
|
|
18473
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18474
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18475
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18476
|
+
context.report({
|
|
18477
|
+
node,
|
|
18478
|
+
message: MESSAGE$27
|
|
18479
|
+
});
|
|
18480
|
+
} })
|
|
18481
|
+
});
|
|
18482
|
+
//#endregion
|
|
18365
18483
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18366
18484
|
const noDynamicImportPath = defineRule({
|
|
18367
18485
|
id: "no-dynamic-import-path",
|
|
@@ -19740,7 +19858,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19740
19858
|
"ReactDOM",
|
|
19741
19859
|
"ReactDom"
|
|
19742
19860
|
]);
|
|
19743
|
-
const MESSAGE$
|
|
19861
|
+
const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19744
19862
|
const noFindDomNode = defineRule({
|
|
19745
19863
|
id: "no-find-dom-node",
|
|
19746
19864
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19751,7 +19869,7 @@ const noFindDomNode = defineRule({
|
|
|
19751
19869
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19752
19870
|
context.report({
|
|
19753
19871
|
node: callee,
|
|
19754
|
-
message: MESSAGE$
|
|
19872
|
+
message: MESSAGE$26
|
|
19755
19873
|
});
|
|
19756
19874
|
return;
|
|
19757
19875
|
}
|
|
@@ -19762,7 +19880,7 @@ const noFindDomNode = defineRule({
|
|
|
19762
19880
|
if (callee.property.name !== "findDOMNode") return;
|
|
19763
19881
|
context.report({
|
|
19764
19882
|
node: callee.property,
|
|
19765
|
-
message: MESSAGE$
|
|
19883
|
+
message: MESSAGE$26
|
|
19766
19884
|
});
|
|
19767
19885
|
}
|
|
19768
19886
|
} })
|
|
@@ -19803,6 +19921,41 @@ const noFullLodashImport = defineRule({
|
|
|
19803
19921
|
} })
|
|
19804
19922
|
});
|
|
19805
19923
|
//#endregion
|
|
19924
|
+
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19925
|
+
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19926
|
+
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19927
|
+
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.";
|
|
19928
|
+
const noFullViewportWidth = defineRule({
|
|
19929
|
+
id: "no-full-viewport-width",
|
|
19930
|
+
title: "Full viewport width causes overflow",
|
|
19931
|
+
tags: ["design", "test-noise"],
|
|
19932
|
+
severity: "warn",
|
|
19933
|
+
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19934
|
+
create: (context) => ({
|
|
19935
|
+
JSXAttribute(node) {
|
|
19936
|
+
const expression = getInlineStyleExpression(node);
|
|
19937
|
+
if (!expression) return;
|
|
19938
|
+
for (const property of expression.properties ?? []) {
|
|
19939
|
+
const key = getStylePropertyKey(property);
|
|
19940
|
+
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19941
|
+
const value = getStylePropertyStringValue(property);
|
|
19942
|
+
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19943
|
+
node: property,
|
|
19944
|
+
message: MESSAGE$25
|
|
19945
|
+
});
|
|
19946
|
+
}
|
|
19947
|
+
},
|
|
19948
|
+
JSXOpeningElement(node) {
|
|
19949
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
19950
|
+
if (!classNameValue) return;
|
|
19951
|
+
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
19952
|
+
node,
|
|
19953
|
+
message: MESSAGE$25
|
|
19954
|
+
});
|
|
19955
|
+
}
|
|
19956
|
+
})
|
|
19957
|
+
});
|
|
19958
|
+
//#endregion
|
|
19806
19959
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19807
19960
|
const noGenericHandlerNames = defineRule({
|
|
19808
19961
|
id: "no-generic-handler-names",
|
|
@@ -19865,7 +20018,7 @@ const noGiantComponent = defineRule({
|
|
|
19865
20018
|
});
|
|
19866
20019
|
//#endregion
|
|
19867
20020
|
//#region src/plugin/constants/style.ts
|
|
19868
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
20021
|
+
const LAYOUT_PROPERTIES$1 = new Set([
|
|
19869
20022
|
"width",
|
|
19870
20023
|
"height",
|
|
19871
20024
|
"top",
|
|
@@ -19935,17 +20088,6 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
19935
20088
|
} })
|
|
19936
20089
|
});
|
|
19937
20090
|
//#endregion
|
|
19938
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
19939
|
-
const getStringFromClassNameAttr = (node) => {
|
|
19940
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
19941
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
19942
|
-
if (!classAttr?.value) return null;
|
|
19943
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
19944
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
19945
|
-
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;
|
|
19946
|
-
return null;
|
|
19947
|
-
};
|
|
19948
|
-
//#endregion
|
|
19949
20091
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
19950
20092
|
const noGradientText = defineRule({
|
|
19951
20093
|
id: "no-gradient-text",
|
|
@@ -20004,7 +20146,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20004
20146
|
});
|
|
20005
20147
|
//#endregion
|
|
20006
20148
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20007
|
-
const MESSAGE$
|
|
20149
|
+
const MESSAGE$24 = "`<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.";
|
|
20008
20150
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20009
20151
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20010
20152
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20018,7 +20160,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20018
20160
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20019
20161
|
context.report({
|
|
20020
20162
|
node: node.name,
|
|
20021
|
-
message: MESSAGE$
|
|
20163
|
+
message: MESSAGE$24
|
|
20022
20164
|
});
|
|
20023
20165
|
} })
|
|
20024
20166
|
});
|
|
@@ -20253,7 +20395,7 @@ const noIsMounted = defineRule({
|
|
|
20253
20395
|
});
|
|
20254
20396
|
//#endregion
|
|
20255
20397
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20256
|
-
const MESSAGE$
|
|
20398
|
+
const MESSAGE$23 = "`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)`.";
|
|
20257
20399
|
const isJsonMethodCall = (node, method) => {
|
|
20258
20400
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20259
20401
|
const callee = node.callee;
|
|
@@ -20270,13 +20412,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20270
20412
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20271
20413
|
context.report({
|
|
20272
20414
|
node,
|
|
20273
|
-
message: MESSAGE$
|
|
20415
|
+
message: MESSAGE$23
|
|
20274
20416
|
});
|
|
20275
20417
|
} })
|
|
20276
20418
|
});
|
|
20277
20419
|
//#endregion
|
|
20278
20420
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20279
|
-
const MESSAGE$
|
|
20421
|
+
const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20280
20422
|
const isJsxElementTypeReference = (node) => {
|
|
20281
20423
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20282
20424
|
const typeName = node.typeName;
|
|
@@ -20293,7 +20435,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20293
20435
|
if (!typeAnnotation) return;
|
|
20294
20436
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20295
20437
|
node: typeAnnotation,
|
|
20296
|
-
message: MESSAGE$
|
|
20438
|
+
message: MESSAGE$22
|
|
20297
20439
|
});
|
|
20298
20440
|
};
|
|
20299
20441
|
const noJsxElementType = defineRule({
|
|
@@ -20403,7 +20545,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20403
20545
|
let propertyName = null;
|
|
20404
20546
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20405
20547
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20406
|
-
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20548
|
+
if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
|
|
20407
20549
|
node: property,
|
|
20408
20550
|
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`
|
|
20409
20551
|
});
|
|
@@ -20593,6 +20735,134 @@ const noLongTransitionDuration = defineRule({
|
|
|
20593
20735
|
} })
|
|
20594
20736
|
});
|
|
20595
20737
|
//#endregion
|
|
20738
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20739
|
+
const getStylePropertyNumberValue = (property) => {
|
|
20740
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
20741
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20742
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20743
|
+
return null;
|
|
20744
|
+
};
|
|
20745
|
+
//#endregion
|
|
20746
|
+
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20747
|
+
const linearizeChannel = (channel) => {
|
|
20748
|
+
const normalized = channel / 255;
|
|
20749
|
+
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20750
|
+
};
|
|
20751
|
+
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20752
|
+
const getWcagContrastRatio = (foreground, background) => {
|
|
20753
|
+
const foregroundLuminance = relativeLuminance(foreground);
|
|
20754
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
20755
|
+
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20756
|
+
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20757
|
+
return (lighter + .05) / (darker + .05);
|
|
20758
|
+
};
|
|
20759
|
+
//#endregion
|
|
20760
|
+
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20761
|
+
const UNRESOLVABLE = new Set([
|
|
20762
|
+
"transparent",
|
|
20763
|
+
"currentcolor",
|
|
20764
|
+
"inherit",
|
|
20765
|
+
"initial",
|
|
20766
|
+
"unset",
|
|
20767
|
+
"revert",
|
|
20768
|
+
"none"
|
|
20769
|
+
]);
|
|
20770
|
+
const resolveOpaqueColor = (raw) => {
|
|
20771
|
+
const value = raw.trim().toLowerCase();
|
|
20772
|
+
if (UNRESOLVABLE.has(value)) return null;
|
|
20773
|
+
if (value === "white") return {
|
|
20774
|
+
red: 255,
|
|
20775
|
+
green: 255,
|
|
20776
|
+
blue: 255
|
|
20777
|
+
};
|
|
20778
|
+
if (value === "black") return {
|
|
20779
|
+
red: 0,
|
|
20780
|
+
green: 0,
|
|
20781
|
+
blue: 0
|
|
20782
|
+
};
|
|
20783
|
+
if (value.startsWith("var(")) return null;
|
|
20784
|
+
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20785
|
+
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20786
|
+
if (value.startsWith("rgb")) {
|
|
20787
|
+
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20788
|
+
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20789
|
+
}
|
|
20790
|
+
return parseColorToRgb(value);
|
|
20791
|
+
};
|
|
20792
|
+
const toPx = (property) => {
|
|
20793
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20794
|
+
if (numberValue !== null) return numberValue;
|
|
20795
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20796
|
+
if (stringValue === null) return null;
|
|
20797
|
+
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20798
|
+
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20799
|
+
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20800
|
+
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20801
|
+
return null;
|
|
20802
|
+
};
|
|
20803
|
+
const isBoldWeight = (property) => {
|
|
20804
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20805
|
+
if (numberValue !== null) return numberValue >= 700;
|
|
20806
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20807
|
+
if (stringValue === null) return false;
|
|
20808
|
+
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20809
|
+
const numericWeight = Number(stringValue);
|
|
20810
|
+
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20811
|
+
};
|
|
20812
|
+
const noLowContrastInlineStyle = defineRule({
|
|
20813
|
+
id: "no-low-contrast-inline-style",
|
|
20814
|
+
title: "Low-contrast text in inline style",
|
|
20815
|
+
tags: ["test-noise"],
|
|
20816
|
+
severity: "warn",
|
|
20817
|
+
category: "Accessibility",
|
|
20818
|
+
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.",
|
|
20819
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
20820
|
+
const expression = getInlineStyleExpression(node);
|
|
20821
|
+
if (!expression) return;
|
|
20822
|
+
const properties = expression.properties ?? [];
|
|
20823
|
+
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20824
|
+
let foreground = null;
|
|
20825
|
+
let backgroundColorRaw = null;
|
|
20826
|
+
let backgroundShorthandRaw = null;
|
|
20827
|
+
let backgroundIsUnknown = false;
|
|
20828
|
+
let fontSizePx = null;
|
|
20829
|
+
let isBold = false;
|
|
20830
|
+
for (const property of properties) {
|
|
20831
|
+
const key = getStylePropertyKey(property);
|
|
20832
|
+
if (!key) continue;
|
|
20833
|
+
if (key === "backgroundImage") {
|
|
20834
|
+
backgroundIsUnknown = true;
|
|
20835
|
+
continue;
|
|
20836
|
+
}
|
|
20837
|
+
if (key === "fontSize" && property.type === "Property") {
|
|
20838
|
+
fontSizePx = toPx(property);
|
|
20839
|
+
continue;
|
|
20840
|
+
}
|
|
20841
|
+
if (key === "fontWeight" && property.type === "Property") {
|
|
20842
|
+
isBold = isBoldWeight(property);
|
|
20843
|
+
continue;
|
|
20844
|
+
}
|
|
20845
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20846
|
+
if (key === "color") {
|
|
20847
|
+
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20848
|
+
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20849
|
+
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20850
|
+
else backgroundShorthandRaw = stringValue;
|
|
20851
|
+
}
|
|
20852
|
+
if (backgroundIsUnknown) return;
|
|
20853
|
+
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20854
|
+
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20855
|
+
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20856
|
+
if (!foreground || !background) return;
|
|
20857
|
+
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20858
|
+
const ratio = getWcagContrastRatio(foreground, background);
|
|
20859
|
+
if (ratio < threshold) context.report({
|
|
20860
|
+
node,
|
|
20861
|
+
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.`
|
|
20862
|
+
});
|
|
20863
|
+
} })
|
|
20864
|
+
});
|
|
20865
|
+
//#endregion
|
|
20596
20866
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20597
20867
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20598
20868
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -20748,7 +21018,7 @@ const noMoment = defineRule({
|
|
|
20748
21018
|
});
|
|
20749
21019
|
//#endregion
|
|
20750
21020
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20751
|
-
const MESSAGE$
|
|
21021
|
+
const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20752
21022
|
const resolveSettings$16 = (settings) => {
|
|
20753
21023
|
const reactDoctor = settings?.["react-doctor"];
|
|
20754
21024
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21070,7 +21340,7 @@ const noMultiComp = defineRule({
|
|
|
21070
21340
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21071
21341
|
for (const component of flagged.slice(1)) context.report({
|
|
21072
21342
|
node: component.reportNode,
|
|
21073
|
-
message: MESSAGE$
|
|
21343
|
+
message: MESSAGE$21
|
|
21074
21344
|
});
|
|
21075
21345
|
} };
|
|
21076
21346
|
}
|
|
@@ -21238,7 +21508,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21238
21508
|
};
|
|
21239
21509
|
//#endregion
|
|
21240
21510
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21241
|
-
const MESSAGE$
|
|
21511
|
+
const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21242
21512
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21243
21513
|
"copyWithin",
|
|
21244
21514
|
"fill",
|
|
@@ -21448,7 +21718,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21448
21718
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21449
21719
|
context.report({
|
|
21450
21720
|
node: options.crossFileConsumerCallSite,
|
|
21451
|
-
message: `${MESSAGE$
|
|
21721
|
+
message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21452
21722
|
});
|
|
21453
21723
|
return;
|
|
21454
21724
|
}
|
|
@@ -21457,7 +21727,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21457
21727
|
reportedNodes.add(mutation.node);
|
|
21458
21728
|
context.report({
|
|
21459
21729
|
node: mutation.node,
|
|
21460
|
-
message: MESSAGE$
|
|
21730
|
+
message: MESSAGE$20
|
|
21461
21731
|
});
|
|
21462
21732
|
}
|
|
21463
21733
|
};
|
|
@@ -21729,7 +21999,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21729
21999
|
});
|
|
21730
22000
|
//#endregion
|
|
21731
22001
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21732
|
-
const MESSAGE$
|
|
22002
|
+
const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21733
22003
|
const resolveSettings$14 = (settings) => {
|
|
21734
22004
|
const reactDoctor = settings?.["react-doctor"];
|
|
21735
22005
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21757,7 +22027,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21757
22027
|
if (numeric === null) {
|
|
21758
22028
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21759
22029
|
node: tabIndex,
|
|
21760
|
-
message: MESSAGE$
|
|
22030
|
+
message: MESSAGE$19
|
|
21761
22031
|
});
|
|
21762
22032
|
return;
|
|
21763
22033
|
}
|
|
@@ -21770,7 +22040,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21770
22040
|
if (!roleAttribute) {
|
|
21771
22041
|
context.report({
|
|
21772
22042
|
node: tabIndex,
|
|
21773
|
-
message: MESSAGE$
|
|
22043
|
+
message: MESSAGE$19
|
|
21774
22044
|
});
|
|
21775
22045
|
return;
|
|
21776
22046
|
}
|
|
@@ -21784,20 +22054,12 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21784
22054
|
}
|
|
21785
22055
|
context.report({
|
|
21786
22056
|
node: tabIndex,
|
|
21787
|
-
message: MESSAGE$
|
|
22057
|
+
message: MESSAGE$19
|
|
21788
22058
|
});
|
|
21789
22059
|
} };
|
|
21790
22060
|
}
|
|
21791
22061
|
});
|
|
21792
22062
|
//#endregion
|
|
21793
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21794
|
-
const getStylePropertyNumberValue = (property) => {
|
|
21795
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
21796
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21797
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21798
|
-
return null;
|
|
21799
|
-
};
|
|
21800
|
-
//#endregion
|
|
21801
22063
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
21802
22064
|
const noOutlineNone = defineRule({
|
|
21803
22065
|
id: "no-outline-none",
|
|
@@ -22475,7 +22737,7 @@ const noRandomKey = defineRule({
|
|
|
22475
22737
|
});
|
|
22476
22738
|
//#endregion
|
|
22477
22739
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22478
|
-
const MESSAGE$
|
|
22740
|
+
const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22479
22741
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22480
22742
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22481
22743
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22501,13 +22763,13 @@ const noReactChildren = defineRule({
|
|
|
22501
22763
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22502
22764
|
context.report({
|
|
22503
22765
|
node: calleeOuter,
|
|
22504
|
-
message: MESSAGE$
|
|
22766
|
+
message: MESSAGE$18
|
|
22505
22767
|
});
|
|
22506
22768
|
return;
|
|
22507
22769
|
}
|
|
22508
22770
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22509
22771
|
node: calleeOuter,
|
|
22510
|
-
message: MESSAGE$
|
|
22772
|
+
message: MESSAGE$18
|
|
22511
22773
|
});
|
|
22512
22774
|
} })
|
|
22513
22775
|
});
|
|
@@ -22618,6 +22880,86 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22618
22880
|
})
|
|
22619
22881
|
});
|
|
22620
22882
|
//#endregion
|
|
22883
|
+
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22884
|
+
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22885
|
+
"div",
|
|
22886
|
+
"p",
|
|
22887
|
+
"section",
|
|
22888
|
+
"article",
|
|
22889
|
+
"main",
|
|
22890
|
+
"header",
|
|
22891
|
+
"footer",
|
|
22892
|
+
"nav",
|
|
22893
|
+
"aside",
|
|
22894
|
+
"figure",
|
|
22895
|
+
"figcaption",
|
|
22896
|
+
"blockquote",
|
|
22897
|
+
"form",
|
|
22898
|
+
"fieldset",
|
|
22899
|
+
"address",
|
|
22900
|
+
"pre",
|
|
22901
|
+
"ul",
|
|
22902
|
+
"ol",
|
|
22903
|
+
"dl",
|
|
22904
|
+
"dt",
|
|
22905
|
+
"dd",
|
|
22906
|
+
"h1",
|
|
22907
|
+
"h2",
|
|
22908
|
+
"h3",
|
|
22909
|
+
"h4",
|
|
22910
|
+
"h5",
|
|
22911
|
+
"h6"
|
|
22912
|
+
]);
|
|
22913
|
+
const INLINE_DEFAULT_TAGS = new Set([
|
|
22914
|
+
"span",
|
|
22915
|
+
"a",
|
|
22916
|
+
"b",
|
|
22917
|
+
"i",
|
|
22918
|
+
"em",
|
|
22919
|
+
"strong",
|
|
22920
|
+
"small",
|
|
22921
|
+
"code",
|
|
22922
|
+
"abbr",
|
|
22923
|
+
"cite",
|
|
22924
|
+
"label",
|
|
22925
|
+
"mark",
|
|
22926
|
+
"q",
|
|
22927
|
+
"s",
|
|
22928
|
+
"u",
|
|
22929
|
+
"sub",
|
|
22930
|
+
"sup",
|
|
22931
|
+
"kbd",
|
|
22932
|
+
"samp",
|
|
22933
|
+
"var",
|
|
22934
|
+
"time"
|
|
22935
|
+
]);
|
|
22936
|
+
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22937
|
+
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22938
|
+
const noRedundantDisplayClass = defineRule({
|
|
22939
|
+
id: "no-redundant-display-class",
|
|
22940
|
+
title: "Redundant display utility",
|
|
22941
|
+
tags: ["design", "test-noise"],
|
|
22942
|
+
severity: "warn",
|
|
22943
|
+
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`.",
|
|
22944
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
22945
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
22946
|
+
const tagName = node.name.name;
|
|
22947
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
22948
|
+
if (!classNameValue) return;
|
|
22949
|
+
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
22950
|
+
context.report({
|
|
22951
|
+
node,
|
|
22952
|
+
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22953
|
+
});
|
|
22954
|
+
return;
|
|
22955
|
+
}
|
|
22956
|
+
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
22957
|
+
node,
|
|
22958
|
+
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22959
|
+
});
|
|
22960
|
+
} })
|
|
22961
|
+
});
|
|
22962
|
+
//#endregion
|
|
22621
22963
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22622
22964
|
const ELEMENT_ROLE_PAIRS = [
|
|
22623
22965
|
["a", "link"],
|
|
@@ -22830,7 +23172,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22830
23172
|
});
|
|
22831
23173
|
//#endregion
|
|
22832
23174
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22833
|
-
const MESSAGE$
|
|
23175
|
+
const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22834
23176
|
const isReactDomRenderCall = (node) => {
|
|
22835
23177
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22836
23178
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22854,7 +23196,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22854
23196
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22855
23197
|
context.report({
|
|
22856
23198
|
node: node.callee,
|
|
22857
|
-
message: MESSAGE$
|
|
23199
|
+
message: MESSAGE$17
|
|
22858
23200
|
});
|
|
22859
23201
|
} })
|
|
22860
23202
|
});
|
|
@@ -23552,7 +23894,7 @@ const getParentComponent = (node) => {
|
|
|
23552
23894
|
};
|
|
23553
23895
|
//#endregion
|
|
23554
23896
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23555
|
-
const MESSAGE$
|
|
23897
|
+
const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23556
23898
|
const noSetState = defineRule({
|
|
23557
23899
|
id: "no-set-state",
|
|
23558
23900
|
title: "Local class state forbidden",
|
|
@@ -23567,7 +23909,7 @@ const noSetState = defineRule({
|
|
|
23567
23909
|
if (!getParentComponent(node)) return;
|
|
23568
23910
|
context.report({
|
|
23569
23911
|
node: node.callee,
|
|
23570
|
-
message: MESSAGE$
|
|
23912
|
+
message: MESSAGE$16
|
|
23571
23913
|
});
|
|
23572
23914
|
} })
|
|
23573
23915
|
});
|
|
@@ -23729,7 +24071,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23729
24071
|
};
|
|
23730
24072
|
//#endregion
|
|
23731
24073
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23732
|
-
const MESSAGE$
|
|
24074
|
+
const MESSAGE$15 = "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.";
|
|
23733
24075
|
const DEFAULT_HANDLERS = [
|
|
23734
24076
|
"onClick",
|
|
23735
24077
|
"onMouseDown",
|
|
@@ -23789,7 +24131,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23789
24131
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23790
24132
|
context.report({
|
|
23791
24133
|
node: node.name,
|
|
23792
|
-
message: MESSAGE$
|
|
24134
|
+
message: MESSAGE$15
|
|
23793
24135
|
});
|
|
23794
24136
|
return;
|
|
23795
24137
|
}
|
|
@@ -23799,19 +24141,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23799
24141
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23800
24142
|
context.report({
|
|
23801
24143
|
node: node.name,
|
|
23802
|
-
message: MESSAGE$
|
|
24144
|
+
message: MESSAGE$15
|
|
23803
24145
|
});
|
|
23804
24146
|
return;
|
|
23805
24147
|
}
|
|
23806
24148
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23807
24149
|
context.report({
|
|
23808
24150
|
node: node.name,
|
|
23809
|
-
message: MESSAGE$
|
|
24151
|
+
message: MESSAGE$15
|
|
23810
24152
|
});
|
|
23811
24153
|
} };
|
|
23812
24154
|
}
|
|
23813
24155
|
});
|
|
23814
24156
|
//#endregion
|
|
24157
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
24158
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
24159
|
+
"disabled",
|
|
24160
|
+
"checked",
|
|
24161
|
+
"readonly",
|
|
24162
|
+
"required",
|
|
24163
|
+
"selected",
|
|
24164
|
+
"multiple",
|
|
24165
|
+
"autofocus",
|
|
24166
|
+
"autoplay",
|
|
24167
|
+
"controls",
|
|
24168
|
+
"loop",
|
|
24169
|
+
"muted",
|
|
24170
|
+
"open",
|
|
24171
|
+
"reversed",
|
|
24172
|
+
"default",
|
|
24173
|
+
"novalidate",
|
|
24174
|
+
"formnovalidate",
|
|
24175
|
+
"playsinline",
|
|
24176
|
+
"itemscope",
|
|
24177
|
+
"allowfullscreen"
|
|
24178
|
+
]);
|
|
24179
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
24180
|
+
id: "no-string-false-on-boolean-attribute",
|
|
24181
|
+
title: "String true/false on a boolean attribute",
|
|
24182
|
+
severity: "warn",
|
|
24183
|
+
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
24184
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24185
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24186
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
24187
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
24188
|
+
for (const attribute of node.attributes) {
|
|
24189
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24190
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
24191
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
24192
|
+
const value = getJsxPropStringValue(attribute);
|
|
24193
|
+
if (value !== "false" && value !== "true") continue;
|
|
24194
|
+
const attributeName = attribute.name.name;
|
|
24195
|
+
const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
|
|
24196
|
+
context.report({
|
|
24197
|
+
node: attribute,
|
|
24198
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
24199
|
+
});
|
|
24200
|
+
}
|
|
24201
|
+
} })
|
|
24202
|
+
});
|
|
24203
|
+
//#endregion
|
|
23815
24204
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23816
24205
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23817
24206
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23862,8 +24251,154 @@ const noStringRefs = defineRule({
|
|
|
23862
24251
|
}
|
|
23863
24252
|
});
|
|
23864
24253
|
//#endregion
|
|
24254
|
+
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24255
|
+
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24256
|
+
if (token.includes(":")) return false;
|
|
24257
|
+
if (!token.startsWith(prefix)) return false;
|
|
24258
|
+
const value = token.slice(prefix.length);
|
|
24259
|
+
if (value === "" || value === "current") return false;
|
|
24260
|
+
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24261
|
+
return true;
|
|
24262
|
+
});
|
|
24263
|
+
const isCurrentColor = (attribute) => {
|
|
24264
|
+
const value = getJsxPropStringValue(attribute);
|
|
24265
|
+
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24266
|
+
};
|
|
24267
|
+
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24268
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
24269
|
+
title: "currentColor fights a fill/stroke class",
|
|
24270
|
+
tags: ["design", "test-noise"],
|
|
24271
|
+
severity: "warn",
|
|
24272
|
+
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.",
|
|
24273
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24274
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24275
|
+
if (!classNameValue) return;
|
|
24276
|
+
for (const paint of ["fill", "stroke"]) {
|
|
24277
|
+
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24278
|
+
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24279
|
+
context.report({
|
|
24280
|
+
node: attribute,
|
|
24281
|
+
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.`
|
|
24282
|
+
});
|
|
24283
|
+
return;
|
|
24284
|
+
}
|
|
24285
|
+
}
|
|
24286
|
+
} })
|
|
24287
|
+
});
|
|
24288
|
+
//#endregion
|
|
24289
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24290
|
+
const MESSAGE$14 = "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)`).";
|
|
24291
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24292
|
+
const noSyncXhr = defineRule({
|
|
24293
|
+
id: "no-sync-xhr",
|
|
24294
|
+
title: "Synchronous XMLHttpRequest",
|
|
24295
|
+
severity: "warn",
|
|
24296
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24297
|
+
create: (context) => ({ CallExpression(node) {
|
|
24298
|
+
const callee = node.callee;
|
|
24299
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24300
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24301
|
+
const asyncArgument = node.arguments?.[2];
|
|
24302
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24303
|
+
context.report({
|
|
24304
|
+
node,
|
|
24305
|
+
message: MESSAGE$14
|
|
24306
|
+
});
|
|
24307
|
+
} })
|
|
24308
|
+
});
|
|
24309
|
+
//#endregion
|
|
24310
|
+
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24311
|
+
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24312
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
24313
|
+
"width",
|
|
24314
|
+
"height",
|
|
24315
|
+
"min-width",
|
|
24316
|
+
"max-width",
|
|
24317
|
+
"min-height",
|
|
24318
|
+
"max-height",
|
|
24319
|
+
"top",
|
|
24320
|
+
"left",
|
|
24321
|
+
"right",
|
|
24322
|
+
"bottom",
|
|
24323
|
+
"inset",
|
|
24324
|
+
"inset-block",
|
|
24325
|
+
"inset-inline",
|
|
24326
|
+
"margin",
|
|
24327
|
+
"margin-top",
|
|
24328
|
+
"margin-right",
|
|
24329
|
+
"margin-bottom",
|
|
24330
|
+
"margin-left",
|
|
24331
|
+
"margin-block",
|
|
24332
|
+
"margin-inline",
|
|
24333
|
+
"padding",
|
|
24334
|
+
"padding-top",
|
|
24335
|
+
"padding-right",
|
|
24336
|
+
"padding-bottom",
|
|
24337
|
+
"padding-left",
|
|
24338
|
+
"padding-block",
|
|
24339
|
+
"padding-inline"
|
|
24340
|
+
]);
|
|
24341
|
+
const noTailwindLayoutTransition = defineRule({
|
|
24342
|
+
id: "no-tailwind-layout-transition",
|
|
24343
|
+
title: "Animating a layout property",
|
|
24344
|
+
tags: ["design", "test-noise"],
|
|
24345
|
+
severity: "warn",
|
|
24346
|
+
category: "Performance",
|
|
24347
|
+
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`.",
|
|
24348
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24349
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24350
|
+
if (!classNameValue) return;
|
|
24351
|
+
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24352
|
+
const animatedProperties = transitionMatch[1];
|
|
24353
|
+
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24354
|
+
if (layoutProperty) context.report({
|
|
24355
|
+
node,
|
|
24356
|
+
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24357
|
+
});
|
|
24358
|
+
}
|
|
24359
|
+
} })
|
|
24360
|
+
});
|
|
24361
|
+
//#endregion
|
|
24362
|
+
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24363
|
+
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\"`.";
|
|
24364
|
+
const targetIsBlank = (attribute) => {
|
|
24365
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
24366
|
+
if (stringValue !== null) return stringValue === "_blank";
|
|
24367
|
+
const value = attribute.value;
|
|
24368
|
+
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24369
|
+
const expression = value.expression;
|
|
24370
|
+
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24371
|
+
}
|
|
24372
|
+
return false;
|
|
24373
|
+
};
|
|
24374
|
+
const noTargetBlankWithoutRel = defineRule({
|
|
24375
|
+
id: "no-target-blank-without-rel",
|
|
24376
|
+
title: "target=_blank without rel=noopener",
|
|
24377
|
+
severity: "warn",
|
|
24378
|
+
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24379
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24380
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24381
|
+
const tagName = node.name.name;
|
|
24382
|
+
if (tagName !== "a" && tagName !== "area") return;
|
|
24383
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24384
|
+
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24385
|
+
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24386
|
+
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24387
|
+
if (relAttribute) {
|
|
24388
|
+
const relValue = getJsxPropStringValue(relAttribute);
|
|
24389
|
+
if (relValue === null) return;
|
|
24390
|
+
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24391
|
+
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24392
|
+
}
|
|
24393
|
+
context.report({
|
|
24394
|
+
node: node.name,
|
|
24395
|
+
message: MESSAGE$13
|
|
24396
|
+
});
|
|
24397
|
+
} })
|
|
24398
|
+
});
|
|
24399
|
+
//#endregion
|
|
23865
24400
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23866
|
-
const MESSAGE$
|
|
24401
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
23867
24402
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
23868
24403
|
let ancestor = node.parent;
|
|
23869
24404
|
while (ancestor) {
|
|
@@ -23932,7 +24467,7 @@ const noThisInSfc = defineRule({
|
|
|
23932
24467
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
23933
24468
|
context.report({
|
|
23934
24469
|
node,
|
|
23935
|
-
message: MESSAGE$
|
|
24470
|
+
message: MESSAGE$12
|
|
23936
24471
|
});
|
|
23937
24472
|
} };
|
|
23938
24473
|
}
|
|
@@ -23970,26 +24505,39 @@ const noTinyText = defineRule({
|
|
|
23970
24505
|
});
|
|
23971
24506
|
//#endregion
|
|
23972
24507
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24508
|
+
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24509
|
+
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`.";
|
|
23973
24510
|
const noTransitionAll = defineRule({
|
|
23974
24511
|
id: "no-transition-all",
|
|
23975
24512
|
title: "transition: all animates everything",
|
|
23976
24513
|
tags: ["test-noise"],
|
|
23977
24514
|
severity: "warn",
|
|
23978
24515
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
23979
|
-
create: (context) => ({
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
|
|
23986
|
-
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
24516
|
+
create: (context) => ({
|
|
24517
|
+
JSXAttribute(node) {
|
|
24518
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24519
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24520
|
+
const expression = node.value.expression;
|
|
24521
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24522
|
+
for (const property of expression.properties ?? []) {
|
|
24523
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24524
|
+
const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
24525
|
+
if (key !== "transition" && key !== "transitionProperty") continue;
|
|
24526
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
|
|
24527
|
+
node: property,
|
|
24528
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24529
|
+
});
|
|
24530
|
+
}
|
|
24531
|
+
},
|
|
24532
|
+
JSXOpeningElement(node) {
|
|
24533
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24534
|
+
if (!classNameValue) return;
|
|
24535
|
+
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24536
|
+
node,
|
|
24537
|
+
message: TAILWIND_MESSAGE
|
|
23990
24538
|
});
|
|
23991
24539
|
}
|
|
23992
|
-
}
|
|
24540
|
+
})
|
|
23993
24541
|
});
|
|
23994
24542
|
//#endregion
|
|
23995
24543
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -24033,7 +24581,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
24033
24581
|
}
|
|
24034
24582
|
return stateNames;
|
|
24035
24583
|
};
|
|
24036
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
24037
24584
|
const noUncontrolledInput = defineRule({
|
|
24038
24585
|
id: "no-uncontrolled-input",
|
|
24039
24586
|
title: "Uncontrolled input value",
|
|
@@ -24137,6 +24684,38 @@ const noUnescapedEntities = defineRule({
|
|
|
24137
24684
|
} })
|
|
24138
24685
|
});
|
|
24139
24686
|
//#endregion
|
|
24687
|
+
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24688
|
+
const UNINFORMATIVE_LABELS = new Set([
|
|
24689
|
+
"icon",
|
|
24690
|
+
"button",
|
|
24691
|
+
"image",
|
|
24692
|
+
"img",
|
|
24693
|
+
"link",
|
|
24694
|
+
"graphic",
|
|
24695
|
+
"svg",
|
|
24696
|
+
"picture",
|
|
24697
|
+
"element",
|
|
24698
|
+
"field",
|
|
24699
|
+
"input"
|
|
24700
|
+
]);
|
|
24701
|
+
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\"`.";
|
|
24702
|
+
const noUninformativeAriaLabel = defineRule({
|
|
24703
|
+
id: "no-uninformative-aria-label",
|
|
24704
|
+
title: "Uninformative aria-label",
|
|
24705
|
+
severity: "warn",
|
|
24706
|
+
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24707
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24708
|
+
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24709
|
+
if (!ariaLabel) return;
|
|
24710
|
+
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24711
|
+
if (labelValue === null) return;
|
|
24712
|
+
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24713
|
+
node: ariaLabel,
|
|
24714
|
+
message: MESSAGE$11
|
|
24715
|
+
});
|
|
24716
|
+
} })
|
|
24717
|
+
});
|
|
24718
|
+
//#endregion
|
|
24140
24719
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24141
24720
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24142
24721
|
"activedescendant",
|
|
@@ -25608,7 +26187,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
25608
26187
|
//#endregion
|
|
25609
26188
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
25610
26189
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
25611
|
-
const MESSAGE$
|
|
26190
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
25612
26191
|
const resolveSettings$7 = (settings) => {
|
|
25613
26192
|
const reactDoctor = settings?.["react-doctor"];
|
|
25614
26193
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -25642,7 +26221,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
25642
26221
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25643
26222
|
context.report({
|
|
25644
26223
|
node: node.callee,
|
|
25645
|
-
message: MESSAGE$
|
|
26224
|
+
message: MESSAGE$10
|
|
25646
26225
|
});
|
|
25647
26226
|
} };
|
|
25648
26227
|
}
|
|
@@ -26520,7 +27099,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
26520
27099
|
});
|
|
26521
27100
|
//#endregion
|
|
26522
27101
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
26523
|
-
const MESSAGE$
|
|
27102
|
+
const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
26524
27103
|
const preactPreferOndblclick = defineRule({
|
|
26525
27104
|
id: "preact-prefer-ondblclick",
|
|
26526
27105
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -26535,7 +27114,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
26535
27114
|
if (!onDoubleClickAttribute) return;
|
|
26536
27115
|
context.report({
|
|
26537
27116
|
node: onDoubleClickAttribute,
|
|
26538
|
-
message: MESSAGE$
|
|
27117
|
+
message: MESSAGE$9
|
|
26539
27118
|
});
|
|
26540
27119
|
} })
|
|
26541
27120
|
});
|
|
@@ -26575,6 +27154,42 @@ const preactPreferOninput = defineRule({
|
|
|
26575
27154
|
} })
|
|
26576
27155
|
});
|
|
26577
27156
|
//#endregion
|
|
27157
|
+
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27158
|
+
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27159
|
+
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27160
|
+
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`).";
|
|
27161
|
+
const preferDvhOverVh = defineRule({
|
|
27162
|
+
id: "prefer-dvh-over-vh",
|
|
27163
|
+
title: "Use dvh instead of vh for full height",
|
|
27164
|
+
tags: ["design", "test-noise"],
|
|
27165
|
+
severity: "warn",
|
|
27166
|
+
requires: ["tailwind:3.4"],
|
|
27167
|
+
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+.)",
|
|
27168
|
+
create: (context) => ({
|
|
27169
|
+
JSXAttribute(node) {
|
|
27170
|
+
const expression = getInlineStyleExpression(node);
|
|
27171
|
+
if (!expression) return;
|
|
27172
|
+
for (const property of expression.properties ?? []) {
|
|
27173
|
+
const key = getStylePropertyKey(property);
|
|
27174
|
+
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27175
|
+
const value = getStylePropertyStringValue(property);
|
|
27176
|
+
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27177
|
+
node: property,
|
|
27178
|
+
message: MESSAGE$8
|
|
27179
|
+
});
|
|
27180
|
+
}
|
|
27181
|
+
},
|
|
27182
|
+
JSXOpeningElement(node) {
|
|
27183
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27184
|
+
if (!classNameValue) return;
|
|
27185
|
+
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27186
|
+
node,
|
|
27187
|
+
message: MESSAGE$8
|
|
27188
|
+
});
|
|
27189
|
+
}
|
|
27190
|
+
})
|
|
27191
|
+
});
|
|
27192
|
+
//#endregion
|
|
26578
27193
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
26579
27194
|
const preferDynamicImport = defineRule({
|
|
26580
27195
|
id: "prefer-dynamic-import",
|
|
@@ -27166,6 +27781,26 @@ const preferTagOverRole = defineRule({
|
|
|
27166
27781
|
} })
|
|
27167
27782
|
});
|
|
27168
27783
|
//#endregion
|
|
27784
|
+
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27785
|
+
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27786
|
+
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27787
|
+
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27788
|
+
const preferTruncateShorthand = defineRule({
|
|
27789
|
+
id: "prefer-truncate-shorthand",
|
|
27790
|
+
title: "Use truncate shorthand",
|
|
27791
|
+
tags: ["design", "test-noise"],
|
|
27792
|
+
severity: "warn",
|
|
27793
|
+
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27794
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
27795
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27796
|
+
if (!classNameValue) return;
|
|
27797
|
+
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27798
|
+
node,
|
|
27799
|
+
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27800
|
+
});
|
|
27801
|
+
} })
|
|
27802
|
+
});
|
|
27803
|
+
//#endregion
|
|
27169
27804
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27170
27805
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27171
27806
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -38842,6 +39477,17 @@ const reactDoctorRules = [
|
|
|
38842
39477
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
38843
39478
|
}
|
|
38844
39479
|
},
|
|
39480
|
+
{
|
|
39481
|
+
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39482
|
+
id: "no-arbitrary-px-font-size",
|
|
39483
|
+
source: "react-doctor",
|
|
39484
|
+
originallyExternal: false,
|
|
39485
|
+
rule: {
|
|
39486
|
+
...noArbitraryPxFontSize,
|
|
39487
|
+
framework: "global",
|
|
39488
|
+
category: "Accessibility"
|
|
39489
|
+
}
|
|
39490
|
+
},
|
|
38845
39491
|
{
|
|
38846
39492
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
38847
39493
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -38901,6 +39547,18 @@ const reactDoctorRules = [
|
|
|
38901
39547
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
38902
39548
|
}
|
|
38903
39549
|
},
|
|
39550
|
+
{
|
|
39551
|
+
key: "react-doctor/no-autoplay-without-muted",
|
|
39552
|
+
id: "no-autoplay-without-muted",
|
|
39553
|
+
source: "react-doctor",
|
|
39554
|
+
originallyExternal: false,
|
|
39555
|
+
rule: {
|
|
39556
|
+
...noAutoplayWithoutMuted,
|
|
39557
|
+
framework: "global",
|
|
39558
|
+
category: "Accessibility",
|
|
39559
|
+
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39560
|
+
}
|
|
39561
|
+
},
|
|
38904
39562
|
{
|
|
38905
39563
|
key: "react-doctor/no-barrel-import",
|
|
38906
39564
|
id: "no-barrel-import",
|
|
@@ -39054,6 +39712,17 @@ const reactDoctorRules = [
|
|
|
39054
39712
|
category: "Maintainability"
|
|
39055
39713
|
}
|
|
39056
39714
|
},
|
|
39715
|
+
{
|
|
39716
|
+
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39717
|
+
id: "no-deprecated-tailwind-class",
|
|
39718
|
+
source: "react-doctor",
|
|
39719
|
+
originallyExternal: false,
|
|
39720
|
+
rule: {
|
|
39721
|
+
...noDeprecatedTailwindClass,
|
|
39722
|
+
framework: "global",
|
|
39723
|
+
category: "Maintainability"
|
|
39724
|
+
}
|
|
39725
|
+
},
|
|
39057
39726
|
{
|
|
39058
39727
|
key: "react-doctor/no-derived-state",
|
|
39059
39728
|
id: "no-derived-state",
|
|
@@ -39173,6 +39842,17 @@ const reactDoctorRules = [
|
|
|
39173
39842
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39174
39843
|
}
|
|
39175
39844
|
},
|
|
39845
|
+
{
|
|
39846
|
+
key: "react-doctor/no-document-write",
|
|
39847
|
+
id: "no-document-write",
|
|
39848
|
+
source: "react-doctor",
|
|
39849
|
+
originallyExternal: false,
|
|
39850
|
+
rule: {
|
|
39851
|
+
...noDocumentWrite,
|
|
39852
|
+
framework: "global",
|
|
39853
|
+
category: "Performance"
|
|
39854
|
+
}
|
|
39855
|
+
},
|
|
39176
39856
|
{
|
|
39177
39857
|
key: "react-doctor/no-dynamic-import-path",
|
|
39178
39858
|
id: "no-dynamic-import-path",
|
|
@@ -39314,6 +39994,17 @@ const reactDoctorRules = [
|
|
|
39314
39994
|
category: "Performance"
|
|
39315
39995
|
}
|
|
39316
39996
|
},
|
|
39997
|
+
{
|
|
39998
|
+
key: "react-doctor/no-full-viewport-width",
|
|
39999
|
+
id: "no-full-viewport-width",
|
|
40000
|
+
source: "react-doctor",
|
|
40001
|
+
originallyExternal: false,
|
|
40002
|
+
rule: {
|
|
40003
|
+
...noFullViewportWidth,
|
|
40004
|
+
framework: "global",
|
|
40005
|
+
category: "Maintainability"
|
|
40006
|
+
}
|
|
40007
|
+
},
|
|
39317
40008
|
{
|
|
39318
40009
|
key: "react-doctor/no-generic-handler-names",
|
|
39319
40010
|
id: "no-generic-handler-names",
|
|
@@ -39553,6 +40244,17 @@ const reactDoctorRules = [
|
|
|
39553
40244
|
category: "Performance"
|
|
39554
40245
|
}
|
|
39555
40246
|
},
|
|
40247
|
+
{
|
|
40248
|
+
key: "react-doctor/no-low-contrast-inline-style",
|
|
40249
|
+
id: "no-low-contrast-inline-style",
|
|
40250
|
+
source: "react-doctor",
|
|
40251
|
+
originallyExternal: false,
|
|
40252
|
+
rule: {
|
|
40253
|
+
...noLowContrastInlineStyle,
|
|
40254
|
+
framework: "global",
|
|
40255
|
+
category: "Accessibility"
|
|
40256
|
+
}
|
|
40257
|
+
},
|
|
39556
40258
|
{
|
|
39557
40259
|
key: "react-doctor/no-many-boolean-props",
|
|
39558
40260
|
id: "no-many-boolean-props",
|
|
@@ -39830,6 +40532,17 @@ const reactDoctorRules = [
|
|
|
39830
40532
|
category: "Maintainability"
|
|
39831
40533
|
}
|
|
39832
40534
|
},
|
|
40535
|
+
{
|
|
40536
|
+
key: "react-doctor/no-redundant-display-class",
|
|
40537
|
+
id: "no-redundant-display-class",
|
|
40538
|
+
source: "react-doctor",
|
|
40539
|
+
originallyExternal: false,
|
|
40540
|
+
rule: {
|
|
40541
|
+
...noRedundantDisplayClass,
|
|
40542
|
+
framework: "global",
|
|
40543
|
+
category: "Maintainability"
|
|
40544
|
+
}
|
|
40545
|
+
},
|
|
39833
40546
|
{
|
|
39834
40547
|
key: "react-doctor/no-redundant-roles",
|
|
39835
40548
|
id: "no-redundant-roles",
|
|
@@ -39982,6 +40695,18 @@ const reactDoctorRules = [
|
|
|
39982
40695
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39983
40696
|
}
|
|
39984
40697
|
},
|
|
40698
|
+
{
|
|
40699
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40700
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40701
|
+
source: "react-doctor",
|
|
40702
|
+
originallyExternal: false,
|
|
40703
|
+
rule: {
|
|
40704
|
+
...noStringFalseOnBooleanAttribute,
|
|
40705
|
+
framework: "global",
|
|
40706
|
+
category: "Bugs",
|
|
40707
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40708
|
+
}
|
|
40709
|
+
},
|
|
39985
40710
|
{
|
|
39986
40711
|
key: "react-doctor/no-string-refs",
|
|
39987
40712
|
id: "no-string-refs",
|
|
@@ -39994,6 +40719,51 @@ const reactDoctorRules = [
|
|
|
39994
40719
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39995
40720
|
}
|
|
39996
40721
|
},
|
|
40722
|
+
{
|
|
40723
|
+
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40724
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
40725
|
+
source: "react-doctor",
|
|
40726
|
+
originallyExternal: false,
|
|
40727
|
+
rule: {
|
|
40728
|
+
...noSvgCurrentcolorWithFillClass,
|
|
40729
|
+
framework: "global",
|
|
40730
|
+
category: "Maintainability"
|
|
40731
|
+
}
|
|
40732
|
+
},
|
|
40733
|
+
{
|
|
40734
|
+
key: "react-doctor/no-sync-xhr",
|
|
40735
|
+
id: "no-sync-xhr",
|
|
40736
|
+
source: "react-doctor",
|
|
40737
|
+
originallyExternal: false,
|
|
40738
|
+
rule: {
|
|
40739
|
+
...noSyncXhr,
|
|
40740
|
+
framework: "global",
|
|
40741
|
+
category: "Performance"
|
|
40742
|
+
}
|
|
40743
|
+
},
|
|
40744
|
+
{
|
|
40745
|
+
key: "react-doctor/no-tailwind-layout-transition",
|
|
40746
|
+
id: "no-tailwind-layout-transition",
|
|
40747
|
+
source: "react-doctor",
|
|
40748
|
+
originallyExternal: false,
|
|
40749
|
+
rule: {
|
|
40750
|
+
...noTailwindLayoutTransition,
|
|
40751
|
+
framework: "global",
|
|
40752
|
+
category: "Performance"
|
|
40753
|
+
}
|
|
40754
|
+
},
|
|
40755
|
+
{
|
|
40756
|
+
key: "react-doctor/no-target-blank-without-rel",
|
|
40757
|
+
id: "no-target-blank-without-rel",
|
|
40758
|
+
source: "react-doctor",
|
|
40759
|
+
originallyExternal: false,
|
|
40760
|
+
rule: {
|
|
40761
|
+
...noTargetBlankWithoutRel,
|
|
40762
|
+
framework: "global",
|
|
40763
|
+
category: "Accessibility",
|
|
40764
|
+
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40765
|
+
}
|
|
40766
|
+
},
|
|
39997
40767
|
{
|
|
39998
40768
|
key: "react-doctor/no-this-in-sfc",
|
|
39999
40769
|
id: "no-this-in-sfc",
|
|
@@ -40063,6 +40833,18 @@ const reactDoctorRules = [
|
|
|
40063
40833
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
40064
40834
|
}
|
|
40065
40835
|
},
|
|
40836
|
+
{
|
|
40837
|
+
key: "react-doctor/no-uninformative-aria-label",
|
|
40838
|
+
id: "no-uninformative-aria-label",
|
|
40839
|
+
source: "react-doctor",
|
|
40840
|
+
originallyExternal: false,
|
|
40841
|
+
rule: {
|
|
40842
|
+
...noUninformativeAriaLabel,
|
|
40843
|
+
framework: "global",
|
|
40844
|
+
category: "Accessibility",
|
|
40845
|
+
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40846
|
+
}
|
|
40847
|
+
},
|
|
40066
40848
|
{
|
|
40067
40849
|
key: "react-doctor/no-unknown-property",
|
|
40068
40850
|
id: "no-unknown-property",
|
|
@@ -40272,6 +41054,17 @@ const reactDoctorRules = [
|
|
|
40272
41054
|
category: "Bugs"
|
|
40273
41055
|
}
|
|
40274
41056
|
},
|
|
41057
|
+
{
|
|
41058
|
+
key: "react-doctor/prefer-dvh-over-vh",
|
|
41059
|
+
id: "prefer-dvh-over-vh",
|
|
41060
|
+
source: "react-doctor",
|
|
41061
|
+
originallyExternal: false,
|
|
41062
|
+
rule: {
|
|
41063
|
+
...preferDvhOverVh,
|
|
41064
|
+
framework: "global",
|
|
41065
|
+
category: "Maintainability"
|
|
41066
|
+
}
|
|
41067
|
+
},
|
|
40275
41068
|
{
|
|
40276
41069
|
key: "react-doctor/prefer-dynamic-import",
|
|
40277
41070
|
id: "prefer-dynamic-import",
|
|
@@ -40376,6 +41169,17 @@ const reactDoctorRules = [
|
|
|
40376
41169
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
40377
41170
|
}
|
|
40378
41171
|
},
|
|
41172
|
+
{
|
|
41173
|
+
key: "react-doctor/prefer-truncate-shorthand",
|
|
41174
|
+
id: "prefer-truncate-shorthand",
|
|
41175
|
+
source: "react-doctor",
|
|
41176
|
+
originallyExternal: false,
|
|
41177
|
+
rule: {
|
|
41178
|
+
...preferTruncateShorthand,
|
|
41179
|
+
framework: "global",
|
|
41180
|
+
category: "Maintainability"
|
|
41181
|
+
}
|
|
41182
|
+
},
|
|
40379
41183
|
{
|
|
40380
41184
|
key: "react-doctor/prefer-use-effect-event",
|
|
40381
41185
|
id: "prefer-use-effect-event",
|