oxlint-plugin-react-doctor 0.5.6-dev.0a7edbd → 0.5.6-dev.424d8f9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +635 -5
- package/dist/index.js +1036 -179
- 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$64 = "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$64
|
|
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$63 = "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$63
|
|
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$63
|
|
2300
2354
|
});
|
|
2301
2355
|
} })
|
|
2302
2356
|
});
|
|
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3139
|
scan: scanByPattern({
|
|
3086
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3141
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3142
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3144
|
})
|
|
3091
3145
|
});
|
|
@@ -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$62 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4276
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$62
|
|
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$62
|
|
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$61 = "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$61
|
|
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$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4845
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$60
|
|
5006
5060
|
});
|
|
5007
5061
|
} };
|
|
5008
5062
|
}
|
|
@@ -5131,6 +5185,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
5131
5185
|
return findings;
|
|
5132
5186
|
}
|
|
5133
5187
|
});
|
|
5188
|
+
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5134
5189
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5135
5190
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5136
5191
|
"continue",
|
|
@@ -5429,10 +5484,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5429
5484
|
});
|
|
5430
5485
|
//#endregion
|
|
5431
5486
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5432
|
-
const hasJsxSpreadAttribute
|
|
5487
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5433
5488
|
//#endregion
|
|
5434
5489
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5435
|
-
const MESSAGE$
|
|
5490
|
+
const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5436
5491
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5437
5492
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5438
5493
|
"aria-label",
|
|
@@ -5451,11 +5506,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5451
5506
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5452
5507
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5453
5508
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5454
|
-
if (hasJsxSpreadAttribute
|
|
5509
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5455
5510
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5456
5511
|
context.report({
|
|
5457
5512
|
node: node.name,
|
|
5458
|
-
message: MESSAGE$
|
|
5513
|
+
message: MESSAGE$59
|
|
5459
5514
|
});
|
|
5460
5515
|
} })
|
|
5461
5516
|
});
|
|
@@ -5494,7 +5549,7 @@ const isEs6Component = (node) => {
|
|
|
5494
5549
|
};
|
|
5495
5550
|
//#endregion
|
|
5496
5551
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5497
|
-
const MESSAGE$
|
|
5552
|
+
const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5498
5553
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5499
5554
|
"observer",
|
|
5500
5555
|
"lazy",
|
|
@@ -5697,7 +5752,7 @@ const displayName = defineRule({
|
|
|
5697
5752
|
const reportAt = (node) => {
|
|
5698
5753
|
context.report({
|
|
5699
5754
|
node,
|
|
5700
|
-
message: MESSAGE$
|
|
5755
|
+
message: MESSAGE$58
|
|
5701
5756
|
});
|
|
5702
5757
|
};
|
|
5703
5758
|
return {
|
|
@@ -7845,7 +7900,7 @@ const forbidElements = defineRule({
|
|
|
7845
7900
|
});
|
|
7846
7901
|
//#endregion
|
|
7847
7902
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7848
|
-
const MESSAGE$
|
|
7903
|
+
const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7849
7904
|
const forwardRefUsesRef = defineRule({
|
|
7850
7905
|
id: "forward-ref-uses-ref",
|
|
7851
7906
|
title: "forwardRef without ref parameter",
|
|
@@ -7865,7 +7920,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7865
7920
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7866
7921
|
context.report({
|
|
7867
7922
|
node: inner,
|
|
7868
|
-
message: MESSAGE$
|
|
7923
|
+
message: MESSAGE$57
|
|
7869
7924
|
});
|
|
7870
7925
|
} })
|
|
7871
7926
|
});
|
|
@@ -7902,7 +7957,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7902
7957
|
});
|
|
7903
7958
|
//#endregion
|
|
7904
7959
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7905
|
-
const MESSAGE$
|
|
7960
|
+
const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7906
7961
|
const DEFAULT_HEADING_TAGS = [
|
|
7907
7962
|
"h1",
|
|
7908
7963
|
"h2",
|
|
@@ -7935,7 +7990,7 @@ const headingHasContent = defineRule({
|
|
|
7935
7990
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7936
7991
|
context.report({
|
|
7937
7992
|
node,
|
|
7938
|
-
message: MESSAGE$
|
|
7993
|
+
message: MESSAGE$56
|
|
7939
7994
|
});
|
|
7940
7995
|
} };
|
|
7941
7996
|
}
|
|
@@ -8073,7 +8128,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8073
8128
|
});
|
|
8074
8129
|
//#endregion
|
|
8075
8130
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8076
|
-
const MESSAGE$
|
|
8131
|
+
const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8077
8132
|
const resolveSettings$38 = (settings) => {
|
|
8078
8133
|
const reactDoctor = settings?.["react-doctor"];
|
|
8079
8134
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8121,7 +8176,7 @@ const htmlHasLang = defineRule({
|
|
|
8121
8176
|
if (!lang) {
|
|
8122
8177
|
context.report({
|
|
8123
8178
|
node: node.name,
|
|
8124
|
-
message: MESSAGE$
|
|
8179
|
+
message: MESSAGE$55
|
|
8125
8180
|
});
|
|
8126
8181
|
return;
|
|
8127
8182
|
}
|
|
@@ -8129,13 +8184,13 @@ const htmlHasLang = defineRule({
|
|
|
8129
8184
|
if (verdict === "missing" || verdict === "empty") {
|
|
8130
8185
|
context.report({
|
|
8131
8186
|
node: lang,
|
|
8132
|
-
message: MESSAGE$
|
|
8187
|
+
message: MESSAGE$55
|
|
8133
8188
|
});
|
|
8134
8189
|
return;
|
|
8135
8190
|
}
|
|
8136
8191
|
if (hasSpread && !lang) context.report({
|
|
8137
8192
|
node: node.name,
|
|
8138
|
-
message: MESSAGE$
|
|
8193
|
+
message: MESSAGE$55
|
|
8139
8194
|
});
|
|
8140
8195
|
} };
|
|
8141
8196
|
}
|
|
@@ -8349,7 +8404,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8349
8404
|
});
|
|
8350
8405
|
//#endregion
|
|
8351
8406
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8352
|
-
const MESSAGE$
|
|
8407
|
+
const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8353
8408
|
const evaluateTitleValue = (value) => {
|
|
8354
8409
|
if (!value) return "missing";
|
|
8355
8410
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8389,14 +8444,14 @@ const iframeHasTitle = defineRule({
|
|
|
8389
8444
|
if (!titleAttr) {
|
|
8390
8445
|
if (hasSpread || tag === "iframe") context.report({
|
|
8391
8446
|
node: node.name,
|
|
8392
|
-
message: MESSAGE$
|
|
8447
|
+
message: MESSAGE$54
|
|
8393
8448
|
});
|
|
8394
8449
|
return;
|
|
8395
8450
|
}
|
|
8396
8451
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8397
8452
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8398
8453
|
node: titleAttr,
|
|
8399
|
-
message: MESSAGE$
|
|
8454
|
+
message: MESSAGE$54
|
|
8400
8455
|
});
|
|
8401
8456
|
} })
|
|
8402
8457
|
});
|
|
@@ -8500,7 +8555,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8500
8555
|
});
|
|
8501
8556
|
//#endregion
|
|
8502
8557
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8503
|
-
const MESSAGE$
|
|
8558
|
+
const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8504
8559
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8505
8560
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8506
8561
|
"image",
|
|
@@ -8565,7 +8620,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8565
8620
|
if (!altAttribute) return;
|
|
8566
8621
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8567
8622
|
node: altAttribute,
|
|
8568
|
-
message: MESSAGE$
|
|
8623
|
+
message: MESSAGE$53
|
|
8569
8624
|
});
|
|
8570
8625
|
} };
|
|
8571
8626
|
}
|
|
@@ -10922,7 +10977,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10922
10977
|
});
|
|
10923
10978
|
//#endregion
|
|
10924
10979
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10925
|
-
const MESSAGE$
|
|
10980
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10926
10981
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10927
10982
|
"code",
|
|
10928
10983
|
"pre",
|
|
@@ -10958,7 +11013,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10958
11013
|
if (isInsideLiteralTextTag(node)) return;
|
|
10959
11014
|
context.report({
|
|
10960
11015
|
node,
|
|
10961
|
-
message: MESSAGE$
|
|
11016
|
+
message: MESSAGE$52
|
|
10962
11017
|
});
|
|
10963
11018
|
} })
|
|
10964
11019
|
});
|
|
@@ -10989,7 +11044,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10989
11044
|
};
|
|
10990
11045
|
//#endregion
|
|
10991
11046
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10992
|
-
const MESSAGE$
|
|
11047
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10993
11048
|
const CONTEXT_MODULES$1 = [
|
|
10994
11049
|
"react",
|
|
10995
11050
|
"use-context-selector",
|
|
@@ -11087,7 +11142,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11087
11142
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11088
11143
|
context.report({
|
|
11089
11144
|
node: attribute,
|
|
11090
|
-
message: MESSAGE$
|
|
11145
|
+
message: MESSAGE$51
|
|
11091
11146
|
});
|
|
11092
11147
|
}
|
|
11093
11148
|
}
|
|
@@ -11173,7 +11228,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11173
11228
|
};
|
|
11174
11229
|
//#endregion
|
|
11175
11230
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11176
|
-
const MESSAGE$
|
|
11231
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11177
11232
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11178
11233
|
"icon",
|
|
11179
11234
|
"Icon",
|
|
@@ -11442,7 +11497,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11442
11497
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11443
11498
|
context.report({
|
|
11444
11499
|
node,
|
|
11445
|
-
message: MESSAGE$
|
|
11500
|
+
message: MESSAGE$50
|
|
11446
11501
|
});
|
|
11447
11502
|
}
|
|
11448
11503
|
};
|
|
@@ -11730,7 +11785,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11730
11785
|
];
|
|
11731
11786
|
//#endregion
|
|
11732
11787
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11733
|
-
const MESSAGE$
|
|
11788
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11734
11789
|
const isDataArrayPropName = (propName) => {
|
|
11735
11790
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11736
11791
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11814,7 +11869,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11814
11869
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11815
11870
|
context.report({
|
|
11816
11871
|
node,
|
|
11817
|
-
message: MESSAGE$
|
|
11872
|
+
message: MESSAGE$49
|
|
11818
11873
|
});
|
|
11819
11874
|
}
|
|
11820
11875
|
};
|
|
@@ -12072,7 +12127,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12072
12127
|
]);
|
|
12073
12128
|
//#endregion
|
|
12074
12129
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12075
|
-
const MESSAGE$
|
|
12130
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12076
12131
|
const isAccessorPredicateName = (propName) => {
|
|
12077
12132
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12078
12133
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12278,7 +12333,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12278
12333
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12279
12334
|
context.report({
|
|
12280
12335
|
node,
|
|
12281
|
-
message: MESSAGE$
|
|
12336
|
+
message: MESSAGE$48
|
|
12282
12337
|
});
|
|
12283
12338
|
}
|
|
12284
12339
|
};
|
|
@@ -12498,7 +12553,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12498
12553
|
];
|
|
12499
12554
|
//#endregion
|
|
12500
12555
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12501
|
-
const MESSAGE$
|
|
12556
|
+
const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12502
12557
|
const isConfigObjectPropName = (propName) => {
|
|
12503
12558
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12504
12559
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12586,7 +12641,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12586
12641
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12587
12642
|
context.report({
|
|
12588
12643
|
node,
|
|
12589
|
-
message: MESSAGE$
|
|
12644
|
+
message: MESSAGE$47
|
|
12590
12645
|
});
|
|
12591
12646
|
}
|
|
12592
12647
|
};
|
|
@@ -12594,7 +12649,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12594
12649
|
});
|
|
12595
12650
|
//#endregion
|
|
12596
12651
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12597
|
-
const MESSAGE$
|
|
12652
|
+
const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12598
12653
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12599
12654
|
const resolveSettings$28 = (settings) => {
|
|
12600
12655
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12635,7 +12690,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12635
12690
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12636
12691
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12637
12692
|
node: attribute,
|
|
12638
|
-
message: MESSAGE$
|
|
12693
|
+
message: MESSAGE$46
|
|
12639
12694
|
});
|
|
12640
12695
|
}
|
|
12641
12696
|
} };
|
|
@@ -12950,7 +13005,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12950
13005
|
});
|
|
12951
13006
|
//#endregion
|
|
12952
13007
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12953
|
-
const MESSAGE$
|
|
13008
|
+
const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
|
|
12954
13009
|
const resolveSettings$25 = (settings) => {
|
|
12955
13010
|
const reactDoctor = settings?.["react-doctor"];
|
|
12956
13011
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12991,7 +13046,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12991
13046
|
}
|
|
12992
13047
|
context.report({
|
|
12993
13048
|
node: attribute,
|
|
12994
|
-
message: MESSAGE$
|
|
13049
|
+
message: MESSAGE$45
|
|
12995
13050
|
});
|
|
12996
13051
|
}
|
|
12997
13052
|
} };
|
|
@@ -13219,7 +13274,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13219
13274
|
});
|
|
13220
13275
|
//#endregion
|
|
13221
13276
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13222
|
-
const MESSAGE$
|
|
13277
|
+
const MESSAGE$44 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13223
13278
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13224
13279
|
"aa",
|
|
13225
13280
|
"ab",
|
|
@@ -13431,7 +13486,7 @@ const lang = defineRule({
|
|
|
13431
13486
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13432
13487
|
context.report({
|
|
13433
13488
|
node: langAttr,
|
|
13434
|
-
message: MESSAGE$
|
|
13489
|
+
message: MESSAGE$44
|
|
13435
13490
|
});
|
|
13436
13491
|
return;
|
|
13437
13492
|
}
|
|
@@ -13440,7 +13495,7 @@ const lang = defineRule({
|
|
|
13440
13495
|
if (value === null) return;
|
|
13441
13496
|
if (!isValidLangTag(value)) context.report({
|
|
13442
13497
|
node: langAttr,
|
|
13443
|
-
message: MESSAGE$
|
|
13498
|
+
message: MESSAGE$44
|
|
13444
13499
|
});
|
|
13445
13500
|
} })
|
|
13446
13501
|
});
|
|
@@ -13466,6 +13521,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13466
13521
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13467
13522
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13468
13523
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13524
|
+
ignoreStringLiterals: true,
|
|
13469
13525
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13470
13526
|
})
|
|
13471
13527
|
});
|
|
@@ -13484,7 +13540,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13484
13540
|
});
|
|
13485
13541
|
//#endregion
|
|
13486
13542
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13487
|
-
const MESSAGE$
|
|
13543
|
+
const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13488
13544
|
const DEFAULT_AUDIO = ["audio"];
|
|
13489
13545
|
const DEFAULT_VIDEO = ["video"];
|
|
13490
13546
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13525,7 +13581,7 @@ const mediaHasCaption = defineRule({
|
|
|
13525
13581
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13526
13582
|
context.report({
|
|
13527
13583
|
node: node.name,
|
|
13528
|
-
message: MESSAGE$
|
|
13584
|
+
message: MESSAGE$43
|
|
13529
13585
|
});
|
|
13530
13586
|
return;
|
|
13531
13587
|
}
|
|
@@ -13542,7 +13598,7 @@ const mediaHasCaption = defineRule({
|
|
|
13542
13598
|
return kindValue.value.toLowerCase() === "captions";
|
|
13543
13599
|
})) context.report({
|
|
13544
13600
|
node: node.name,
|
|
13545
|
-
message: MESSAGE$
|
|
13601
|
+
message: MESSAGE$43
|
|
13546
13602
|
});
|
|
13547
13603
|
} };
|
|
13548
13604
|
}
|
|
@@ -15343,7 +15399,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15343
15399
|
});
|
|
15344
15400
|
//#endregion
|
|
15345
15401
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15346
|
-
const MESSAGE$
|
|
15402
|
+
const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15347
15403
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15348
15404
|
const noAccessKey = defineRule({
|
|
15349
15405
|
id: "no-access-key",
|
|
@@ -15360,7 +15416,7 @@ const noAccessKey = defineRule({
|
|
|
15360
15416
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15361
15417
|
context.report({
|
|
15362
15418
|
node: accessKey,
|
|
15363
|
-
message: MESSAGE$
|
|
15419
|
+
message: MESSAGE$42
|
|
15364
15420
|
});
|
|
15365
15421
|
return;
|
|
15366
15422
|
}
|
|
@@ -15370,7 +15426,7 @@ const noAccessKey = defineRule({
|
|
|
15370
15426
|
if (isUndefinedIdentifier(expression)) return;
|
|
15371
15427
|
context.report({
|
|
15372
15428
|
node: accessKey,
|
|
15373
|
-
message: MESSAGE$
|
|
15429
|
+
message: MESSAGE$42
|
|
15374
15430
|
});
|
|
15375
15431
|
}
|
|
15376
15432
|
} })
|
|
@@ -15852,8 +15908,41 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15852
15908
|
} })
|
|
15853
15909
|
});
|
|
15854
15910
|
//#endregion
|
|
15911
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15912
|
+
const getStringFromClassNameAttr = (node) => {
|
|
15913
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15914
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15915
|
+
if (!classAttr?.value) return null;
|
|
15916
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15917
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15918
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15919
|
+
return null;
|
|
15920
|
+
};
|
|
15921
|
+
//#endregion
|
|
15922
|
+
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15923
|
+
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15924
|
+
const noArbitraryPxFontSize = defineRule({
|
|
15925
|
+
id: "no-arbitrary-px-font-size",
|
|
15926
|
+
title: "Pixel arbitrary font size",
|
|
15927
|
+
tags: ["design", "test-noise"],
|
|
15928
|
+
severity: "warn",
|
|
15929
|
+
category: "Accessibility",
|
|
15930
|
+
recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
|
|
15931
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
15932
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
15933
|
+
if (!classNameValue) return;
|
|
15934
|
+
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15935
|
+
const rem = parseFloat(match[1]) / 16;
|
|
15936
|
+
context.report({
|
|
15937
|
+
node,
|
|
15938
|
+
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15939
|
+
});
|
|
15940
|
+
}
|
|
15941
|
+
} })
|
|
15942
|
+
});
|
|
15943
|
+
//#endregion
|
|
15855
15944
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15856
|
-
const MESSAGE$
|
|
15945
|
+
const MESSAGE$41 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15857
15946
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15858
15947
|
id: "no-aria-hidden-on-focusable",
|
|
15859
15948
|
title: "aria-hidden on focusable element",
|
|
@@ -15880,7 +15969,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15880
15969
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15881
15970
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15882
15971
|
node: ariaHidden,
|
|
15883
|
-
message: MESSAGE$
|
|
15972
|
+
message: MESSAGE$41
|
|
15884
15973
|
});
|
|
15885
15974
|
} })
|
|
15886
15975
|
});
|
|
@@ -16248,7 +16337,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16248
16337
|
});
|
|
16249
16338
|
//#endregion
|
|
16250
16339
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16251
|
-
const MESSAGE$
|
|
16340
|
+
const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16252
16341
|
const SECOND_INDEX_METHODS = new Set([
|
|
16253
16342
|
"every",
|
|
16254
16343
|
"filter",
|
|
@@ -16452,7 +16541,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16452
16541
|
}
|
|
16453
16542
|
context.report({
|
|
16454
16543
|
node: keyAttribute,
|
|
16455
|
-
message: MESSAGE$
|
|
16544
|
+
message: MESSAGE$40
|
|
16456
16545
|
});
|
|
16457
16546
|
},
|
|
16458
16547
|
CallExpression(node) {
|
|
@@ -16472,7 +16561,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16472
16561
|
if (propName !== "key") continue;
|
|
16473
16562
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16474
16563
|
node: property,
|
|
16475
|
-
message: MESSAGE$
|
|
16564
|
+
message: MESSAGE$40
|
|
16476
16565
|
});
|
|
16477
16566
|
}
|
|
16478
16567
|
}
|
|
@@ -16480,7 +16569,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16480
16569
|
});
|
|
16481
16570
|
//#endregion
|
|
16482
16571
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16483
|
-
const MESSAGE$
|
|
16572
|
+
const MESSAGE$39 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16484
16573
|
const noAsyncEffectCallback = defineRule({
|
|
16485
16574
|
id: "no-async-effect-callback",
|
|
16486
16575
|
title: "Async effect callback",
|
|
@@ -16494,13 +16583,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16494
16583
|
if (!callback.async) return;
|
|
16495
16584
|
context.report({
|
|
16496
16585
|
node: callback,
|
|
16497
|
-
message: MESSAGE$
|
|
16586
|
+
message: MESSAGE$39
|
|
16498
16587
|
});
|
|
16499
16588
|
} })
|
|
16500
16589
|
});
|
|
16501
16590
|
//#endregion
|
|
16502
16591
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16503
|
-
const MESSAGE$
|
|
16592
|
+
const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16504
16593
|
const resolveSettings$21 = (settings) => {
|
|
16505
16594
|
const reactDoctor = settings?.["react-doctor"];
|
|
16506
16595
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16556,12 +16645,45 @@ const noAutofocus = defineRule({
|
|
|
16556
16645
|
}
|
|
16557
16646
|
context.report({
|
|
16558
16647
|
node: autoFocusAttribute,
|
|
16559
|
-
message: MESSAGE$
|
|
16648
|
+
message: MESSAGE$38
|
|
16560
16649
|
});
|
|
16561
16650
|
} };
|
|
16562
16651
|
}
|
|
16563
16652
|
});
|
|
16564
16653
|
//#endregion
|
|
16654
|
+
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16655
|
+
const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
|
|
16656
|
+
const resolveStaticBoolean = (attribute) => {
|
|
16657
|
+
const value = attribute.value;
|
|
16658
|
+
if (!value) return true;
|
|
16659
|
+
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16660
|
+
if (isNodeOfType(literal, "Literal")) {
|
|
16661
|
+
if (literal.value === true || literal.value === "true") return true;
|
|
16662
|
+
if (literal.value === false || literal.value === "false") return false;
|
|
16663
|
+
}
|
|
16664
|
+
return null;
|
|
16665
|
+
};
|
|
16666
|
+
const noAutoplayWithoutMuted = defineRule({
|
|
16667
|
+
id: "no-autoplay-without-muted",
|
|
16668
|
+
title: "Autoplaying media without muted",
|
|
16669
|
+
severity: "warn",
|
|
16670
|
+
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16671
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
16672
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16673
|
+
const tagName = node.name.name;
|
|
16674
|
+
if (tagName !== "video" && tagName !== "audio") return;
|
|
16675
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16676
|
+
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16677
|
+
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16678
|
+
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16679
|
+
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16680
|
+
context.report({
|
|
16681
|
+
node: node.name,
|
|
16682
|
+
message: MESSAGE$37
|
|
16683
|
+
});
|
|
16684
|
+
} })
|
|
16685
|
+
});
|
|
16686
|
+
//#endregion
|
|
16565
16687
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16566
16688
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16567
16689
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -17060,7 +17182,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17060
17182
|
});
|
|
17061
17183
|
//#endregion
|
|
17062
17184
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17063
|
-
const MESSAGE$
|
|
17185
|
+
const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17064
17186
|
const noChildrenProp = defineRule({
|
|
17065
17187
|
id: "no-children-prop",
|
|
17066
17188
|
title: "Children passed as a prop",
|
|
@@ -17072,7 +17194,7 @@ const noChildrenProp = defineRule({
|
|
|
17072
17194
|
if (node.name.name !== "children") return;
|
|
17073
17195
|
context.report({
|
|
17074
17196
|
node: node.name,
|
|
17075
|
-
message: MESSAGE$
|
|
17197
|
+
message: MESSAGE$36
|
|
17076
17198
|
});
|
|
17077
17199
|
},
|
|
17078
17200
|
CallExpression(node) {
|
|
@@ -17085,7 +17207,7 @@ const noChildrenProp = defineRule({
|
|
|
17085
17207
|
const propertyKey = property.key;
|
|
17086
17208
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17087
17209
|
node: propertyKey,
|
|
17088
|
-
message: MESSAGE$
|
|
17210
|
+
message: MESSAGE$36
|
|
17089
17211
|
});
|
|
17090
17212
|
}
|
|
17091
17213
|
}
|
|
@@ -17093,7 +17215,7 @@ const noChildrenProp = defineRule({
|
|
|
17093
17215
|
});
|
|
17094
17216
|
//#endregion
|
|
17095
17217
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17096
|
-
const MESSAGE$
|
|
17218
|
+
const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17097
17219
|
const noCloneElement = defineRule({
|
|
17098
17220
|
id: "no-clone-element",
|
|
17099
17221
|
title: "cloneElement makes child props fragile",
|
|
@@ -17106,7 +17228,7 @@ const noCloneElement = defineRule({
|
|
|
17106
17228
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17107
17229
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17108
17230
|
node: callee,
|
|
17109
|
-
message: MESSAGE$
|
|
17231
|
+
message: MESSAGE$35
|
|
17110
17232
|
});
|
|
17111
17233
|
return;
|
|
17112
17234
|
}
|
|
@@ -17119,7 +17241,7 @@ const noCloneElement = defineRule({
|
|
|
17119
17241
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17120
17242
|
context.report({
|
|
17121
17243
|
node: callee,
|
|
17122
|
-
message: MESSAGE$
|
|
17244
|
+
message: MESSAGE$35
|
|
17123
17245
|
});
|
|
17124
17246
|
}
|
|
17125
17247
|
} })
|
|
@@ -17168,7 +17290,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17168
17290
|
};
|
|
17169
17291
|
//#endregion
|
|
17170
17292
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17171
|
-
const MESSAGE$
|
|
17293
|
+
const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17172
17294
|
const CONTEXT_MODULES = [
|
|
17173
17295
|
"react",
|
|
17174
17296
|
"use-context-selector",
|
|
@@ -17204,13 +17326,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17204
17326
|
if (!componentOrHookName) return;
|
|
17205
17327
|
context.report({
|
|
17206
17328
|
node,
|
|
17207
|
-
message: `${MESSAGE$
|
|
17329
|
+
message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
|
|
17208
17330
|
});
|
|
17209
17331
|
} })
|
|
17210
17332
|
});
|
|
17211
17333
|
//#endregion
|
|
17212
17334
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17213
|
-
const MESSAGE$
|
|
17335
|
+
const MESSAGE$33 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17214
17336
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17215
17337
|
id: "no-create-ref-in-function-component",
|
|
17216
17338
|
title: "createRef in function component",
|
|
@@ -17229,7 +17351,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17229
17351
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17230
17352
|
context.report({
|
|
17231
17353
|
node,
|
|
17232
|
-
message: MESSAGE$
|
|
17354
|
+
message: MESSAGE$33
|
|
17233
17355
|
});
|
|
17234
17356
|
} })
|
|
17235
17357
|
});
|
|
@@ -17369,7 +17491,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17369
17491
|
});
|
|
17370
17492
|
//#endregion
|
|
17371
17493
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17372
|
-
const MESSAGE$
|
|
17494
|
+
const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17373
17495
|
const noDanger = defineRule({
|
|
17374
17496
|
id: "no-danger",
|
|
17375
17497
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17382,7 +17504,7 @@ const noDanger = defineRule({
|
|
|
17382
17504
|
if (!propAttribute) return;
|
|
17383
17505
|
context.report({
|
|
17384
17506
|
node: propAttribute.name,
|
|
17385
|
-
message: MESSAGE$
|
|
17507
|
+
message: MESSAGE$32
|
|
17386
17508
|
});
|
|
17387
17509
|
},
|
|
17388
17510
|
CallExpression(node) {
|
|
@@ -17394,7 +17516,7 @@ const noDanger = defineRule({
|
|
|
17394
17516
|
const propertyKey = property.key;
|
|
17395
17517
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17396
17518
|
node: propertyKey,
|
|
17397
|
-
message: MESSAGE$
|
|
17519
|
+
message: MESSAGE$32
|
|
17398
17520
|
});
|
|
17399
17521
|
}
|
|
17400
17522
|
}
|
|
@@ -17402,7 +17524,7 @@ const noDanger = defineRule({
|
|
|
17402
17524
|
});
|
|
17403
17525
|
//#endregion
|
|
17404
17526
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17405
|
-
const MESSAGE$
|
|
17527
|
+
const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17406
17528
|
const isLineBreak = (child) => {
|
|
17407
17529
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17408
17530
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17472,7 +17594,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17472
17594
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17473
17595
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17474
17596
|
node: opening,
|
|
17475
|
-
message: MESSAGE$
|
|
17597
|
+
message: MESSAGE$31
|
|
17476
17598
|
});
|
|
17477
17599
|
},
|
|
17478
17600
|
CallExpression(node) {
|
|
@@ -17484,7 +17606,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17484
17606
|
if (!propsShape.hasDangerously) return;
|
|
17485
17607
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17486
17608
|
node,
|
|
17487
|
-
message: MESSAGE$
|
|
17609
|
+
message: MESSAGE$31
|
|
17488
17610
|
});
|
|
17489
17611
|
}
|
|
17490
17612
|
})
|
|
@@ -17649,6 +17771,37 @@ const noDefaultProps = defineRule({
|
|
|
17649
17771
|
} })
|
|
17650
17772
|
});
|
|
17651
17773
|
//#endregion
|
|
17774
|
+
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17775
|
+
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17776
|
+
//#endregion
|
|
17777
|
+
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17778
|
+
const renameDeprecatedToken = (token) => {
|
|
17779
|
+
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17780
|
+
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17781
|
+
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17782
|
+
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17783
|
+
return null;
|
|
17784
|
+
};
|
|
17785
|
+
const noDeprecatedTailwindClass = defineRule({
|
|
17786
|
+
id: "no-deprecated-tailwind-class",
|
|
17787
|
+
title: "Deprecated Tailwind v4 utility",
|
|
17788
|
+
tags: ["design", "test-noise"],
|
|
17789
|
+
severity: "warn",
|
|
17790
|
+
requires: ["tailwind:4"],
|
|
17791
|
+
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17792
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
17793
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
17794
|
+
if (!classNameValue) return;
|
|
17795
|
+
for (const token of getClassNameTokens(classNameValue)) {
|
|
17796
|
+
const replacement = renameDeprecatedToken(token);
|
|
17797
|
+
if (replacement) context.report({
|
|
17798
|
+
node,
|
|
17799
|
+
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17800
|
+
});
|
|
17801
|
+
}
|
|
17802
|
+
} })
|
|
17803
|
+
});
|
|
17804
|
+
//#endregion
|
|
17652
17805
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17653
17806
|
const isInitialOnlyPropName = (propName) => {
|
|
17654
17807
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -18061,7 +18214,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18061
18214
|
//#endregion
|
|
18062
18215
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18063
18216
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18064
|
-
const MESSAGE$
|
|
18217
|
+
const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18065
18218
|
const resolveSettings$20 = (settings) => {
|
|
18066
18219
|
const reactDoctor = settings?.["react-doctor"];
|
|
18067
18220
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18080,7 +18233,7 @@ const noDidMountSetState = defineRule({
|
|
|
18080
18233
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18081
18234
|
context.report({
|
|
18082
18235
|
node: node.callee,
|
|
18083
|
-
message: MESSAGE$
|
|
18236
|
+
message: MESSAGE$30
|
|
18084
18237
|
});
|
|
18085
18238
|
} };
|
|
18086
18239
|
}
|
|
@@ -18088,7 +18241,7 @@ const noDidMountSetState = defineRule({
|
|
|
18088
18241
|
//#endregion
|
|
18089
18242
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18090
18243
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18091
|
-
const MESSAGE$
|
|
18244
|
+
const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18092
18245
|
const resolveSettings$19 = (settings) => {
|
|
18093
18246
|
const reactDoctor = settings?.["react-doctor"];
|
|
18094
18247
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18107,7 +18260,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18107
18260
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18108
18261
|
context.report({
|
|
18109
18262
|
node: node.callee,
|
|
18110
|
-
message: MESSAGE$
|
|
18263
|
+
message: MESSAGE$29
|
|
18111
18264
|
});
|
|
18112
18265
|
} };
|
|
18113
18266
|
}
|
|
@@ -18130,7 +18283,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18130
18283
|
};
|
|
18131
18284
|
//#endregion
|
|
18132
18285
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18133
|
-
const MESSAGE$
|
|
18286
|
+
const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18134
18287
|
const shouldIgnoreMutation = (node) => {
|
|
18135
18288
|
let isConstructor = false;
|
|
18136
18289
|
let isInsideCallExpression = false;
|
|
@@ -18152,7 +18305,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18152
18305
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18153
18306
|
context.report({
|
|
18154
18307
|
node: reportNode,
|
|
18155
|
-
message: MESSAGE$
|
|
18308
|
+
message: MESSAGE$28
|
|
18156
18309
|
});
|
|
18157
18310
|
};
|
|
18158
18311
|
const noDirectMutationState = defineRule({
|
|
@@ -18362,6 +18515,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18362
18515
|
} })
|
|
18363
18516
|
});
|
|
18364
18517
|
//#endregion
|
|
18518
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18519
|
+
const MESSAGE$27 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18520
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18521
|
+
const noDocumentWrite = defineRule({
|
|
18522
|
+
id: "no-document-write",
|
|
18523
|
+
title: "document.write/writeln",
|
|
18524
|
+
severity: "warn",
|
|
18525
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18526
|
+
create: (context) => ({ CallExpression(node) {
|
|
18527
|
+
const callee = node.callee;
|
|
18528
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18529
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18530
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18531
|
+
context.report({
|
|
18532
|
+
node,
|
|
18533
|
+
message: MESSAGE$27
|
|
18534
|
+
});
|
|
18535
|
+
} })
|
|
18536
|
+
});
|
|
18537
|
+
//#endregion
|
|
18365
18538
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18366
18539
|
const noDynamicImportPath = defineRule({
|
|
18367
18540
|
id: "no-dynamic-import-path",
|
|
@@ -19740,7 +19913,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19740
19913
|
"ReactDOM",
|
|
19741
19914
|
"ReactDom"
|
|
19742
19915
|
]);
|
|
19743
|
-
const MESSAGE$
|
|
19916
|
+
const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19744
19917
|
const noFindDomNode = defineRule({
|
|
19745
19918
|
id: "no-find-dom-node",
|
|
19746
19919
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19751,7 +19924,7 @@ const noFindDomNode = defineRule({
|
|
|
19751
19924
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19752
19925
|
context.report({
|
|
19753
19926
|
node: callee,
|
|
19754
|
-
message: MESSAGE$
|
|
19927
|
+
message: MESSAGE$26
|
|
19755
19928
|
});
|
|
19756
19929
|
return;
|
|
19757
19930
|
}
|
|
@@ -19762,7 +19935,7 @@ const noFindDomNode = defineRule({
|
|
|
19762
19935
|
if (callee.property.name !== "findDOMNode") return;
|
|
19763
19936
|
context.report({
|
|
19764
19937
|
node: callee.property,
|
|
19765
|
-
message: MESSAGE$
|
|
19938
|
+
message: MESSAGE$26
|
|
19766
19939
|
});
|
|
19767
19940
|
}
|
|
19768
19941
|
} })
|
|
@@ -19803,6 +19976,41 @@ const noFullLodashImport = defineRule({
|
|
|
19803
19976
|
} })
|
|
19804
19977
|
});
|
|
19805
19978
|
//#endregion
|
|
19979
|
+
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19980
|
+
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19981
|
+
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19982
|
+
const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
|
|
19983
|
+
const noFullViewportWidth = defineRule({
|
|
19984
|
+
id: "no-full-viewport-width",
|
|
19985
|
+
title: "Full viewport width causes overflow",
|
|
19986
|
+
tags: ["design", "test-noise"],
|
|
19987
|
+
severity: "warn",
|
|
19988
|
+
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19989
|
+
create: (context) => ({
|
|
19990
|
+
JSXAttribute(node) {
|
|
19991
|
+
const expression = getInlineStyleExpression(node);
|
|
19992
|
+
if (!expression) return;
|
|
19993
|
+
for (const property of expression.properties ?? []) {
|
|
19994
|
+
const key = getStylePropertyKey(property);
|
|
19995
|
+
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19996
|
+
const value = getStylePropertyStringValue(property);
|
|
19997
|
+
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19998
|
+
node: property,
|
|
19999
|
+
message: MESSAGE$25
|
|
20000
|
+
});
|
|
20001
|
+
}
|
|
20002
|
+
},
|
|
20003
|
+
JSXOpeningElement(node) {
|
|
20004
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
20005
|
+
if (!classNameValue) return;
|
|
20006
|
+
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
20007
|
+
node,
|
|
20008
|
+
message: MESSAGE$25
|
|
20009
|
+
});
|
|
20010
|
+
}
|
|
20011
|
+
})
|
|
20012
|
+
});
|
|
20013
|
+
//#endregion
|
|
19806
20014
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19807
20015
|
const noGenericHandlerNames = defineRule({
|
|
19808
20016
|
id: "no-generic-handler-names",
|
|
@@ -19865,7 +20073,7 @@ const noGiantComponent = defineRule({
|
|
|
19865
20073
|
});
|
|
19866
20074
|
//#endregion
|
|
19867
20075
|
//#region src/plugin/constants/style.ts
|
|
19868
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
20076
|
+
const LAYOUT_PROPERTIES$1 = new Set([
|
|
19869
20077
|
"width",
|
|
19870
20078
|
"height",
|
|
19871
20079
|
"top",
|
|
@@ -19935,17 +20143,6 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
19935
20143
|
} })
|
|
19936
20144
|
});
|
|
19937
20145
|
//#endregion
|
|
19938
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
19939
|
-
const getStringFromClassNameAttr = (node) => {
|
|
19940
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
19941
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
19942
|
-
if (!classAttr?.value) return null;
|
|
19943
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
19944
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
19945
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
19946
|
-
return null;
|
|
19947
|
-
};
|
|
19948
|
-
//#endregion
|
|
19949
20146
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
19950
20147
|
const noGradientText = defineRule({
|
|
19951
20148
|
id: "no-gradient-text",
|
|
@@ -20004,7 +20201,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20004
20201
|
});
|
|
20005
20202
|
//#endregion
|
|
20006
20203
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20007
|
-
const MESSAGE$
|
|
20204
|
+
const MESSAGE$24 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20008
20205
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20009
20206
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20010
20207
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20018,7 +20215,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20018
20215
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20019
20216
|
context.report({
|
|
20020
20217
|
node: node.name,
|
|
20021
|
-
message: MESSAGE$
|
|
20218
|
+
message: MESSAGE$24
|
|
20022
20219
|
});
|
|
20023
20220
|
} })
|
|
20024
20221
|
});
|
|
@@ -20253,7 +20450,7 @@ const noIsMounted = defineRule({
|
|
|
20253
20450
|
});
|
|
20254
20451
|
//#endregion
|
|
20255
20452
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20256
|
-
const MESSAGE$
|
|
20453
|
+
const MESSAGE$23 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20257
20454
|
const isJsonMethodCall = (node, method) => {
|
|
20258
20455
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20259
20456
|
const callee = node.callee;
|
|
@@ -20270,13 +20467,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20270
20467
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20271
20468
|
context.report({
|
|
20272
20469
|
node,
|
|
20273
|
-
message: MESSAGE$
|
|
20470
|
+
message: MESSAGE$23
|
|
20274
20471
|
});
|
|
20275
20472
|
} })
|
|
20276
20473
|
});
|
|
20277
20474
|
//#endregion
|
|
20278
20475
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20279
|
-
const MESSAGE$
|
|
20476
|
+
const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20280
20477
|
const isJsxElementTypeReference = (node) => {
|
|
20281
20478
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20282
20479
|
const typeName = node.typeName;
|
|
@@ -20293,7 +20490,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20293
20490
|
if (!typeAnnotation) return;
|
|
20294
20491
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20295
20492
|
node: typeAnnotation,
|
|
20296
|
-
message: MESSAGE$
|
|
20493
|
+
message: MESSAGE$22
|
|
20297
20494
|
});
|
|
20298
20495
|
};
|
|
20299
20496
|
const noJsxElementType = defineRule({
|
|
@@ -20403,7 +20600,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20403
20600
|
let propertyName = null;
|
|
20404
20601
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20405
20602
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20406
|
-
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20603
|
+
if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
|
|
20407
20604
|
node: property,
|
|
20408
20605
|
message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
|
|
20409
20606
|
});
|
|
@@ -20593,6 +20790,134 @@ const noLongTransitionDuration = defineRule({
|
|
|
20593
20790
|
} })
|
|
20594
20791
|
});
|
|
20595
20792
|
//#endregion
|
|
20793
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20794
|
+
const getStylePropertyNumberValue = (property) => {
|
|
20795
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
20796
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20797
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20798
|
+
return null;
|
|
20799
|
+
};
|
|
20800
|
+
//#endregion
|
|
20801
|
+
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20802
|
+
const linearizeChannel = (channel) => {
|
|
20803
|
+
const normalized = channel / 255;
|
|
20804
|
+
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20805
|
+
};
|
|
20806
|
+
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20807
|
+
const getWcagContrastRatio = (foreground, background) => {
|
|
20808
|
+
const foregroundLuminance = relativeLuminance(foreground);
|
|
20809
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
20810
|
+
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20811
|
+
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20812
|
+
return (lighter + .05) / (darker + .05);
|
|
20813
|
+
};
|
|
20814
|
+
//#endregion
|
|
20815
|
+
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20816
|
+
const UNRESOLVABLE = new Set([
|
|
20817
|
+
"transparent",
|
|
20818
|
+
"currentcolor",
|
|
20819
|
+
"inherit",
|
|
20820
|
+
"initial",
|
|
20821
|
+
"unset",
|
|
20822
|
+
"revert",
|
|
20823
|
+
"none"
|
|
20824
|
+
]);
|
|
20825
|
+
const resolveOpaqueColor = (raw) => {
|
|
20826
|
+
const value = raw.trim().toLowerCase();
|
|
20827
|
+
if (UNRESOLVABLE.has(value)) return null;
|
|
20828
|
+
if (value === "white") return {
|
|
20829
|
+
red: 255,
|
|
20830
|
+
green: 255,
|
|
20831
|
+
blue: 255
|
|
20832
|
+
};
|
|
20833
|
+
if (value === "black") return {
|
|
20834
|
+
red: 0,
|
|
20835
|
+
green: 0,
|
|
20836
|
+
blue: 0
|
|
20837
|
+
};
|
|
20838
|
+
if (value.startsWith("var(")) return null;
|
|
20839
|
+
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20840
|
+
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20841
|
+
if (value.startsWith("rgb")) {
|
|
20842
|
+
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20843
|
+
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20844
|
+
}
|
|
20845
|
+
return parseColorToRgb(value);
|
|
20846
|
+
};
|
|
20847
|
+
const toPx = (property) => {
|
|
20848
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20849
|
+
if (numberValue !== null) return numberValue;
|
|
20850
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20851
|
+
if (stringValue === null) return null;
|
|
20852
|
+
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20853
|
+
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20854
|
+
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20855
|
+
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20856
|
+
return null;
|
|
20857
|
+
};
|
|
20858
|
+
const isBoldWeight = (property) => {
|
|
20859
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20860
|
+
if (numberValue !== null) return numberValue >= 700;
|
|
20861
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20862
|
+
if (stringValue === null) return false;
|
|
20863
|
+
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20864
|
+
const numericWeight = Number(stringValue);
|
|
20865
|
+
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20866
|
+
};
|
|
20867
|
+
const noLowContrastInlineStyle = defineRule({
|
|
20868
|
+
id: "no-low-contrast-inline-style",
|
|
20869
|
+
title: "Low-contrast text in inline style",
|
|
20870
|
+
tags: ["test-noise"],
|
|
20871
|
+
severity: "warn",
|
|
20872
|
+
category: "Accessibility",
|
|
20873
|
+
recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
|
|
20874
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
20875
|
+
const expression = getInlineStyleExpression(node);
|
|
20876
|
+
if (!expression) return;
|
|
20877
|
+
const properties = expression.properties ?? [];
|
|
20878
|
+
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20879
|
+
let foreground = null;
|
|
20880
|
+
let backgroundColorRaw = null;
|
|
20881
|
+
let backgroundShorthandRaw = null;
|
|
20882
|
+
let backgroundIsUnknown = false;
|
|
20883
|
+
let fontSizePx = null;
|
|
20884
|
+
let isBold = false;
|
|
20885
|
+
for (const property of properties) {
|
|
20886
|
+
const key = getStylePropertyKey(property);
|
|
20887
|
+
if (!key) continue;
|
|
20888
|
+
if (key === "backgroundImage") {
|
|
20889
|
+
backgroundIsUnknown = true;
|
|
20890
|
+
continue;
|
|
20891
|
+
}
|
|
20892
|
+
if (key === "fontSize" && property.type === "Property") {
|
|
20893
|
+
fontSizePx = toPx(property);
|
|
20894
|
+
continue;
|
|
20895
|
+
}
|
|
20896
|
+
if (key === "fontWeight" && property.type === "Property") {
|
|
20897
|
+
isBold = isBoldWeight(property);
|
|
20898
|
+
continue;
|
|
20899
|
+
}
|
|
20900
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20901
|
+
if (key === "color") {
|
|
20902
|
+
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20903
|
+
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20904
|
+
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20905
|
+
else backgroundShorthandRaw = stringValue;
|
|
20906
|
+
}
|
|
20907
|
+
if (backgroundIsUnknown) return;
|
|
20908
|
+
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20909
|
+
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20910
|
+
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20911
|
+
if (!foreground || !background) return;
|
|
20912
|
+
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20913
|
+
const ratio = getWcagContrastRatio(foreground, background);
|
|
20914
|
+
if (ratio < threshold) context.report({
|
|
20915
|
+
node,
|
|
20916
|
+
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
20917
|
+
});
|
|
20918
|
+
} })
|
|
20919
|
+
});
|
|
20920
|
+
//#endregion
|
|
20596
20921
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20597
20922
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20598
20923
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -20748,7 +21073,7 @@ const noMoment = defineRule({
|
|
|
20748
21073
|
});
|
|
20749
21074
|
//#endregion
|
|
20750
21075
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20751
|
-
const MESSAGE$
|
|
21076
|
+
const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20752
21077
|
const resolveSettings$16 = (settings) => {
|
|
20753
21078
|
const reactDoctor = settings?.["react-doctor"];
|
|
20754
21079
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21070,7 +21395,7 @@ const noMultiComp = defineRule({
|
|
|
21070
21395
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21071
21396
|
for (const component of flagged.slice(1)) context.report({
|
|
21072
21397
|
node: component.reportNode,
|
|
21073
|
-
message: MESSAGE$
|
|
21398
|
+
message: MESSAGE$21
|
|
21074
21399
|
});
|
|
21075
21400
|
} };
|
|
21076
21401
|
}
|
|
@@ -21238,7 +21563,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21238
21563
|
};
|
|
21239
21564
|
//#endregion
|
|
21240
21565
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21241
|
-
const MESSAGE$
|
|
21566
|
+
const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21242
21567
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21243
21568
|
"copyWithin",
|
|
21244
21569
|
"fill",
|
|
@@ -21448,7 +21773,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21448
21773
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21449
21774
|
context.report({
|
|
21450
21775
|
node: options.crossFileConsumerCallSite,
|
|
21451
|
-
message: `${MESSAGE$
|
|
21776
|
+
message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21452
21777
|
});
|
|
21453
21778
|
return;
|
|
21454
21779
|
}
|
|
@@ -21457,7 +21782,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21457
21782
|
reportedNodes.add(mutation.node);
|
|
21458
21783
|
context.report({
|
|
21459
21784
|
node: mutation.node,
|
|
21460
|
-
message: MESSAGE$
|
|
21785
|
+
message: MESSAGE$20
|
|
21461
21786
|
});
|
|
21462
21787
|
}
|
|
21463
21788
|
};
|
|
@@ -21729,7 +22054,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21729
22054
|
});
|
|
21730
22055
|
//#endregion
|
|
21731
22056
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21732
|
-
const MESSAGE$
|
|
22057
|
+
const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21733
22058
|
const resolveSettings$14 = (settings) => {
|
|
21734
22059
|
const reactDoctor = settings?.["react-doctor"];
|
|
21735
22060
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21757,7 +22082,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21757
22082
|
if (numeric === null) {
|
|
21758
22083
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21759
22084
|
node: tabIndex,
|
|
21760
|
-
message: MESSAGE$
|
|
22085
|
+
message: MESSAGE$19
|
|
21761
22086
|
});
|
|
21762
22087
|
return;
|
|
21763
22088
|
}
|
|
@@ -21770,7 +22095,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21770
22095
|
if (!roleAttribute) {
|
|
21771
22096
|
context.report({
|
|
21772
22097
|
node: tabIndex,
|
|
21773
|
-
message: MESSAGE$
|
|
22098
|
+
message: MESSAGE$19
|
|
21774
22099
|
});
|
|
21775
22100
|
return;
|
|
21776
22101
|
}
|
|
@@ -21784,20 +22109,12 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21784
22109
|
}
|
|
21785
22110
|
context.report({
|
|
21786
22111
|
node: tabIndex,
|
|
21787
|
-
message: MESSAGE$
|
|
22112
|
+
message: MESSAGE$19
|
|
21788
22113
|
});
|
|
21789
22114
|
} };
|
|
21790
22115
|
}
|
|
21791
22116
|
});
|
|
21792
22117
|
//#endregion
|
|
21793
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21794
|
-
const getStylePropertyNumberValue = (property) => {
|
|
21795
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
21796
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21797
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21798
|
-
return null;
|
|
21799
|
-
};
|
|
21800
|
-
//#endregion
|
|
21801
22118
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
21802
22119
|
const noOutlineNone = defineRule({
|
|
21803
22120
|
id: "no-outline-none",
|
|
@@ -22475,7 +22792,7 @@ const noRandomKey = defineRule({
|
|
|
22475
22792
|
});
|
|
22476
22793
|
//#endregion
|
|
22477
22794
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22478
|
-
const MESSAGE$
|
|
22795
|
+
const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22479
22796
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22480
22797
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22481
22798
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22501,13 +22818,13 @@ const noReactChildren = defineRule({
|
|
|
22501
22818
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22502
22819
|
context.report({
|
|
22503
22820
|
node: calleeOuter,
|
|
22504
|
-
message: MESSAGE$
|
|
22821
|
+
message: MESSAGE$18
|
|
22505
22822
|
});
|
|
22506
22823
|
return;
|
|
22507
22824
|
}
|
|
22508
22825
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22509
22826
|
node: calleeOuter,
|
|
22510
|
-
message: MESSAGE$
|
|
22827
|
+
message: MESSAGE$18
|
|
22511
22828
|
});
|
|
22512
22829
|
} })
|
|
22513
22830
|
});
|
|
@@ -22618,6 +22935,86 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22618
22935
|
})
|
|
22619
22936
|
});
|
|
22620
22937
|
//#endregion
|
|
22938
|
+
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22939
|
+
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22940
|
+
"div",
|
|
22941
|
+
"p",
|
|
22942
|
+
"section",
|
|
22943
|
+
"article",
|
|
22944
|
+
"main",
|
|
22945
|
+
"header",
|
|
22946
|
+
"footer",
|
|
22947
|
+
"nav",
|
|
22948
|
+
"aside",
|
|
22949
|
+
"figure",
|
|
22950
|
+
"figcaption",
|
|
22951
|
+
"blockquote",
|
|
22952
|
+
"form",
|
|
22953
|
+
"fieldset",
|
|
22954
|
+
"address",
|
|
22955
|
+
"pre",
|
|
22956
|
+
"ul",
|
|
22957
|
+
"ol",
|
|
22958
|
+
"dl",
|
|
22959
|
+
"dt",
|
|
22960
|
+
"dd",
|
|
22961
|
+
"h1",
|
|
22962
|
+
"h2",
|
|
22963
|
+
"h3",
|
|
22964
|
+
"h4",
|
|
22965
|
+
"h5",
|
|
22966
|
+
"h6"
|
|
22967
|
+
]);
|
|
22968
|
+
const INLINE_DEFAULT_TAGS = new Set([
|
|
22969
|
+
"span",
|
|
22970
|
+
"a",
|
|
22971
|
+
"b",
|
|
22972
|
+
"i",
|
|
22973
|
+
"em",
|
|
22974
|
+
"strong",
|
|
22975
|
+
"small",
|
|
22976
|
+
"code",
|
|
22977
|
+
"abbr",
|
|
22978
|
+
"cite",
|
|
22979
|
+
"label",
|
|
22980
|
+
"mark",
|
|
22981
|
+
"q",
|
|
22982
|
+
"s",
|
|
22983
|
+
"u",
|
|
22984
|
+
"sub",
|
|
22985
|
+
"sup",
|
|
22986
|
+
"kbd",
|
|
22987
|
+
"samp",
|
|
22988
|
+
"var",
|
|
22989
|
+
"time"
|
|
22990
|
+
]);
|
|
22991
|
+
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22992
|
+
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22993
|
+
const noRedundantDisplayClass = defineRule({
|
|
22994
|
+
id: "no-redundant-display-class",
|
|
22995
|
+
title: "Redundant display utility",
|
|
22996
|
+
tags: ["design", "test-noise"],
|
|
22997
|
+
severity: "warn",
|
|
22998
|
+
recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
|
|
22999
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23000
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23001
|
+
const tagName = node.name.name;
|
|
23002
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
23003
|
+
if (!classNameValue) return;
|
|
23004
|
+
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
23005
|
+
context.report({
|
|
23006
|
+
node,
|
|
23007
|
+
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23008
|
+
});
|
|
23009
|
+
return;
|
|
23010
|
+
}
|
|
23011
|
+
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
23012
|
+
node,
|
|
23013
|
+
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23014
|
+
});
|
|
23015
|
+
} })
|
|
23016
|
+
});
|
|
23017
|
+
//#endregion
|
|
22621
23018
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22622
23019
|
const ELEMENT_ROLE_PAIRS = [
|
|
22623
23020
|
["a", "link"],
|
|
@@ -22830,7 +23227,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22830
23227
|
});
|
|
22831
23228
|
//#endregion
|
|
22832
23229
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22833
|
-
const MESSAGE$
|
|
23230
|
+
const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22834
23231
|
const isReactDomRenderCall = (node) => {
|
|
22835
23232
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22836
23233
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22854,7 +23251,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22854
23251
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22855
23252
|
context.report({
|
|
22856
23253
|
node: node.callee,
|
|
22857
|
-
message: MESSAGE$
|
|
23254
|
+
message: MESSAGE$17
|
|
22858
23255
|
});
|
|
22859
23256
|
} })
|
|
22860
23257
|
});
|
|
@@ -23552,7 +23949,7 @@ const getParentComponent = (node) => {
|
|
|
23552
23949
|
};
|
|
23553
23950
|
//#endregion
|
|
23554
23951
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23555
|
-
const MESSAGE$
|
|
23952
|
+
const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23556
23953
|
const noSetState = defineRule({
|
|
23557
23954
|
id: "no-set-state",
|
|
23558
23955
|
title: "Local class state forbidden",
|
|
@@ -23567,7 +23964,7 @@ const noSetState = defineRule({
|
|
|
23567
23964
|
if (!getParentComponent(node)) return;
|
|
23568
23965
|
context.report({
|
|
23569
23966
|
node: node.callee,
|
|
23570
|
-
message: MESSAGE$
|
|
23967
|
+
message: MESSAGE$16
|
|
23571
23968
|
});
|
|
23572
23969
|
} })
|
|
23573
23970
|
});
|
|
@@ -23729,7 +24126,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23729
24126
|
};
|
|
23730
24127
|
//#endregion
|
|
23731
24128
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23732
|
-
const MESSAGE$
|
|
24129
|
+
const MESSAGE$15 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
23733
24130
|
const DEFAULT_HANDLERS = [
|
|
23734
24131
|
"onClick",
|
|
23735
24132
|
"onMouseDown",
|
|
@@ -23789,7 +24186,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23789
24186
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23790
24187
|
context.report({
|
|
23791
24188
|
node: node.name,
|
|
23792
|
-
message: MESSAGE$
|
|
24189
|
+
message: MESSAGE$15
|
|
23793
24190
|
});
|
|
23794
24191
|
return;
|
|
23795
24192
|
}
|
|
@@ -23799,19 +24196,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23799
24196
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23800
24197
|
context.report({
|
|
23801
24198
|
node: node.name,
|
|
23802
|
-
message: MESSAGE$
|
|
24199
|
+
message: MESSAGE$15
|
|
23803
24200
|
});
|
|
23804
24201
|
return;
|
|
23805
24202
|
}
|
|
23806
24203
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23807
24204
|
context.report({
|
|
23808
24205
|
node: node.name,
|
|
23809
|
-
message: MESSAGE$
|
|
24206
|
+
message: MESSAGE$15
|
|
23810
24207
|
});
|
|
23811
24208
|
} };
|
|
23812
24209
|
}
|
|
23813
24210
|
});
|
|
23814
24211
|
//#endregion
|
|
24212
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
24213
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
24214
|
+
"disabled",
|
|
24215
|
+
"checked",
|
|
24216
|
+
"readonly",
|
|
24217
|
+
"required",
|
|
24218
|
+
"selected",
|
|
24219
|
+
"multiple",
|
|
24220
|
+
"autofocus",
|
|
24221
|
+
"autoplay",
|
|
24222
|
+
"controls",
|
|
24223
|
+
"loop",
|
|
24224
|
+
"muted",
|
|
24225
|
+
"open",
|
|
24226
|
+
"reversed",
|
|
24227
|
+
"default",
|
|
24228
|
+
"novalidate",
|
|
24229
|
+
"formnovalidate",
|
|
24230
|
+
"playsinline",
|
|
24231
|
+
"itemscope",
|
|
24232
|
+
"allowfullscreen"
|
|
24233
|
+
]);
|
|
24234
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
24235
|
+
id: "no-string-false-on-boolean-attribute",
|
|
24236
|
+
title: "String true/false on a boolean attribute",
|
|
24237
|
+
severity: "warn",
|
|
24238
|
+
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
24239
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24240
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24241
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
24242
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
24243
|
+
for (const attribute of node.attributes) {
|
|
24244
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24245
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
24246
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
24247
|
+
const value = getJsxPropStringValue(attribute);
|
|
24248
|
+
if (value !== "false" && value !== "true") continue;
|
|
24249
|
+
const attributeName = attribute.name.name;
|
|
24250
|
+
const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
|
|
24251
|
+
context.report({
|
|
24252
|
+
node: attribute,
|
|
24253
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
24254
|
+
});
|
|
24255
|
+
}
|
|
24256
|
+
} })
|
|
24257
|
+
});
|
|
24258
|
+
//#endregion
|
|
23815
24259
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23816
24260
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23817
24261
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23862,8 +24306,154 @@ const noStringRefs = defineRule({
|
|
|
23862
24306
|
}
|
|
23863
24307
|
});
|
|
23864
24308
|
//#endregion
|
|
24309
|
+
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24310
|
+
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24311
|
+
if (token.includes(":")) return false;
|
|
24312
|
+
if (!token.startsWith(prefix)) return false;
|
|
24313
|
+
const value = token.slice(prefix.length);
|
|
24314
|
+
if (value === "" || value === "current") return false;
|
|
24315
|
+
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24316
|
+
return true;
|
|
24317
|
+
});
|
|
24318
|
+
const isCurrentColor = (attribute) => {
|
|
24319
|
+
const value = getJsxPropStringValue(attribute);
|
|
24320
|
+
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24321
|
+
};
|
|
24322
|
+
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24323
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
24324
|
+
title: "currentColor fights a fill/stroke class",
|
|
24325
|
+
tags: ["design", "test-noise"],
|
|
24326
|
+
severity: "warn",
|
|
24327
|
+
recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
|
|
24328
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24329
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24330
|
+
if (!classNameValue) return;
|
|
24331
|
+
for (const paint of ["fill", "stroke"]) {
|
|
24332
|
+
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24333
|
+
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24334
|
+
context.report({
|
|
24335
|
+
node: attribute,
|
|
24336
|
+
message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
|
|
24337
|
+
});
|
|
24338
|
+
return;
|
|
24339
|
+
}
|
|
24340
|
+
}
|
|
24341
|
+
} })
|
|
24342
|
+
});
|
|
24343
|
+
//#endregion
|
|
24344
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24345
|
+
const MESSAGE$14 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
24346
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24347
|
+
const noSyncXhr = defineRule({
|
|
24348
|
+
id: "no-sync-xhr",
|
|
24349
|
+
title: "Synchronous XMLHttpRequest",
|
|
24350
|
+
severity: "warn",
|
|
24351
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24352
|
+
create: (context) => ({ CallExpression(node) {
|
|
24353
|
+
const callee = node.callee;
|
|
24354
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24355
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24356
|
+
const asyncArgument = node.arguments?.[2];
|
|
24357
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24358
|
+
context.report({
|
|
24359
|
+
node,
|
|
24360
|
+
message: MESSAGE$14
|
|
24361
|
+
});
|
|
24362
|
+
} })
|
|
24363
|
+
});
|
|
24364
|
+
//#endregion
|
|
24365
|
+
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24366
|
+
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24367
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
24368
|
+
"width",
|
|
24369
|
+
"height",
|
|
24370
|
+
"min-width",
|
|
24371
|
+
"max-width",
|
|
24372
|
+
"min-height",
|
|
24373
|
+
"max-height",
|
|
24374
|
+
"top",
|
|
24375
|
+
"left",
|
|
24376
|
+
"right",
|
|
24377
|
+
"bottom",
|
|
24378
|
+
"inset",
|
|
24379
|
+
"inset-block",
|
|
24380
|
+
"inset-inline",
|
|
24381
|
+
"margin",
|
|
24382
|
+
"margin-top",
|
|
24383
|
+
"margin-right",
|
|
24384
|
+
"margin-bottom",
|
|
24385
|
+
"margin-left",
|
|
24386
|
+
"margin-block",
|
|
24387
|
+
"margin-inline",
|
|
24388
|
+
"padding",
|
|
24389
|
+
"padding-top",
|
|
24390
|
+
"padding-right",
|
|
24391
|
+
"padding-bottom",
|
|
24392
|
+
"padding-left",
|
|
24393
|
+
"padding-block",
|
|
24394
|
+
"padding-inline"
|
|
24395
|
+
]);
|
|
24396
|
+
const noTailwindLayoutTransition = defineRule({
|
|
24397
|
+
id: "no-tailwind-layout-transition",
|
|
24398
|
+
title: "Animating a layout property",
|
|
24399
|
+
tags: ["design", "test-noise"],
|
|
24400
|
+
severity: "warn",
|
|
24401
|
+
category: "Performance",
|
|
24402
|
+
recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
|
|
24403
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24404
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24405
|
+
if (!classNameValue) return;
|
|
24406
|
+
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24407
|
+
const animatedProperties = transitionMatch[1];
|
|
24408
|
+
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24409
|
+
if (layoutProperty) context.report({
|
|
24410
|
+
node,
|
|
24411
|
+
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24412
|
+
});
|
|
24413
|
+
}
|
|
24414
|
+
} })
|
|
24415
|
+
});
|
|
24416
|
+
//#endregion
|
|
24417
|
+
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24418
|
+
const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
|
|
24419
|
+
const targetIsBlank = (attribute) => {
|
|
24420
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
24421
|
+
if (stringValue !== null) return stringValue === "_blank";
|
|
24422
|
+
const value = attribute.value;
|
|
24423
|
+
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24424
|
+
const expression = value.expression;
|
|
24425
|
+
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24426
|
+
}
|
|
24427
|
+
return false;
|
|
24428
|
+
};
|
|
24429
|
+
const noTargetBlankWithoutRel = defineRule({
|
|
24430
|
+
id: "no-target-blank-without-rel",
|
|
24431
|
+
title: "target=_blank without rel=noopener",
|
|
24432
|
+
severity: "warn",
|
|
24433
|
+
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24434
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24435
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24436
|
+
const tagName = node.name.name;
|
|
24437
|
+
if (tagName !== "a" && tagName !== "area") return;
|
|
24438
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24439
|
+
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24440
|
+
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24441
|
+
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24442
|
+
if (relAttribute) {
|
|
24443
|
+
const relValue = getJsxPropStringValue(relAttribute);
|
|
24444
|
+
if (relValue === null) return;
|
|
24445
|
+
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24446
|
+
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24447
|
+
}
|
|
24448
|
+
context.report({
|
|
24449
|
+
node: node.name,
|
|
24450
|
+
message: MESSAGE$13
|
|
24451
|
+
});
|
|
24452
|
+
} })
|
|
24453
|
+
});
|
|
24454
|
+
//#endregion
|
|
23865
24455
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23866
|
-
const MESSAGE$
|
|
24456
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
23867
24457
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
23868
24458
|
let ancestor = node.parent;
|
|
23869
24459
|
while (ancestor) {
|
|
@@ -23932,7 +24522,7 @@ const noThisInSfc = defineRule({
|
|
|
23932
24522
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
23933
24523
|
context.report({
|
|
23934
24524
|
node,
|
|
23935
|
-
message: MESSAGE$
|
|
24525
|
+
message: MESSAGE$12
|
|
23936
24526
|
});
|
|
23937
24527
|
} };
|
|
23938
24528
|
}
|
|
@@ -23970,26 +24560,39 @@ const noTinyText = defineRule({
|
|
|
23970
24560
|
});
|
|
23971
24561
|
//#endregion
|
|
23972
24562
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24563
|
+
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24564
|
+
const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
|
|
23973
24565
|
const noTransitionAll = defineRule({
|
|
23974
24566
|
id: "no-transition-all",
|
|
23975
24567
|
title: "transition: all animates everything",
|
|
23976
24568
|
tags: ["test-noise"],
|
|
23977
24569
|
severity: "warn",
|
|
23978
24570
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
23979
|
-
create: (context) => ({
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
|
|
23986
|
-
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
24571
|
+
create: (context) => ({
|
|
24572
|
+
JSXAttribute(node) {
|
|
24573
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24574
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24575
|
+
const expression = node.value.expression;
|
|
24576
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24577
|
+
for (const property of expression.properties ?? []) {
|
|
24578
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24579
|
+
const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
24580
|
+
if (key !== "transition" && key !== "transitionProperty") continue;
|
|
24581
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
|
|
24582
|
+
node: property,
|
|
24583
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24584
|
+
});
|
|
24585
|
+
}
|
|
24586
|
+
},
|
|
24587
|
+
JSXOpeningElement(node) {
|
|
24588
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24589
|
+
if (!classNameValue) return;
|
|
24590
|
+
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24591
|
+
node,
|
|
24592
|
+
message: TAILWIND_MESSAGE
|
|
23990
24593
|
});
|
|
23991
24594
|
}
|
|
23992
|
-
}
|
|
24595
|
+
})
|
|
23993
24596
|
});
|
|
23994
24597
|
//#endregion
|
|
23995
24598
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -24033,7 +24636,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
24033
24636
|
}
|
|
24034
24637
|
return stateNames;
|
|
24035
24638
|
};
|
|
24036
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
24037
24639
|
const noUncontrolledInput = defineRule({
|
|
24038
24640
|
id: "no-uncontrolled-input",
|
|
24039
24641
|
title: "Uncontrolled input value",
|
|
@@ -24137,6 +24739,38 @@ const noUnescapedEntities = defineRule({
|
|
|
24137
24739
|
} })
|
|
24138
24740
|
});
|
|
24139
24741
|
//#endregion
|
|
24742
|
+
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24743
|
+
const UNINFORMATIVE_LABELS = new Set([
|
|
24744
|
+
"icon",
|
|
24745
|
+
"button",
|
|
24746
|
+
"image",
|
|
24747
|
+
"img",
|
|
24748
|
+
"link",
|
|
24749
|
+
"graphic",
|
|
24750
|
+
"svg",
|
|
24751
|
+
"picture",
|
|
24752
|
+
"element",
|
|
24753
|
+
"field",
|
|
24754
|
+
"input"
|
|
24755
|
+
]);
|
|
24756
|
+
const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
|
|
24757
|
+
const noUninformativeAriaLabel = defineRule({
|
|
24758
|
+
id: "no-uninformative-aria-label",
|
|
24759
|
+
title: "Uninformative aria-label",
|
|
24760
|
+
severity: "warn",
|
|
24761
|
+
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24762
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24763
|
+
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24764
|
+
if (!ariaLabel) return;
|
|
24765
|
+
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24766
|
+
if (labelValue === null) return;
|
|
24767
|
+
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24768
|
+
node: ariaLabel,
|
|
24769
|
+
message: MESSAGE$11
|
|
24770
|
+
});
|
|
24771
|
+
} })
|
|
24772
|
+
});
|
|
24773
|
+
//#endregion
|
|
24140
24774
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24141
24775
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24142
24776
|
"activedescendant",
|
|
@@ -25608,7 +26242,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
25608
26242
|
//#endregion
|
|
25609
26243
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
25610
26244
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
25611
|
-
const MESSAGE$
|
|
26245
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
25612
26246
|
const resolveSettings$7 = (settings) => {
|
|
25613
26247
|
const reactDoctor = settings?.["react-doctor"];
|
|
25614
26248
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -25642,7 +26276,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
25642
26276
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25643
26277
|
context.report({
|
|
25644
26278
|
node: node.callee,
|
|
25645
|
-
message: MESSAGE$
|
|
26279
|
+
message: MESSAGE$10
|
|
25646
26280
|
});
|
|
25647
26281
|
} };
|
|
25648
26282
|
}
|
|
@@ -26520,7 +27154,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
26520
27154
|
});
|
|
26521
27155
|
//#endregion
|
|
26522
27156
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
26523
|
-
const MESSAGE$
|
|
27157
|
+
const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
26524
27158
|
const preactPreferOndblclick = defineRule({
|
|
26525
27159
|
id: "preact-prefer-ondblclick",
|
|
26526
27160
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -26535,7 +27169,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
26535
27169
|
if (!onDoubleClickAttribute) return;
|
|
26536
27170
|
context.report({
|
|
26537
27171
|
node: onDoubleClickAttribute,
|
|
26538
|
-
message: MESSAGE$
|
|
27172
|
+
message: MESSAGE$9
|
|
26539
27173
|
});
|
|
26540
27174
|
} })
|
|
26541
27175
|
});
|
|
@@ -26575,6 +27209,42 @@ const preactPreferOninput = defineRule({
|
|
|
26575
27209
|
} })
|
|
26576
27210
|
});
|
|
26577
27211
|
//#endregion
|
|
27212
|
+
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27213
|
+
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27214
|
+
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27215
|
+
const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
|
|
27216
|
+
const preferDvhOverVh = defineRule({
|
|
27217
|
+
id: "prefer-dvh-over-vh",
|
|
27218
|
+
title: "Use dvh instead of vh for full height",
|
|
27219
|
+
tags: ["design", "test-noise"],
|
|
27220
|
+
severity: "warn",
|
|
27221
|
+
requires: ["tailwind:3.4"],
|
|
27222
|
+
recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
|
|
27223
|
+
create: (context) => ({
|
|
27224
|
+
JSXAttribute(node) {
|
|
27225
|
+
const expression = getInlineStyleExpression(node);
|
|
27226
|
+
if (!expression) return;
|
|
27227
|
+
for (const property of expression.properties ?? []) {
|
|
27228
|
+
const key = getStylePropertyKey(property);
|
|
27229
|
+
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27230
|
+
const value = getStylePropertyStringValue(property);
|
|
27231
|
+
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27232
|
+
node: property,
|
|
27233
|
+
message: MESSAGE$8
|
|
27234
|
+
});
|
|
27235
|
+
}
|
|
27236
|
+
},
|
|
27237
|
+
JSXOpeningElement(node) {
|
|
27238
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27239
|
+
if (!classNameValue) return;
|
|
27240
|
+
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27241
|
+
node,
|
|
27242
|
+
message: MESSAGE$8
|
|
27243
|
+
});
|
|
27244
|
+
}
|
|
27245
|
+
})
|
|
27246
|
+
});
|
|
27247
|
+
//#endregion
|
|
26578
27248
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
26579
27249
|
const preferDynamicImport = defineRule({
|
|
26580
27250
|
id: "prefer-dynamic-import",
|
|
@@ -27166,6 +27836,26 @@ const preferTagOverRole = defineRule({
|
|
|
27166
27836
|
} })
|
|
27167
27837
|
});
|
|
27168
27838
|
//#endregion
|
|
27839
|
+
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27840
|
+
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27841
|
+
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27842
|
+
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27843
|
+
const preferTruncateShorthand = defineRule({
|
|
27844
|
+
id: "prefer-truncate-shorthand",
|
|
27845
|
+
title: "Use truncate shorthand",
|
|
27846
|
+
tags: ["design", "test-noise"],
|
|
27847
|
+
severity: "warn",
|
|
27848
|
+
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27849
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
27850
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27851
|
+
if (!classNameValue) return;
|
|
27852
|
+
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27853
|
+
node,
|
|
27854
|
+
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27855
|
+
});
|
|
27856
|
+
} })
|
|
27857
|
+
});
|
|
27858
|
+
//#endregion
|
|
27169
27859
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27170
27860
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27171
27861
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -35437,13 +36127,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35437
36127
|
const collectDeclaredNames = (declaration) => {
|
|
35438
36128
|
const names = /* @__PURE__ */ new Set();
|
|
35439
36129
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35440
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35441
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35442
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35443
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35444
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35445
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35446
|
-
}
|
|
36130
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35447
36131
|
return names;
|
|
35448
36132
|
};
|
|
35449
36133
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35453,11 +36137,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35453
36137
|
};
|
|
35454
36138
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35455
36139
|
if (names.size === 0) return false;
|
|
36140
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35456
36141
|
let didRead = false;
|
|
35457
|
-
|
|
35458
|
-
if (
|
|
35459
|
-
|
|
35460
|
-
|
|
36142
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
36143
|
+
if (!declarator.init) continue;
|
|
36144
|
+
walkAst(declarator.init, (child) => {
|
|
36145
|
+
if (didRead) return;
|
|
36146
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
36147
|
+
});
|
|
36148
|
+
}
|
|
35461
36149
|
return didRead;
|
|
35462
36150
|
};
|
|
35463
36151
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36717,7 +37405,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36717
37405
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36718
37406
|
scan: scanByPattern({
|
|
36719
37407
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36720
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
37408
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36721
37409
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36722
37410
|
})
|
|
36723
37411
|
});
|
|
@@ -38842,6 +39530,17 @@ const reactDoctorRules = [
|
|
|
38842
39530
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
38843
39531
|
}
|
|
38844
39532
|
},
|
|
39533
|
+
{
|
|
39534
|
+
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39535
|
+
id: "no-arbitrary-px-font-size",
|
|
39536
|
+
source: "react-doctor",
|
|
39537
|
+
originallyExternal: false,
|
|
39538
|
+
rule: {
|
|
39539
|
+
...noArbitraryPxFontSize,
|
|
39540
|
+
framework: "global",
|
|
39541
|
+
category: "Accessibility"
|
|
39542
|
+
}
|
|
39543
|
+
},
|
|
38845
39544
|
{
|
|
38846
39545
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
38847
39546
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -38901,6 +39600,18 @@ const reactDoctorRules = [
|
|
|
38901
39600
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
38902
39601
|
}
|
|
38903
39602
|
},
|
|
39603
|
+
{
|
|
39604
|
+
key: "react-doctor/no-autoplay-without-muted",
|
|
39605
|
+
id: "no-autoplay-without-muted",
|
|
39606
|
+
source: "react-doctor",
|
|
39607
|
+
originallyExternal: false,
|
|
39608
|
+
rule: {
|
|
39609
|
+
...noAutoplayWithoutMuted,
|
|
39610
|
+
framework: "global",
|
|
39611
|
+
category: "Accessibility",
|
|
39612
|
+
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39613
|
+
}
|
|
39614
|
+
},
|
|
38904
39615
|
{
|
|
38905
39616
|
key: "react-doctor/no-barrel-import",
|
|
38906
39617
|
id: "no-barrel-import",
|
|
@@ -39054,6 +39765,17 @@ const reactDoctorRules = [
|
|
|
39054
39765
|
category: "Maintainability"
|
|
39055
39766
|
}
|
|
39056
39767
|
},
|
|
39768
|
+
{
|
|
39769
|
+
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39770
|
+
id: "no-deprecated-tailwind-class",
|
|
39771
|
+
source: "react-doctor",
|
|
39772
|
+
originallyExternal: false,
|
|
39773
|
+
rule: {
|
|
39774
|
+
...noDeprecatedTailwindClass,
|
|
39775
|
+
framework: "global",
|
|
39776
|
+
category: "Maintainability"
|
|
39777
|
+
}
|
|
39778
|
+
},
|
|
39057
39779
|
{
|
|
39058
39780
|
key: "react-doctor/no-derived-state",
|
|
39059
39781
|
id: "no-derived-state",
|
|
@@ -39173,6 +39895,17 @@ const reactDoctorRules = [
|
|
|
39173
39895
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39174
39896
|
}
|
|
39175
39897
|
},
|
|
39898
|
+
{
|
|
39899
|
+
key: "react-doctor/no-document-write",
|
|
39900
|
+
id: "no-document-write",
|
|
39901
|
+
source: "react-doctor",
|
|
39902
|
+
originallyExternal: false,
|
|
39903
|
+
rule: {
|
|
39904
|
+
...noDocumentWrite,
|
|
39905
|
+
framework: "global",
|
|
39906
|
+
category: "Performance"
|
|
39907
|
+
}
|
|
39908
|
+
},
|
|
39176
39909
|
{
|
|
39177
39910
|
key: "react-doctor/no-dynamic-import-path",
|
|
39178
39911
|
id: "no-dynamic-import-path",
|
|
@@ -39314,6 +40047,17 @@ const reactDoctorRules = [
|
|
|
39314
40047
|
category: "Performance"
|
|
39315
40048
|
}
|
|
39316
40049
|
},
|
|
40050
|
+
{
|
|
40051
|
+
key: "react-doctor/no-full-viewport-width",
|
|
40052
|
+
id: "no-full-viewport-width",
|
|
40053
|
+
source: "react-doctor",
|
|
40054
|
+
originallyExternal: false,
|
|
40055
|
+
rule: {
|
|
40056
|
+
...noFullViewportWidth,
|
|
40057
|
+
framework: "global",
|
|
40058
|
+
category: "Maintainability"
|
|
40059
|
+
}
|
|
40060
|
+
},
|
|
39317
40061
|
{
|
|
39318
40062
|
key: "react-doctor/no-generic-handler-names",
|
|
39319
40063
|
id: "no-generic-handler-names",
|
|
@@ -39553,6 +40297,17 @@ const reactDoctorRules = [
|
|
|
39553
40297
|
category: "Performance"
|
|
39554
40298
|
}
|
|
39555
40299
|
},
|
|
40300
|
+
{
|
|
40301
|
+
key: "react-doctor/no-low-contrast-inline-style",
|
|
40302
|
+
id: "no-low-contrast-inline-style",
|
|
40303
|
+
source: "react-doctor",
|
|
40304
|
+
originallyExternal: false,
|
|
40305
|
+
rule: {
|
|
40306
|
+
...noLowContrastInlineStyle,
|
|
40307
|
+
framework: "global",
|
|
40308
|
+
category: "Accessibility"
|
|
40309
|
+
}
|
|
40310
|
+
},
|
|
39556
40311
|
{
|
|
39557
40312
|
key: "react-doctor/no-many-boolean-props",
|
|
39558
40313
|
id: "no-many-boolean-props",
|
|
@@ -39830,6 +40585,17 @@ const reactDoctorRules = [
|
|
|
39830
40585
|
category: "Maintainability"
|
|
39831
40586
|
}
|
|
39832
40587
|
},
|
|
40588
|
+
{
|
|
40589
|
+
key: "react-doctor/no-redundant-display-class",
|
|
40590
|
+
id: "no-redundant-display-class",
|
|
40591
|
+
source: "react-doctor",
|
|
40592
|
+
originallyExternal: false,
|
|
40593
|
+
rule: {
|
|
40594
|
+
...noRedundantDisplayClass,
|
|
40595
|
+
framework: "global",
|
|
40596
|
+
category: "Maintainability"
|
|
40597
|
+
}
|
|
40598
|
+
},
|
|
39833
40599
|
{
|
|
39834
40600
|
key: "react-doctor/no-redundant-roles",
|
|
39835
40601
|
id: "no-redundant-roles",
|
|
@@ -39982,6 +40748,18 @@ const reactDoctorRules = [
|
|
|
39982
40748
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39983
40749
|
}
|
|
39984
40750
|
},
|
|
40751
|
+
{
|
|
40752
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40753
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40754
|
+
source: "react-doctor",
|
|
40755
|
+
originallyExternal: false,
|
|
40756
|
+
rule: {
|
|
40757
|
+
...noStringFalseOnBooleanAttribute,
|
|
40758
|
+
framework: "global",
|
|
40759
|
+
category: "Bugs",
|
|
40760
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40761
|
+
}
|
|
40762
|
+
},
|
|
39985
40763
|
{
|
|
39986
40764
|
key: "react-doctor/no-string-refs",
|
|
39987
40765
|
id: "no-string-refs",
|
|
@@ -39994,6 +40772,51 @@ const reactDoctorRules = [
|
|
|
39994
40772
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39995
40773
|
}
|
|
39996
40774
|
},
|
|
40775
|
+
{
|
|
40776
|
+
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40777
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
40778
|
+
source: "react-doctor",
|
|
40779
|
+
originallyExternal: false,
|
|
40780
|
+
rule: {
|
|
40781
|
+
...noSvgCurrentcolorWithFillClass,
|
|
40782
|
+
framework: "global",
|
|
40783
|
+
category: "Maintainability"
|
|
40784
|
+
}
|
|
40785
|
+
},
|
|
40786
|
+
{
|
|
40787
|
+
key: "react-doctor/no-sync-xhr",
|
|
40788
|
+
id: "no-sync-xhr",
|
|
40789
|
+
source: "react-doctor",
|
|
40790
|
+
originallyExternal: false,
|
|
40791
|
+
rule: {
|
|
40792
|
+
...noSyncXhr,
|
|
40793
|
+
framework: "global",
|
|
40794
|
+
category: "Performance"
|
|
40795
|
+
}
|
|
40796
|
+
},
|
|
40797
|
+
{
|
|
40798
|
+
key: "react-doctor/no-tailwind-layout-transition",
|
|
40799
|
+
id: "no-tailwind-layout-transition",
|
|
40800
|
+
source: "react-doctor",
|
|
40801
|
+
originallyExternal: false,
|
|
40802
|
+
rule: {
|
|
40803
|
+
...noTailwindLayoutTransition,
|
|
40804
|
+
framework: "global",
|
|
40805
|
+
category: "Performance"
|
|
40806
|
+
}
|
|
40807
|
+
},
|
|
40808
|
+
{
|
|
40809
|
+
key: "react-doctor/no-target-blank-without-rel",
|
|
40810
|
+
id: "no-target-blank-without-rel",
|
|
40811
|
+
source: "react-doctor",
|
|
40812
|
+
originallyExternal: false,
|
|
40813
|
+
rule: {
|
|
40814
|
+
...noTargetBlankWithoutRel,
|
|
40815
|
+
framework: "global",
|
|
40816
|
+
category: "Accessibility",
|
|
40817
|
+
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40818
|
+
}
|
|
40819
|
+
},
|
|
39997
40820
|
{
|
|
39998
40821
|
key: "react-doctor/no-this-in-sfc",
|
|
39999
40822
|
id: "no-this-in-sfc",
|
|
@@ -40063,6 +40886,18 @@ const reactDoctorRules = [
|
|
|
40063
40886
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
40064
40887
|
}
|
|
40065
40888
|
},
|
|
40889
|
+
{
|
|
40890
|
+
key: "react-doctor/no-uninformative-aria-label",
|
|
40891
|
+
id: "no-uninformative-aria-label",
|
|
40892
|
+
source: "react-doctor",
|
|
40893
|
+
originallyExternal: false,
|
|
40894
|
+
rule: {
|
|
40895
|
+
...noUninformativeAriaLabel,
|
|
40896
|
+
framework: "global",
|
|
40897
|
+
category: "Accessibility",
|
|
40898
|
+
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40899
|
+
}
|
|
40900
|
+
},
|
|
40066
40901
|
{
|
|
40067
40902
|
key: "react-doctor/no-unknown-property",
|
|
40068
40903
|
id: "no-unknown-property",
|
|
@@ -40272,6 +41107,17 @@ const reactDoctorRules = [
|
|
|
40272
41107
|
category: "Bugs"
|
|
40273
41108
|
}
|
|
40274
41109
|
},
|
|
41110
|
+
{
|
|
41111
|
+
key: "react-doctor/prefer-dvh-over-vh",
|
|
41112
|
+
id: "prefer-dvh-over-vh",
|
|
41113
|
+
source: "react-doctor",
|
|
41114
|
+
originallyExternal: false,
|
|
41115
|
+
rule: {
|
|
41116
|
+
...preferDvhOverVh,
|
|
41117
|
+
framework: "global",
|
|
41118
|
+
category: "Maintainability"
|
|
41119
|
+
}
|
|
41120
|
+
},
|
|
40275
41121
|
{
|
|
40276
41122
|
key: "react-doctor/prefer-dynamic-import",
|
|
40277
41123
|
id: "prefer-dynamic-import",
|
|
@@ -40376,6 +41222,17 @@ const reactDoctorRules = [
|
|
|
40376
41222
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
40377
41223
|
}
|
|
40378
41224
|
},
|
|
41225
|
+
{
|
|
41226
|
+
key: "react-doctor/prefer-truncate-shorthand",
|
|
41227
|
+
id: "prefer-truncate-shorthand",
|
|
41228
|
+
source: "react-doctor",
|
|
41229
|
+
originallyExternal: false,
|
|
41230
|
+
rule: {
|
|
41231
|
+
...preferTruncateShorthand,
|
|
41232
|
+
framework: "global",
|
|
41233
|
+
category: "Maintainability"
|
|
41234
|
+
}
|
|
41235
|
+
},
|
|
40379
41236
|
{
|
|
40380
41237
|
key: "react-doctor/prefer-use-effect-event",
|
|
40381
41238
|
id: "prefer-use-effect-event",
|