oxlint-plugin-react-doctor 0.5.6-dev.6b8e756 → 0.5.6-dev.8057497
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -504
- package/dist/index.js +225 -852
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
890
890
|
});
|
|
891
891
|
//#endregion
|
|
892
892
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
893
|
-
const
|
|
893
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
894
|
+
const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
895
|
+
for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
|
|
896
|
+
const character = content[cursor];
|
|
897
|
+
if (character === "\\") {
|
|
898
|
+
cursor += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (character === delimiter) return false;
|
|
902
|
+
if (WHITESPACE_PATTERN.test(character)) return true;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
};
|
|
906
|
+
const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
894
907
|
const characters = content.split("");
|
|
895
908
|
let stringDelimiter = null;
|
|
909
|
+
let isBlankingString = false;
|
|
910
|
+
const templateExpressionDepths = [];
|
|
896
911
|
let index = 0;
|
|
912
|
+
const blankUnlessNewline = (offset) => {
|
|
913
|
+
if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
|
|
914
|
+
};
|
|
897
915
|
while (index < content.length) {
|
|
898
916
|
const character = content[index];
|
|
899
917
|
const nextCharacter = content[index + 1];
|
|
900
918
|
if (stringDelimiter !== null) {
|
|
901
919
|
if (character === "\\") {
|
|
920
|
+
if (isBlankingString) {
|
|
921
|
+
blankUnlessNewline(index);
|
|
922
|
+
blankUnlessNewline(index + 1);
|
|
923
|
+
}
|
|
902
924
|
index += 2;
|
|
903
925
|
continue;
|
|
904
926
|
}
|
|
905
|
-
if (character === stringDelimiter)
|
|
927
|
+
if (character === stringDelimiter) {
|
|
928
|
+
stringDelimiter = null;
|
|
929
|
+
index += 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
|
|
933
|
+
templateExpressionDepths.push(0);
|
|
934
|
+
stringDelimiter = null;
|
|
935
|
+
index += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (isBlankingString) blankUnlessNewline(index);
|
|
906
939
|
index += 1;
|
|
907
940
|
continue;
|
|
908
941
|
}
|
|
909
|
-
if (character === "\"" || character === "'"
|
|
942
|
+
if (character === "\"" || character === "'") {
|
|
910
943
|
stringDelimiter = character;
|
|
944
|
+
isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
|
|
945
|
+
index += 1;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (character === "`") {
|
|
949
|
+
stringDelimiter = "`";
|
|
950
|
+
isBlankingString = blankStringContents;
|
|
911
951
|
index += 1;
|
|
912
952
|
continue;
|
|
913
953
|
}
|
|
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
|
|
|
926
966
|
index += 2;
|
|
927
967
|
break;
|
|
928
968
|
}
|
|
929
|
-
|
|
969
|
+
blankUnlessNewline(index);
|
|
930
970
|
index += 1;
|
|
931
971
|
}
|
|
932
972
|
continue;
|
|
933
973
|
}
|
|
974
|
+
if (templateExpressionDepths.length > 0) {
|
|
975
|
+
const innermost = templateExpressionDepths.length - 1;
|
|
976
|
+
if (character === "{") templateExpressionDepths[innermost] += 1;
|
|
977
|
+
else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
|
|
978
|
+
templateExpressionDepths.pop();
|
|
979
|
+
stringDelimiter = "`";
|
|
980
|
+
isBlankingString = blankStringContents;
|
|
981
|
+
} else templateExpressionDepths[innermost] -= 1;
|
|
982
|
+
}
|
|
934
983
|
index += 1;
|
|
935
984
|
}
|
|
936
985
|
return characters.join("");
|
|
937
986
|
};
|
|
987
|
+
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
988
|
+
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
938
989
|
//#endregion
|
|
939
990
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
940
991
|
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
941
|
-
const
|
|
992
|
+
const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
993
|
+
const getScannableContent = (file, ignoreStringLiterals = false) => {
|
|
942
994
|
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
943
|
-
const
|
|
995
|
+
const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
|
|
996
|
+
const cachedContent = cache.get(file);
|
|
944
997
|
if (cachedContent !== void 0) return cachedContent;
|
|
945
|
-
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
946
|
-
|
|
998
|
+
const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
|
|
999
|
+
cache.set(file, strippedContent);
|
|
947
1000
|
return strippedContent;
|
|
948
1001
|
};
|
|
949
|
-
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
1002
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
|
|
950
1003
|
if (!shouldScan(file)) return [];
|
|
951
|
-
const content = getScannableContent(file);
|
|
1004
|
+
const content = getScannableContent(file, ignoreStringLiterals);
|
|
952
1005
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
953
1006
|
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
954
1007
|
if (matchedPattern === void 0) return [];
|
|
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
973
1026
|
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
974
1027
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
975
1028
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1029
|
+
ignoreStringLiterals: true,
|
|
976
1030
|
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
977
1031
|
})
|
|
978
1032
|
});
|
|
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1915
|
});
|
|
1862
1916
|
//#endregion
|
|
1863
1917
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1918
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1919
|
const anchorHasContent = defineRule({
|
|
1866
1920
|
id: "anchor-has-content",
|
|
1867
1921
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1931
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1932
|
context.report({
|
|
1879
1933
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1934
|
+
message: MESSAGE$59
|
|
1881
1935
|
});
|
|
1882
1936
|
} })
|
|
1883
1937
|
});
|
|
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2325
|
};
|
|
2272
2326
|
//#endregion
|
|
2273
2327
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2328
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2329
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2330
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2331
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2343
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2344
|
context.report({
|
|
2291
2345
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2346
|
+
message: MESSAGE$58
|
|
2293
2347
|
});
|
|
2294
2348
|
return;
|
|
2295
2349
|
}
|
|
2296
2350
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2351
|
context.report({
|
|
2298
2352
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2353
|
+
message: MESSAGE$58
|
|
2300
2354
|
});
|
|
2301
2355
|
} })
|
|
2302
2356
|
});
|
|
@@ -4272,7 +4326,7 @@ const asyncParallel = defineRule({
|
|
|
4272
4326
|
});
|
|
4273
4327
|
//#endregion
|
|
4274
4328
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4275
|
-
const MESSAGE$
|
|
4329
|
+
const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4276
4330
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
4331
|
const STORAGE_GLOBALS = new Set([
|
|
4278
4332
|
"window",
|
|
@@ -4306,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4306
4360
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
4361
|
context.report({
|
|
4308
4362
|
node,
|
|
4309
|
-
message: MESSAGE$
|
|
4363
|
+
message: MESSAGE$57
|
|
4310
4364
|
});
|
|
4311
4365
|
},
|
|
4312
4366
|
AssignmentExpression(node) {
|
|
@@ -4317,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4317
4371
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
4372
|
context.report({
|
|
4319
4373
|
node: target,
|
|
4320
|
-
message: MESSAGE$
|
|
4374
|
+
message: MESSAGE$57
|
|
4321
4375
|
});
|
|
4322
4376
|
}
|
|
4323
4377
|
})
|
|
@@ -4694,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4694
4748
|
//#endregion
|
|
4695
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4697
|
-
const MESSAGE$
|
|
4751
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4752
|
const KEY_HANDLERS = [
|
|
4699
4753
|
"onKeyUp",
|
|
4700
4754
|
"onKeyDown",
|
|
@@ -4726,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4726
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4781
|
context.report({
|
|
4728
4782
|
node: node.name,
|
|
4729
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4730
4784
|
});
|
|
4731
4785
|
} };
|
|
4732
4786
|
}
|
|
@@ -4841,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4841
4895
|
};
|
|
4842
4896
|
//#endregion
|
|
4843
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4844
|
-
const MESSAGE$
|
|
4898
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4845
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4846
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4847
4901
|
"alt",
|
|
@@ -5002,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5002
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5003
5057
|
context.report({
|
|
5004
5058
|
node: opening,
|
|
5005
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
5006
5060
|
});
|
|
5007
5061
|
} };
|
|
5008
5062
|
}
|
|
@@ -5131,7 +5185,6 @@ const dangerousHtmlSink = defineRule({
|
|
|
5131
5185
|
return findings;
|
|
5132
5186
|
}
|
|
5133
5187
|
});
|
|
5134
|
-
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5135
5188
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5136
5189
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5137
5190
|
"continue",
|
|
@@ -5430,10 +5483,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5430
5483
|
});
|
|
5431
5484
|
//#endregion
|
|
5432
5485
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5433
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5486
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5434
5487
|
//#endregion
|
|
5435
5488
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5436
|
-
const MESSAGE$
|
|
5489
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5437
5490
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5438
5491
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5439
5492
|
"aria-label",
|
|
@@ -5452,11 +5505,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5452
5505
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5453
5506
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5454
5507
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5455
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5508
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5456
5509
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5457
5510
|
context.report({
|
|
5458
5511
|
node: node.name,
|
|
5459
|
-
message: MESSAGE$
|
|
5512
|
+
message: MESSAGE$54
|
|
5460
5513
|
});
|
|
5461
5514
|
} })
|
|
5462
5515
|
});
|
|
@@ -5495,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5495
5548
|
};
|
|
5496
5549
|
//#endregion
|
|
5497
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5498
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5499
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5500
5553
|
"observer",
|
|
5501
5554
|
"lazy",
|
|
@@ -5698,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5698
5751
|
const reportAt = (node) => {
|
|
5699
5752
|
context.report({
|
|
5700
5753
|
node,
|
|
5701
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5702
5755
|
});
|
|
5703
5756
|
};
|
|
5704
5757
|
return {
|
|
@@ -7846,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7846
7899
|
});
|
|
7847
7900
|
//#endregion
|
|
7848
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7849
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7850
7903
|
const forwardRefUsesRef = defineRule({
|
|
7851
7904
|
id: "forward-ref-uses-ref",
|
|
7852
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7866,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7866
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7867
7920
|
context.report({
|
|
7868
7921
|
node: inner,
|
|
7869
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7870
7923
|
});
|
|
7871
7924
|
} })
|
|
7872
7925
|
});
|
|
@@ -7903,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7903
7956
|
});
|
|
7904
7957
|
//#endregion
|
|
7905
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7906
|
-
const MESSAGE$
|
|
7959
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7907
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7908
7961
|
"h1",
|
|
7909
7962
|
"h2",
|
|
@@ -7936,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7936
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7937
7990
|
context.report({
|
|
7938
7991
|
node,
|
|
7939
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7940
7993
|
});
|
|
7941
7994
|
} };
|
|
7942
7995
|
}
|
|
@@ -8074,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8074
8127
|
});
|
|
8075
8128
|
//#endregion
|
|
8076
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8077
|
-
const MESSAGE$
|
|
8130
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8078
8131
|
const resolveSettings$38 = (settings) => {
|
|
8079
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
8080
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8122,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
8122
8175
|
if (!lang) {
|
|
8123
8176
|
context.report({
|
|
8124
8177
|
node: node.name,
|
|
8125
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
8126
8179
|
});
|
|
8127
8180
|
return;
|
|
8128
8181
|
}
|
|
@@ -8130,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
8130
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
8131
8184
|
context.report({
|
|
8132
8185
|
node: lang,
|
|
8133
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
8134
8187
|
});
|
|
8135
8188
|
return;
|
|
8136
8189
|
}
|
|
8137
8190
|
if (hasSpread && !lang) context.report({
|
|
8138
8191
|
node: node.name,
|
|
8139
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
8140
8193
|
});
|
|
8141
8194
|
} };
|
|
8142
8195
|
}
|
|
@@ -8350,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8350
8403
|
});
|
|
8351
8404
|
//#endregion
|
|
8352
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8353
|
-
const MESSAGE$
|
|
8406
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8354
8407
|
const evaluateTitleValue = (value) => {
|
|
8355
8408
|
if (!value) return "missing";
|
|
8356
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8390,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8390
8443
|
if (!titleAttr) {
|
|
8391
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8392
8445
|
node: node.name,
|
|
8393
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8394
8447
|
});
|
|
8395
8448
|
return;
|
|
8396
8449
|
}
|
|
8397
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8398
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8399
8452
|
node: titleAttr,
|
|
8400
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8401
8454
|
});
|
|
8402
8455
|
} })
|
|
8403
8456
|
});
|
|
@@ -8501,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8501
8554
|
});
|
|
8502
8555
|
//#endregion
|
|
8503
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8504
|
-
const MESSAGE$
|
|
8557
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8505
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8506
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8507
8560
|
"image",
|
|
@@ -8566,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8566
8619
|
if (!altAttribute) return;
|
|
8567
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8568
8621
|
node: altAttribute,
|
|
8569
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8570
8623
|
});
|
|
8571
8624
|
} };
|
|
8572
8625
|
}
|
|
@@ -10923,7 +10976,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10923
10976
|
});
|
|
10924
10977
|
//#endregion
|
|
10925
10978
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10926
|
-
const MESSAGE$
|
|
10979
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10927
10980
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10928
10981
|
"code",
|
|
10929
10982
|
"pre",
|
|
@@ -10959,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10959
11012
|
if (isInsideLiteralTextTag(node)) return;
|
|
10960
11013
|
context.report({
|
|
10961
11014
|
node,
|
|
10962
|
-
message: MESSAGE$
|
|
11015
|
+
message: MESSAGE$47
|
|
10963
11016
|
});
|
|
10964
11017
|
} })
|
|
10965
11018
|
});
|
|
@@ -10990,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10990
11043
|
};
|
|
10991
11044
|
//#endregion
|
|
10992
11045
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10993
|
-
const MESSAGE$
|
|
11046
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10994
11047
|
const CONTEXT_MODULES$1 = [
|
|
10995
11048
|
"react",
|
|
10996
11049
|
"use-context-selector",
|
|
@@ -11088,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11088
11141
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11089
11142
|
context.report({
|
|
11090
11143
|
node: attribute,
|
|
11091
|
-
message: MESSAGE$
|
|
11144
|
+
message: MESSAGE$46
|
|
11092
11145
|
});
|
|
11093
11146
|
}
|
|
11094
11147
|
}
|
|
@@ -11174,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11174
11227
|
};
|
|
11175
11228
|
//#endregion
|
|
11176
11229
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11177
|
-
const MESSAGE$
|
|
11230
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11178
11231
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11179
11232
|
"icon",
|
|
11180
11233
|
"Icon",
|
|
@@ -11443,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11443
11496
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11444
11497
|
context.report({
|
|
11445
11498
|
node,
|
|
11446
|
-
message: MESSAGE$
|
|
11499
|
+
message: MESSAGE$45
|
|
11447
11500
|
});
|
|
11448
11501
|
}
|
|
11449
11502
|
};
|
|
@@ -11731,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11731
11784
|
];
|
|
11732
11785
|
//#endregion
|
|
11733
11786
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11734
|
-
const MESSAGE$
|
|
11787
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11735
11788
|
const isDataArrayPropName = (propName) => {
|
|
11736
11789
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11737
11790
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11815,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11815
11868
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11816
11869
|
context.report({
|
|
11817
11870
|
node,
|
|
11818
|
-
message: MESSAGE$
|
|
11871
|
+
message: MESSAGE$44
|
|
11819
11872
|
});
|
|
11820
11873
|
}
|
|
11821
11874
|
};
|
|
@@ -12073,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12073
12126
|
]);
|
|
12074
12127
|
//#endregion
|
|
12075
12128
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12076
|
-
const MESSAGE$
|
|
12129
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12077
12130
|
const isAccessorPredicateName = (propName) => {
|
|
12078
12131
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12079
12132
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12279,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12279
12332
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12280
12333
|
context.report({
|
|
12281
12334
|
node,
|
|
12282
|
-
message: MESSAGE$
|
|
12335
|
+
message: MESSAGE$43
|
|
12283
12336
|
});
|
|
12284
12337
|
}
|
|
12285
12338
|
};
|
|
@@ -12499,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12499
12552
|
];
|
|
12500
12553
|
//#endregion
|
|
12501
12554
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12502
|
-
const MESSAGE$
|
|
12555
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12503
12556
|
const isConfigObjectPropName = (propName) => {
|
|
12504
12557
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12505
12558
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12587,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12587
12640
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12588
12641
|
context.report({
|
|
12589
12642
|
node,
|
|
12590
|
-
message: MESSAGE$
|
|
12643
|
+
message: MESSAGE$42
|
|
12591
12644
|
});
|
|
12592
12645
|
}
|
|
12593
12646
|
};
|
|
@@ -12595,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12595
12648
|
});
|
|
12596
12649
|
//#endregion
|
|
12597
12650
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12598
|
-
const MESSAGE$
|
|
12651
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12599
12652
|
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;
|
|
12600
12653
|
const resolveSettings$28 = (settings) => {
|
|
12601
12654
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12636,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12636
12689
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12637
12690
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12638
12691
|
node: attribute,
|
|
12639
|
-
message: MESSAGE$
|
|
12692
|
+
message: MESSAGE$41
|
|
12640
12693
|
});
|
|
12641
12694
|
}
|
|
12642
12695
|
} };
|
|
@@ -12951,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12951
13004
|
});
|
|
12952
13005
|
//#endregion
|
|
12953
13006
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12954
|
-
const MESSAGE$
|
|
13007
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12955
13008
|
const resolveSettings$25 = (settings) => {
|
|
12956
13009
|
const reactDoctor = settings?.["react-doctor"];
|
|
12957
13010
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12992,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12992
13045
|
}
|
|
12993
13046
|
context.report({
|
|
12994
13047
|
node: attribute,
|
|
12995
|
-
message: MESSAGE$
|
|
13048
|
+
message: MESSAGE$40
|
|
12996
13049
|
});
|
|
12997
13050
|
}
|
|
12998
13051
|
} };
|
|
@@ -13220,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13220
13273
|
});
|
|
13221
13274
|
//#endregion
|
|
13222
13275
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13223
|
-
const MESSAGE$
|
|
13276
|
+
const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13224
13277
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13225
13278
|
"aa",
|
|
13226
13279
|
"ab",
|
|
@@ -13432,7 +13485,7 @@ const lang = defineRule({
|
|
|
13432
13485
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13433
13486
|
context.report({
|
|
13434
13487
|
node: langAttr,
|
|
13435
|
-
message: MESSAGE$
|
|
13488
|
+
message: MESSAGE$39
|
|
13436
13489
|
});
|
|
13437
13490
|
return;
|
|
13438
13491
|
}
|
|
@@ -13441,7 +13494,7 @@ const lang = defineRule({
|
|
|
13441
13494
|
if (value === null) return;
|
|
13442
13495
|
if (!isValidLangTag(value)) context.report({
|
|
13443
13496
|
node: langAttr,
|
|
13444
|
-
message: MESSAGE$
|
|
13497
|
+
message: MESSAGE$39
|
|
13445
13498
|
});
|
|
13446
13499
|
} })
|
|
13447
13500
|
});
|
|
@@ -13467,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13467
13520
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13468
13521
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13469
13522
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13523
|
+
ignoreStringLiterals: true,
|
|
13470
13524
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13471
13525
|
})
|
|
13472
13526
|
});
|
|
@@ -13485,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13485
13539
|
});
|
|
13486
13540
|
//#endregion
|
|
13487
13541
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13488
|
-
const MESSAGE$
|
|
13542
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13489
13543
|
const DEFAULT_AUDIO = ["audio"];
|
|
13490
13544
|
const DEFAULT_VIDEO = ["video"];
|
|
13491
13545
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13526,7 +13580,7 @@ const mediaHasCaption = defineRule({
|
|
|
13526
13580
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13527
13581
|
context.report({
|
|
13528
13582
|
node: node.name,
|
|
13529
|
-
message: MESSAGE$
|
|
13583
|
+
message: MESSAGE$38
|
|
13530
13584
|
});
|
|
13531
13585
|
return;
|
|
13532
13586
|
}
|
|
@@ -13543,7 +13597,7 @@ const mediaHasCaption = defineRule({
|
|
|
13543
13597
|
return kindValue.value.toLowerCase() === "captions";
|
|
13544
13598
|
})) context.report({
|
|
13545
13599
|
node: node.name,
|
|
13546
|
-
message: MESSAGE$
|
|
13600
|
+
message: MESSAGE$38
|
|
13547
13601
|
});
|
|
13548
13602
|
} };
|
|
13549
13603
|
}
|
|
@@ -15344,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15344
15398
|
});
|
|
15345
15399
|
//#endregion
|
|
15346
15400
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15347
|
-
const MESSAGE$
|
|
15401
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15348
15402
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15349
15403
|
const noAccessKey = defineRule({
|
|
15350
15404
|
id: "no-access-key",
|
|
@@ -15361,7 +15415,7 @@ const noAccessKey = defineRule({
|
|
|
15361
15415
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15362
15416
|
context.report({
|
|
15363
15417
|
node: accessKey,
|
|
15364
|
-
message: MESSAGE$
|
|
15418
|
+
message: MESSAGE$37
|
|
15365
15419
|
});
|
|
15366
15420
|
return;
|
|
15367
15421
|
}
|
|
@@ -15371,7 +15425,7 @@ const noAccessKey = defineRule({
|
|
|
15371
15425
|
if (isUndefinedIdentifier(expression)) return;
|
|
15372
15426
|
context.report({
|
|
15373
15427
|
node: accessKey,
|
|
15374
|
-
message: MESSAGE$
|
|
15428
|
+
message: MESSAGE$37
|
|
15375
15429
|
});
|
|
15376
15430
|
}
|
|
15377
15431
|
} })
|
|
@@ -15853,41 +15907,8 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15853
15907
|
} })
|
|
15854
15908
|
});
|
|
15855
15909
|
//#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
|
|
15889
15910
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15890
|
-
const MESSAGE$
|
|
15911
|
+
const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15891
15912
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15892
15913
|
id: "no-aria-hidden-on-focusable",
|
|
15893
15914
|
title: "aria-hidden on focusable element",
|
|
@@ -15914,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15914
15935
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15915
15936
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15916
15937
|
node: ariaHidden,
|
|
15917
|
-
message: MESSAGE$
|
|
15938
|
+
message: MESSAGE$36
|
|
15918
15939
|
});
|
|
15919
15940
|
} })
|
|
15920
15941
|
});
|
|
@@ -16282,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16282
16303
|
});
|
|
16283
16304
|
//#endregion
|
|
16284
16305
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16285
|
-
const MESSAGE$
|
|
16306
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16286
16307
|
const SECOND_INDEX_METHODS = new Set([
|
|
16287
16308
|
"every",
|
|
16288
16309
|
"filter",
|
|
@@ -16486,7 +16507,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16486
16507
|
}
|
|
16487
16508
|
context.report({
|
|
16488
16509
|
node: keyAttribute,
|
|
16489
|
-
message: MESSAGE$
|
|
16510
|
+
message: MESSAGE$35
|
|
16490
16511
|
});
|
|
16491
16512
|
},
|
|
16492
16513
|
CallExpression(node) {
|
|
@@ -16506,7 +16527,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16506
16527
|
if (propName !== "key") continue;
|
|
16507
16528
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16508
16529
|
node: property,
|
|
16509
|
-
message: MESSAGE$
|
|
16530
|
+
message: MESSAGE$35
|
|
16510
16531
|
});
|
|
16511
16532
|
}
|
|
16512
16533
|
}
|
|
@@ -16514,7 +16535,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16514
16535
|
});
|
|
16515
16536
|
//#endregion
|
|
16516
16537
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16517
|
-
const MESSAGE$
|
|
16538
|
+
const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16518
16539
|
const noAsyncEffectCallback = defineRule({
|
|
16519
16540
|
id: "no-async-effect-callback",
|
|
16520
16541
|
title: "Async effect callback",
|
|
@@ -16528,13 +16549,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16528
16549
|
if (!callback.async) return;
|
|
16529
16550
|
context.report({
|
|
16530
16551
|
node: callback,
|
|
16531
|
-
message: MESSAGE$
|
|
16552
|
+
message: MESSAGE$34
|
|
16532
16553
|
});
|
|
16533
16554
|
} })
|
|
16534
16555
|
});
|
|
16535
16556
|
//#endregion
|
|
16536
16557
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16537
|
-
const MESSAGE$
|
|
16558
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16538
16559
|
const resolveSettings$21 = (settings) => {
|
|
16539
16560
|
const reactDoctor = settings?.["react-doctor"];
|
|
16540
16561
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16590,45 +16611,12 @@ const noAutofocus = defineRule({
|
|
|
16590
16611
|
}
|
|
16591
16612
|
context.report({
|
|
16592
16613
|
node: autoFocusAttribute,
|
|
16593
|
-
message: MESSAGE$
|
|
16614
|
+
message: MESSAGE$33
|
|
16594
16615
|
});
|
|
16595
16616
|
} };
|
|
16596
16617
|
}
|
|
16597
16618
|
});
|
|
16598
16619
|
//#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
|
|
16632
16620
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16633
16621
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16634
16622
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -17127,7 +17115,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17127
17115
|
});
|
|
17128
17116
|
//#endregion
|
|
17129
17117
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17130
|
-
const MESSAGE$
|
|
17118
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17131
17119
|
const noChildrenProp = defineRule({
|
|
17132
17120
|
id: "no-children-prop",
|
|
17133
17121
|
title: "Children passed as a prop",
|
|
@@ -17139,7 +17127,7 @@ const noChildrenProp = defineRule({
|
|
|
17139
17127
|
if (node.name.name !== "children") return;
|
|
17140
17128
|
context.report({
|
|
17141
17129
|
node: node.name,
|
|
17142
|
-
message: MESSAGE$
|
|
17130
|
+
message: MESSAGE$32
|
|
17143
17131
|
});
|
|
17144
17132
|
},
|
|
17145
17133
|
CallExpression(node) {
|
|
@@ -17152,7 +17140,7 @@ const noChildrenProp = defineRule({
|
|
|
17152
17140
|
const propertyKey = property.key;
|
|
17153
17141
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17154
17142
|
node: propertyKey,
|
|
17155
|
-
message: MESSAGE$
|
|
17143
|
+
message: MESSAGE$32
|
|
17156
17144
|
});
|
|
17157
17145
|
}
|
|
17158
17146
|
}
|
|
@@ -17160,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
17160
17148
|
});
|
|
17161
17149
|
//#endregion
|
|
17162
17150
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17163
|
-
const MESSAGE$
|
|
17151
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17164
17152
|
const noCloneElement = defineRule({
|
|
17165
17153
|
id: "no-clone-element",
|
|
17166
17154
|
title: "cloneElement makes child props fragile",
|
|
@@ -17173,7 +17161,7 @@ const noCloneElement = defineRule({
|
|
|
17173
17161
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17174
17162
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17175
17163
|
node: callee,
|
|
17176
|
-
message: MESSAGE$
|
|
17164
|
+
message: MESSAGE$31
|
|
17177
17165
|
});
|
|
17178
17166
|
return;
|
|
17179
17167
|
}
|
|
@@ -17186,7 +17174,7 @@ const noCloneElement = defineRule({
|
|
|
17186
17174
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17187
17175
|
context.report({
|
|
17188
17176
|
node: callee,
|
|
17189
|
-
message: MESSAGE$
|
|
17177
|
+
message: MESSAGE$31
|
|
17190
17178
|
});
|
|
17191
17179
|
}
|
|
17192
17180
|
} })
|
|
@@ -17235,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17235
17223
|
};
|
|
17236
17224
|
//#endregion
|
|
17237
17225
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17238
|
-
const MESSAGE$
|
|
17226
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17239
17227
|
const CONTEXT_MODULES = [
|
|
17240
17228
|
"react",
|
|
17241
17229
|
"use-context-selector",
|
|
@@ -17271,13 +17259,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17271
17259
|
if (!componentOrHookName) return;
|
|
17272
17260
|
context.report({
|
|
17273
17261
|
node,
|
|
17274
|
-
message: `${MESSAGE$
|
|
17262
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17275
17263
|
});
|
|
17276
17264
|
} })
|
|
17277
17265
|
});
|
|
17278
17266
|
//#endregion
|
|
17279
17267
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17280
|
-
const MESSAGE$
|
|
17268
|
+
const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17281
17269
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17282
17270
|
id: "no-create-ref-in-function-component",
|
|
17283
17271
|
title: "createRef in function component",
|
|
@@ -17296,7 +17284,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17296
17284
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17297
17285
|
context.report({
|
|
17298
17286
|
node,
|
|
17299
|
-
message: MESSAGE$
|
|
17287
|
+
message: MESSAGE$29
|
|
17300
17288
|
});
|
|
17301
17289
|
} })
|
|
17302
17290
|
});
|
|
@@ -17436,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17436
17424
|
});
|
|
17437
17425
|
//#endregion
|
|
17438
17426
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17439
|
-
const MESSAGE$
|
|
17427
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17440
17428
|
const noDanger = defineRule({
|
|
17441
17429
|
id: "no-danger",
|
|
17442
17430
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17449,7 +17437,7 @@ const noDanger = defineRule({
|
|
|
17449
17437
|
if (!propAttribute) return;
|
|
17450
17438
|
context.report({
|
|
17451
17439
|
node: propAttribute.name,
|
|
17452
|
-
message: MESSAGE$
|
|
17440
|
+
message: MESSAGE$28
|
|
17453
17441
|
});
|
|
17454
17442
|
},
|
|
17455
17443
|
CallExpression(node) {
|
|
@@ -17461,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17461
17449
|
const propertyKey = property.key;
|
|
17462
17450
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17463
17451
|
node: propertyKey,
|
|
17464
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$28
|
|
17465
17453
|
});
|
|
17466
17454
|
}
|
|
17467
17455
|
}
|
|
@@ -17469,7 +17457,7 @@ const noDanger = defineRule({
|
|
|
17469
17457
|
});
|
|
17470
17458
|
//#endregion
|
|
17471
17459
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17472
|
-
const MESSAGE$
|
|
17460
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17473
17461
|
const isLineBreak = (child) => {
|
|
17474
17462
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17475
17463
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17539,7 +17527,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17539
17527
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17540
17528
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17541
17529
|
node: opening,
|
|
17542
|
-
message: MESSAGE$
|
|
17530
|
+
message: MESSAGE$27
|
|
17543
17531
|
});
|
|
17544
17532
|
},
|
|
17545
17533
|
CallExpression(node) {
|
|
@@ -17551,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17551
17539
|
if (!propsShape.hasDangerously) return;
|
|
17552
17540
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17553
17541
|
node,
|
|
17554
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$27
|
|
17555
17543
|
});
|
|
17556
17544
|
}
|
|
17557
17545
|
})
|
|
@@ -17716,37 +17704,6 @@ const noDefaultProps = defineRule({
|
|
|
17716
17704
|
} })
|
|
17717
17705
|
});
|
|
17718
17706
|
//#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
|
|
17750
17707
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17751
17708
|
const isInitialOnlyPropName = (propName) => {
|
|
17752
17709
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -18159,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18159
18116
|
//#endregion
|
|
18160
18117
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18161
18118
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18162
|
-
const MESSAGE$
|
|
18119
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18163
18120
|
const resolveSettings$20 = (settings) => {
|
|
18164
18121
|
const reactDoctor = settings?.["react-doctor"];
|
|
18165
18122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18178,7 +18135,7 @@ const noDidMountSetState = defineRule({
|
|
|
18178
18135
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18179
18136
|
context.report({
|
|
18180
18137
|
node: node.callee,
|
|
18181
|
-
message: MESSAGE$
|
|
18138
|
+
message: MESSAGE$26
|
|
18182
18139
|
});
|
|
18183
18140
|
} };
|
|
18184
18141
|
}
|
|
@@ -18186,7 +18143,7 @@ const noDidMountSetState = defineRule({
|
|
|
18186
18143
|
//#endregion
|
|
18187
18144
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18188
18145
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18189
|
-
const MESSAGE$
|
|
18146
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18190
18147
|
const resolveSettings$19 = (settings) => {
|
|
18191
18148
|
const reactDoctor = settings?.["react-doctor"];
|
|
18192
18149
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18205,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18205
18162
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18206
18163
|
context.report({
|
|
18207
18164
|
node: node.callee,
|
|
18208
|
-
message: MESSAGE$
|
|
18165
|
+
message: MESSAGE$25
|
|
18209
18166
|
});
|
|
18210
18167
|
} };
|
|
18211
18168
|
}
|
|
@@ -18228,7 +18185,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18228
18185
|
};
|
|
18229
18186
|
//#endregion
|
|
18230
18187
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18231
|
-
const MESSAGE$
|
|
18188
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18232
18189
|
const shouldIgnoreMutation = (node) => {
|
|
18233
18190
|
let isConstructor = false;
|
|
18234
18191
|
let isInsideCallExpression = false;
|
|
@@ -18250,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18250
18207
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18251
18208
|
context.report({
|
|
18252
18209
|
node: reportNode,
|
|
18253
|
-
message: MESSAGE$
|
|
18210
|
+
message: MESSAGE$24
|
|
18254
18211
|
});
|
|
18255
18212
|
};
|
|
18256
18213
|
const noDirectMutationState = defineRule({
|
|
@@ -18461,7 +18418,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18461
18418
|
});
|
|
18462
18419
|
//#endregion
|
|
18463
18420
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18464
|
-
const MESSAGE$
|
|
18421
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18465
18422
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18466
18423
|
const noDocumentWrite = defineRule({
|
|
18467
18424
|
id: "no-document-write",
|
|
@@ -18475,7 +18432,7 @@ const noDocumentWrite = defineRule({
|
|
|
18475
18432
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18476
18433
|
context.report({
|
|
18477
18434
|
node,
|
|
18478
|
-
message: MESSAGE$
|
|
18435
|
+
message: MESSAGE$23
|
|
18479
18436
|
});
|
|
18480
18437
|
} })
|
|
18481
18438
|
});
|
|
@@ -19858,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19858
19815
|
"ReactDOM",
|
|
19859
19816
|
"ReactDom"
|
|
19860
19817
|
]);
|
|
19861
|
-
const MESSAGE$
|
|
19818
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19862
19819
|
const noFindDomNode = defineRule({
|
|
19863
19820
|
id: "no-find-dom-node",
|
|
19864
19821
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19869,7 +19826,7 @@ const noFindDomNode = defineRule({
|
|
|
19869
19826
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19870
19827
|
context.report({
|
|
19871
19828
|
node: callee,
|
|
19872
|
-
message: MESSAGE$
|
|
19829
|
+
message: MESSAGE$22
|
|
19873
19830
|
});
|
|
19874
19831
|
return;
|
|
19875
19832
|
}
|
|
@@ -19880,7 +19837,7 @@ const noFindDomNode = defineRule({
|
|
|
19880
19837
|
if (callee.property.name !== "findDOMNode") return;
|
|
19881
19838
|
context.report({
|
|
19882
19839
|
node: callee.property,
|
|
19883
|
-
message: MESSAGE$
|
|
19840
|
+
message: MESSAGE$22
|
|
19884
19841
|
});
|
|
19885
19842
|
}
|
|
19886
19843
|
} })
|
|
@@ -19921,41 +19878,6 @@ const noFullLodashImport = defineRule({
|
|
|
19921
19878
|
} })
|
|
19922
19879
|
});
|
|
19923
19880
|
//#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
|
|
19959
19881
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19960
19882
|
const noGenericHandlerNames = defineRule({
|
|
19961
19883
|
id: "no-generic-handler-names",
|
|
@@ -20018,7 +19940,7 @@ const noGiantComponent = defineRule({
|
|
|
20018
19940
|
});
|
|
20019
19941
|
//#endregion
|
|
20020
19942
|
//#region src/plugin/constants/style.ts
|
|
20021
|
-
const LAYOUT_PROPERTIES
|
|
19943
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
20022
19944
|
"width",
|
|
20023
19945
|
"height",
|
|
20024
19946
|
"top",
|
|
@@ -20088,6 +20010,17 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
20088
20010
|
} })
|
|
20089
20011
|
});
|
|
20090
20012
|
//#endregion
|
|
20013
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
20014
|
+
const getStringFromClassNameAttr = (node) => {
|
|
20015
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
20016
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
20017
|
+
if (!classAttr?.value) return null;
|
|
20018
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
20019
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
20020
|
+
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;
|
|
20021
|
+
return null;
|
|
20022
|
+
};
|
|
20023
|
+
//#endregion
|
|
20091
20024
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
20092
20025
|
const noGradientText = defineRule({
|
|
20093
20026
|
id: "no-gradient-text",
|
|
@@ -20146,7 +20079,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20146
20079
|
});
|
|
20147
20080
|
//#endregion
|
|
20148
20081
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20149
|
-
const MESSAGE$
|
|
20082
|
+
const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20150
20083
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20151
20084
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20152
20085
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20160,7 +20093,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20160
20093
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20161
20094
|
context.report({
|
|
20162
20095
|
node: node.name,
|
|
20163
|
-
message: MESSAGE$
|
|
20096
|
+
message: MESSAGE$21
|
|
20164
20097
|
});
|
|
20165
20098
|
} })
|
|
20166
20099
|
});
|
|
@@ -20395,7 +20328,7 @@ const noIsMounted = defineRule({
|
|
|
20395
20328
|
});
|
|
20396
20329
|
//#endregion
|
|
20397
20330
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20398
|
-
const MESSAGE$
|
|
20331
|
+
const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20399
20332
|
const isJsonMethodCall = (node, method) => {
|
|
20400
20333
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20401
20334
|
const callee = node.callee;
|
|
@@ -20412,13 +20345,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20412
20345
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20413
20346
|
context.report({
|
|
20414
20347
|
node,
|
|
20415
|
-
message: MESSAGE$
|
|
20348
|
+
message: MESSAGE$20
|
|
20416
20349
|
});
|
|
20417
20350
|
} })
|
|
20418
20351
|
});
|
|
20419
20352
|
//#endregion
|
|
20420
20353
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20421
|
-
const MESSAGE$
|
|
20354
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20422
20355
|
const isJsxElementTypeReference = (node) => {
|
|
20423
20356
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20424
20357
|
const typeName = node.typeName;
|
|
@@ -20435,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20435
20368
|
if (!typeAnnotation) return;
|
|
20436
20369
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20437
20370
|
node: typeAnnotation,
|
|
20438
|
-
message: MESSAGE$
|
|
20371
|
+
message: MESSAGE$19
|
|
20439
20372
|
});
|
|
20440
20373
|
};
|
|
20441
20374
|
const noJsxElementType = defineRule({
|
|
@@ -20545,7 +20478,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20545
20478
|
let propertyName = null;
|
|
20546
20479
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20547
20480
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20548
|
-
if (propertyName && LAYOUT_PROPERTIES
|
|
20481
|
+
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20549
20482
|
node: property,
|
|
20550
20483
|
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`
|
|
20551
20484
|
});
|
|
@@ -20735,134 +20668,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20735
20668
|
} })
|
|
20736
20669
|
});
|
|
20737
20670
|
//#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
|
|
20866
20671
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20867
20672
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20868
20673
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -21018,7 +20823,7 @@ const noMoment = defineRule({
|
|
|
21018
20823
|
});
|
|
21019
20824
|
//#endregion
|
|
21020
20825
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
21021
|
-
const MESSAGE$
|
|
20826
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
21022
20827
|
const resolveSettings$16 = (settings) => {
|
|
21023
20828
|
const reactDoctor = settings?.["react-doctor"];
|
|
21024
20829
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21340,7 +21145,7 @@ const noMultiComp = defineRule({
|
|
|
21340
21145
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21341
21146
|
for (const component of flagged.slice(1)) context.report({
|
|
21342
21147
|
node: component.reportNode,
|
|
21343
|
-
message: MESSAGE$
|
|
21148
|
+
message: MESSAGE$18
|
|
21344
21149
|
});
|
|
21345
21150
|
} };
|
|
21346
21151
|
}
|
|
@@ -21508,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21508
21313
|
};
|
|
21509
21314
|
//#endregion
|
|
21510
21315
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21511
|
-
const MESSAGE$
|
|
21316
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21512
21317
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21513
21318
|
"copyWithin",
|
|
21514
21319
|
"fill",
|
|
@@ -21718,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21718
21523
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21719
21524
|
context.report({
|
|
21720
21525
|
node: options.crossFileConsumerCallSite,
|
|
21721
|
-
message: `${MESSAGE$
|
|
21526
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21722
21527
|
});
|
|
21723
21528
|
return;
|
|
21724
21529
|
}
|
|
@@ -21727,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21727
21532
|
reportedNodes.add(mutation.node);
|
|
21728
21533
|
context.report({
|
|
21729
21534
|
node: mutation.node,
|
|
21730
|
-
message: MESSAGE$
|
|
21535
|
+
message: MESSAGE$17
|
|
21731
21536
|
});
|
|
21732
21537
|
}
|
|
21733
21538
|
};
|
|
@@ -21999,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21999
21804
|
});
|
|
22000
21805
|
//#endregion
|
|
22001
21806
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
22002
|
-
const MESSAGE$
|
|
21807
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
22003
21808
|
const resolveSettings$14 = (settings) => {
|
|
22004
21809
|
const reactDoctor = settings?.["react-doctor"];
|
|
22005
21810
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -22027,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22027
21832
|
if (numeric === null) {
|
|
22028
21833
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
22029
21834
|
node: tabIndex,
|
|
22030
|
-
message: MESSAGE$
|
|
21835
|
+
message: MESSAGE$16
|
|
22031
21836
|
});
|
|
22032
21837
|
return;
|
|
22033
21838
|
}
|
|
@@ -22040,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22040
21845
|
if (!roleAttribute) {
|
|
22041
21846
|
context.report({
|
|
22042
21847
|
node: tabIndex,
|
|
22043
|
-
message: MESSAGE$
|
|
21848
|
+
message: MESSAGE$16
|
|
22044
21849
|
});
|
|
22045
21850
|
return;
|
|
22046
21851
|
}
|
|
@@ -22054,12 +21859,20 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22054
21859
|
}
|
|
22055
21860
|
context.report({
|
|
22056
21861
|
node: tabIndex,
|
|
22057
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
22058
21863
|
});
|
|
22059
21864
|
} };
|
|
22060
21865
|
}
|
|
22061
21866
|
});
|
|
22062
21867
|
//#endregion
|
|
21868
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21869
|
+
const getStylePropertyNumberValue = (property) => {
|
|
21870
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
21871
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21872
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21873
|
+
return null;
|
|
21874
|
+
};
|
|
21875
|
+
//#endregion
|
|
22063
21876
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
22064
21877
|
const noOutlineNone = defineRule({
|
|
22065
21878
|
id: "no-outline-none",
|
|
@@ -22737,7 +22550,7 @@ const noRandomKey = defineRule({
|
|
|
22737
22550
|
});
|
|
22738
22551
|
//#endregion
|
|
22739
22552
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22740
|
-
const MESSAGE$
|
|
22553
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22741
22554
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22742
22555
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22743
22556
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22763,13 +22576,13 @@ const noReactChildren = defineRule({
|
|
|
22763
22576
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22764
22577
|
context.report({
|
|
22765
22578
|
node: calleeOuter,
|
|
22766
|
-
message: MESSAGE$
|
|
22579
|
+
message: MESSAGE$15
|
|
22767
22580
|
});
|
|
22768
22581
|
return;
|
|
22769
22582
|
}
|
|
22770
22583
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22771
22584
|
node: calleeOuter,
|
|
22772
|
-
message: MESSAGE$
|
|
22585
|
+
message: MESSAGE$15
|
|
22773
22586
|
});
|
|
22774
22587
|
} })
|
|
22775
22588
|
});
|
|
@@ -22880,86 +22693,6 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22880
22693
|
})
|
|
22881
22694
|
});
|
|
22882
22695
|
//#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
|
|
22963
22696
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22964
22697
|
const ELEMENT_ROLE_PAIRS = [
|
|
22965
22698
|
["a", "link"],
|
|
@@ -23172,7 +22905,7 @@ const noRenderPropChildren = defineRule({
|
|
|
23172
22905
|
});
|
|
23173
22906
|
//#endregion
|
|
23174
22907
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
23175
|
-
const MESSAGE$
|
|
22908
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
23176
22909
|
const isReactDomRenderCall = (node) => {
|
|
23177
22910
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
23178
22911
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -23196,7 +22929,7 @@ const noRenderReturnValue = defineRule({
|
|
|
23196
22929
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
23197
22930
|
context.report({
|
|
23198
22931
|
node: node.callee,
|
|
23199
|
-
message: MESSAGE$
|
|
22932
|
+
message: MESSAGE$14
|
|
23200
22933
|
});
|
|
23201
22934
|
} })
|
|
23202
22935
|
});
|
|
@@ -23894,7 +23627,7 @@ const getParentComponent = (node) => {
|
|
|
23894
23627
|
};
|
|
23895
23628
|
//#endregion
|
|
23896
23629
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23897
|
-
const MESSAGE$
|
|
23630
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23898
23631
|
const noSetState = defineRule({
|
|
23899
23632
|
id: "no-set-state",
|
|
23900
23633
|
title: "Local class state forbidden",
|
|
@@ -23909,7 +23642,7 @@ const noSetState = defineRule({
|
|
|
23909
23642
|
if (!getParentComponent(node)) return;
|
|
23910
23643
|
context.report({
|
|
23911
23644
|
node: node.callee,
|
|
23912
|
-
message: MESSAGE$
|
|
23645
|
+
message: MESSAGE$13
|
|
23913
23646
|
});
|
|
23914
23647
|
} })
|
|
23915
23648
|
});
|
|
@@ -24071,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
24071
23804
|
};
|
|
24072
23805
|
//#endregion
|
|
24073
23806
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
24074
|
-
const MESSAGE$
|
|
23807
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
24075
23808
|
const DEFAULT_HANDLERS = [
|
|
24076
23809
|
"onClick",
|
|
24077
23810
|
"onMouseDown",
|
|
@@ -24131,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
24131
23864
|
if (!roleAttribute || !roleAttribute.value) {
|
|
24132
23865
|
context.report({
|
|
24133
23866
|
node: node.name,
|
|
24134
|
-
message: MESSAGE$
|
|
23867
|
+
message: MESSAGE$12
|
|
24135
23868
|
});
|
|
24136
23869
|
return;
|
|
24137
23870
|
}
|
|
@@ -24141,14 +23874,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
24141
23874
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
24142
23875
|
context.report({
|
|
24143
23876
|
node: node.name,
|
|
24144
|
-
message: MESSAGE$
|
|
23877
|
+
message: MESSAGE$12
|
|
24145
23878
|
});
|
|
24146
23879
|
return;
|
|
24147
23880
|
}
|
|
24148
23881
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
24149
23882
|
context.report({
|
|
24150
23883
|
node: node.name,
|
|
24151
|
-
message: MESSAGE$
|
|
23884
|
+
message: MESSAGE$12
|
|
24152
23885
|
});
|
|
24153
23886
|
} };
|
|
24154
23887
|
}
|
|
@@ -24251,43 +23984,8 @@ const noStringRefs = defineRule({
|
|
|
24251
23984
|
}
|
|
24252
23985
|
});
|
|
24253
23986
|
//#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
23987
|
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24290
|
-
const MESSAGE$
|
|
23988
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
24291
23989
|
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24292
23990
|
const noSyncXhr = defineRule({
|
|
24293
23991
|
id: "no-sync-xhr",
|
|
@@ -24302,103 +24000,13 @@ const noSyncXhr = defineRule({
|
|
|
24302
24000
|
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24303
24001
|
context.report({
|
|
24304
24002
|
node,
|
|
24305
|
-
message: MESSAGE$
|
|
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
|
|
24003
|
+
message: MESSAGE$11
|
|
24396
24004
|
});
|
|
24397
24005
|
} })
|
|
24398
24006
|
});
|
|
24399
24007
|
//#endregion
|
|
24400
24008
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
24401
|
-
const MESSAGE$
|
|
24009
|
+
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
24402
24010
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
24403
24011
|
let ancestor = node.parent;
|
|
24404
24012
|
while (ancestor) {
|
|
@@ -24467,7 +24075,7 @@ const noThisInSfc = defineRule({
|
|
|
24467
24075
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
24468
24076
|
context.report({
|
|
24469
24077
|
node,
|
|
24470
|
-
message: MESSAGE$
|
|
24078
|
+
message: MESSAGE$10
|
|
24471
24079
|
});
|
|
24472
24080
|
} };
|
|
24473
24081
|
}
|
|
@@ -24505,39 +24113,26 @@ const noTinyText = defineRule({
|
|
|
24505
24113
|
});
|
|
24506
24114
|
//#endregion
|
|
24507
24115
|
//#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`.";
|
|
24510
24116
|
const noTransitionAll = defineRule({
|
|
24511
24117
|
id: "no-transition-all",
|
|
24512
24118
|
title: "transition: all animates everything",
|
|
24513
24119
|
tags: ["test-noise"],
|
|
24514
24120
|
severity: "warn",
|
|
24515
24121
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
24516
|
-
create: (context) => ({
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
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
|
|
24122
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
24123
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24124
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24125
|
+
const expression = node.value.expression;
|
|
24126
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24127
|
+
for (const property of expression.properties ?? []) {
|
|
24128
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24129
|
+
if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
|
|
24130
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
|
|
24131
|
+
node: property,
|
|
24132
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24538
24133
|
});
|
|
24539
24134
|
}
|
|
24540
|
-
})
|
|
24135
|
+
} })
|
|
24541
24136
|
});
|
|
24542
24137
|
//#endregion
|
|
24543
24138
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -24581,6 +24176,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
24581
24176
|
}
|
|
24582
24177
|
return stateNames;
|
|
24583
24178
|
};
|
|
24179
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
24584
24180
|
const noUncontrolledInput = defineRule({
|
|
24585
24181
|
id: "no-uncontrolled-input",
|
|
24586
24182
|
title: "Uncontrolled input value",
|
|
@@ -24684,38 +24280,6 @@ const noUnescapedEntities = defineRule({
|
|
|
24684
24280
|
} })
|
|
24685
24281
|
});
|
|
24686
24282
|
//#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
|
|
24719
24283
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24720
24284
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24721
24285
|
"activedescendant",
|
|
@@ -26187,7 +25751,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
26187
25751
|
//#endregion
|
|
26188
25752
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
26189
25753
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
26190
|
-
const MESSAGE$
|
|
25754
|
+
const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
26191
25755
|
const resolveSettings$7 = (settings) => {
|
|
26192
25756
|
const reactDoctor = settings?.["react-doctor"];
|
|
26193
25757
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -26221,7 +25785,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
26221
25785
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
26222
25786
|
context.report({
|
|
26223
25787
|
node: node.callee,
|
|
26224
|
-
message: MESSAGE$
|
|
25788
|
+
message: MESSAGE$9
|
|
26225
25789
|
});
|
|
26226
25790
|
} };
|
|
26227
25791
|
}
|
|
@@ -27099,7 +26663,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
27099
26663
|
});
|
|
27100
26664
|
//#endregion
|
|
27101
26665
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
27102
|
-
const MESSAGE$
|
|
26666
|
+
const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
27103
26667
|
const preactPreferOndblclick = defineRule({
|
|
27104
26668
|
id: "preact-prefer-ondblclick",
|
|
27105
26669
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -27114,7 +26678,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
27114
26678
|
if (!onDoubleClickAttribute) return;
|
|
27115
26679
|
context.report({
|
|
27116
26680
|
node: onDoubleClickAttribute,
|
|
27117
|
-
message: MESSAGE$
|
|
26681
|
+
message: MESSAGE$8
|
|
27118
26682
|
});
|
|
27119
26683
|
} })
|
|
27120
26684
|
});
|
|
@@ -27154,42 +26718,6 @@ const preactPreferOninput = defineRule({
|
|
|
27154
26718
|
} })
|
|
27155
26719
|
});
|
|
27156
26720
|
//#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
|
|
27193
26721
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
27194
26722
|
const preferDynamicImport = defineRule({
|
|
27195
26723
|
id: "prefer-dynamic-import",
|
|
@@ -27781,26 +27309,6 @@ const preferTagOverRole = defineRule({
|
|
|
27781
27309
|
} })
|
|
27782
27310
|
});
|
|
27783
27311
|
//#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
|
|
27804
27312
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27805
27313
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27806
27314
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -39475,17 +38983,6 @@ const reactDoctorRules = [
|
|
|
39475
38983
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
39476
38984
|
}
|
|
39477
38985
|
},
|
|
39478
|
-
{
|
|
39479
|
-
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39480
|
-
id: "no-arbitrary-px-font-size",
|
|
39481
|
-
source: "react-doctor",
|
|
39482
|
-
originallyExternal: false,
|
|
39483
|
-
rule: {
|
|
39484
|
-
...noArbitraryPxFontSize,
|
|
39485
|
-
framework: "global",
|
|
39486
|
-
category: "Accessibility"
|
|
39487
|
-
}
|
|
39488
|
-
},
|
|
39489
38986
|
{
|
|
39490
38987
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
39491
38988
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -39545,18 +39042,6 @@ const reactDoctorRules = [
|
|
|
39545
39042
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
39546
39043
|
}
|
|
39547
39044
|
},
|
|
39548
|
-
{
|
|
39549
|
-
key: "react-doctor/no-autoplay-without-muted",
|
|
39550
|
-
id: "no-autoplay-without-muted",
|
|
39551
|
-
source: "react-doctor",
|
|
39552
|
-
originallyExternal: false,
|
|
39553
|
-
rule: {
|
|
39554
|
-
...noAutoplayWithoutMuted,
|
|
39555
|
-
framework: "global",
|
|
39556
|
-
category: "Accessibility",
|
|
39557
|
-
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39558
|
-
}
|
|
39559
|
-
},
|
|
39560
39045
|
{
|
|
39561
39046
|
key: "react-doctor/no-barrel-import",
|
|
39562
39047
|
id: "no-barrel-import",
|
|
@@ -39710,17 +39195,6 @@ const reactDoctorRules = [
|
|
|
39710
39195
|
category: "Maintainability"
|
|
39711
39196
|
}
|
|
39712
39197
|
},
|
|
39713
|
-
{
|
|
39714
|
-
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39715
|
-
id: "no-deprecated-tailwind-class",
|
|
39716
|
-
source: "react-doctor",
|
|
39717
|
-
originallyExternal: false,
|
|
39718
|
-
rule: {
|
|
39719
|
-
...noDeprecatedTailwindClass,
|
|
39720
|
-
framework: "global",
|
|
39721
|
-
category: "Maintainability"
|
|
39722
|
-
}
|
|
39723
|
-
},
|
|
39724
39198
|
{
|
|
39725
39199
|
key: "react-doctor/no-derived-state",
|
|
39726
39200
|
id: "no-derived-state",
|
|
@@ -39992,17 +39466,6 @@ const reactDoctorRules = [
|
|
|
39992
39466
|
category: "Performance"
|
|
39993
39467
|
}
|
|
39994
39468
|
},
|
|
39995
|
-
{
|
|
39996
|
-
key: "react-doctor/no-full-viewport-width",
|
|
39997
|
-
id: "no-full-viewport-width",
|
|
39998
|
-
source: "react-doctor",
|
|
39999
|
-
originallyExternal: false,
|
|
40000
|
-
rule: {
|
|
40001
|
-
...noFullViewportWidth,
|
|
40002
|
-
framework: "global",
|
|
40003
|
-
category: "Maintainability"
|
|
40004
|
-
}
|
|
40005
|
-
},
|
|
40006
39469
|
{
|
|
40007
39470
|
key: "react-doctor/no-generic-handler-names",
|
|
40008
39471
|
id: "no-generic-handler-names",
|
|
@@ -40242,17 +39705,6 @@ const reactDoctorRules = [
|
|
|
40242
39705
|
category: "Performance"
|
|
40243
39706
|
}
|
|
40244
39707
|
},
|
|
40245
|
-
{
|
|
40246
|
-
key: "react-doctor/no-low-contrast-inline-style",
|
|
40247
|
-
id: "no-low-contrast-inline-style",
|
|
40248
|
-
source: "react-doctor",
|
|
40249
|
-
originallyExternal: false,
|
|
40250
|
-
rule: {
|
|
40251
|
-
...noLowContrastInlineStyle,
|
|
40252
|
-
framework: "global",
|
|
40253
|
-
category: "Accessibility"
|
|
40254
|
-
}
|
|
40255
|
-
},
|
|
40256
39708
|
{
|
|
40257
39709
|
key: "react-doctor/no-many-boolean-props",
|
|
40258
39710
|
id: "no-many-boolean-props",
|
|
@@ -40530,17 +39982,6 @@ const reactDoctorRules = [
|
|
|
40530
39982
|
category: "Maintainability"
|
|
40531
39983
|
}
|
|
40532
39984
|
},
|
|
40533
|
-
{
|
|
40534
|
-
key: "react-doctor/no-redundant-display-class",
|
|
40535
|
-
id: "no-redundant-display-class",
|
|
40536
|
-
source: "react-doctor",
|
|
40537
|
-
originallyExternal: false,
|
|
40538
|
-
rule: {
|
|
40539
|
-
...noRedundantDisplayClass,
|
|
40540
|
-
framework: "global",
|
|
40541
|
-
category: "Maintainability"
|
|
40542
|
-
}
|
|
40543
|
-
},
|
|
40544
39985
|
{
|
|
40545
39986
|
key: "react-doctor/no-redundant-roles",
|
|
40546
39987
|
id: "no-redundant-roles",
|
|
@@ -40717,17 +40158,6 @@ const reactDoctorRules = [
|
|
|
40717
40158
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
40718
40159
|
}
|
|
40719
40160
|
},
|
|
40720
|
-
{
|
|
40721
|
-
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40722
|
-
id: "no-svg-currentcolor-with-fill-class",
|
|
40723
|
-
source: "react-doctor",
|
|
40724
|
-
originallyExternal: false,
|
|
40725
|
-
rule: {
|
|
40726
|
-
...noSvgCurrentcolorWithFillClass,
|
|
40727
|
-
framework: "global",
|
|
40728
|
-
category: "Maintainability"
|
|
40729
|
-
}
|
|
40730
|
-
},
|
|
40731
40161
|
{
|
|
40732
40162
|
key: "react-doctor/no-sync-xhr",
|
|
40733
40163
|
id: "no-sync-xhr",
|
|
@@ -40739,29 +40169,6 @@ const reactDoctorRules = [
|
|
|
40739
40169
|
category: "Performance"
|
|
40740
40170
|
}
|
|
40741
40171
|
},
|
|
40742
|
-
{
|
|
40743
|
-
key: "react-doctor/no-tailwind-layout-transition",
|
|
40744
|
-
id: "no-tailwind-layout-transition",
|
|
40745
|
-
source: "react-doctor",
|
|
40746
|
-
originallyExternal: false,
|
|
40747
|
-
rule: {
|
|
40748
|
-
...noTailwindLayoutTransition,
|
|
40749
|
-
framework: "global",
|
|
40750
|
-
category: "Performance"
|
|
40751
|
-
}
|
|
40752
|
-
},
|
|
40753
|
-
{
|
|
40754
|
-
key: "react-doctor/no-target-blank-without-rel",
|
|
40755
|
-
id: "no-target-blank-without-rel",
|
|
40756
|
-
source: "react-doctor",
|
|
40757
|
-
originallyExternal: false,
|
|
40758
|
-
rule: {
|
|
40759
|
-
...noTargetBlankWithoutRel,
|
|
40760
|
-
framework: "global",
|
|
40761
|
-
category: "Accessibility",
|
|
40762
|
-
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40763
|
-
}
|
|
40764
|
-
},
|
|
40765
40172
|
{
|
|
40766
40173
|
key: "react-doctor/no-this-in-sfc",
|
|
40767
40174
|
id: "no-this-in-sfc",
|
|
@@ -40831,18 +40238,6 @@ const reactDoctorRules = [
|
|
|
40831
40238
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
40832
40239
|
}
|
|
40833
40240
|
},
|
|
40834
|
-
{
|
|
40835
|
-
key: "react-doctor/no-uninformative-aria-label",
|
|
40836
|
-
id: "no-uninformative-aria-label",
|
|
40837
|
-
source: "react-doctor",
|
|
40838
|
-
originallyExternal: false,
|
|
40839
|
-
rule: {
|
|
40840
|
-
...noUninformativeAriaLabel,
|
|
40841
|
-
framework: "global",
|
|
40842
|
-
category: "Accessibility",
|
|
40843
|
-
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40844
|
-
}
|
|
40845
|
-
},
|
|
40846
40241
|
{
|
|
40847
40242
|
key: "react-doctor/no-unknown-property",
|
|
40848
40243
|
id: "no-unknown-property",
|
|
@@ -41052,17 +40447,6 @@ const reactDoctorRules = [
|
|
|
41052
40447
|
category: "Bugs"
|
|
41053
40448
|
}
|
|
41054
40449
|
},
|
|
41055
|
-
{
|
|
41056
|
-
key: "react-doctor/prefer-dvh-over-vh",
|
|
41057
|
-
id: "prefer-dvh-over-vh",
|
|
41058
|
-
source: "react-doctor",
|
|
41059
|
-
originallyExternal: false,
|
|
41060
|
-
rule: {
|
|
41061
|
-
...preferDvhOverVh,
|
|
41062
|
-
framework: "global",
|
|
41063
|
-
category: "Maintainability"
|
|
41064
|
-
}
|
|
41065
|
-
},
|
|
41066
40450
|
{
|
|
41067
40451
|
key: "react-doctor/prefer-dynamic-import",
|
|
41068
40452
|
id: "prefer-dynamic-import",
|
|
@@ -41167,17 +40551,6 @@ const reactDoctorRules = [
|
|
|
41167
40551
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
41168
40552
|
}
|
|
41169
40553
|
},
|
|
41170
|
-
{
|
|
41171
|
-
key: "react-doctor/prefer-truncate-shorthand",
|
|
41172
|
-
id: "prefer-truncate-shorthand",
|
|
41173
|
-
source: "react-doctor",
|
|
41174
|
-
originallyExternal: false,
|
|
41175
|
-
rule: {
|
|
41176
|
-
...preferTruncateShorthand,
|
|
41177
|
-
framework: "global",
|
|
41178
|
-
category: "Maintainability"
|
|
41179
|
-
}
|
|
41180
|
-
},
|
|
41181
40554
|
{
|
|
41182
40555
|
key: "react-doctor/prefer-use-effect-event",
|
|
41183
40556
|
id: "prefer-use-effect-event",
|