oxlint-plugin-react-doctor 0.5.6-dev.5b742fa → 0.5.6-dev.66133f8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -505
- package/dist/index.js +300 -966
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
890
890
|
});
|
|
891
891
|
//#endregion
|
|
892
892
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
893
|
-
const
|
|
893
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
894
|
+
const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
895
|
+
for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
|
|
896
|
+
const character = content[cursor];
|
|
897
|
+
if (character === "\\") {
|
|
898
|
+
cursor += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (character === delimiter) return false;
|
|
902
|
+
if (WHITESPACE_PATTERN.test(character)) return true;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
};
|
|
906
|
+
const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
894
907
|
const characters = content.split("");
|
|
895
908
|
let stringDelimiter = null;
|
|
909
|
+
let isBlankingString = false;
|
|
910
|
+
const templateExpressionDepths = [];
|
|
896
911
|
let index = 0;
|
|
912
|
+
const blankUnlessNewline = (offset) => {
|
|
913
|
+
if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
|
|
914
|
+
};
|
|
897
915
|
while (index < content.length) {
|
|
898
916
|
const character = content[index];
|
|
899
917
|
const nextCharacter = content[index + 1];
|
|
900
918
|
if (stringDelimiter !== null) {
|
|
901
919
|
if (character === "\\") {
|
|
920
|
+
if (isBlankingString) {
|
|
921
|
+
blankUnlessNewline(index);
|
|
922
|
+
blankUnlessNewline(index + 1);
|
|
923
|
+
}
|
|
902
924
|
index += 2;
|
|
903
925
|
continue;
|
|
904
926
|
}
|
|
905
|
-
if (character === stringDelimiter)
|
|
927
|
+
if (character === stringDelimiter) {
|
|
928
|
+
stringDelimiter = null;
|
|
929
|
+
index += 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
|
|
933
|
+
templateExpressionDepths.push(0);
|
|
934
|
+
stringDelimiter = null;
|
|
935
|
+
index += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (isBlankingString) blankUnlessNewline(index);
|
|
906
939
|
index += 1;
|
|
907
940
|
continue;
|
|
908
941
|
}
|
|
909
|
-
if (character === "\"" || character === "'"
|
|
942
|
+
if (character === "\"" || character === "'") {
|
|
910
943
|
stringDelimiter = character;
|
|
944
|
+
isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
|
|
945
|
+
index += 1;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (character === "`") {
|
|
949
|
+
stringDelimiter = "`";
|
|
950
|
+
isBlankingString = blankStringContents;
|
|
911
951
|
index += 1;
|
|
912
952
|
continue;
|
|
913
953
|
}
|
|
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
|
|
|
926
966
|
index += 2;
|
|
927
967
|
break;
|
|
928
968
|
}
|
|
929
|
-
|
|
969
|
+
blankUnlessNewline(index);
|
|
930
970
|
index += 1;
|
|
931
971
|
}
|
|
932
972
|
continue;
|
|
933
973
|
}
|
|
974
|
+
if (templateExpressionDepths.length > 0) {
|
|
975
|
+
const innermost = templateExpressionDepths.length - 1;
|
|
976
|
+
if (character === "{") templateExpressionDepths[innermost] += 1;
|
|
977
|
+
else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
|
|
978
|
+
templateExpressionDepths.pop();
|
|
979
|
+
stringDelimiter = "`";
|
|
980
|
+
isBlankingString = blankStringContents;
|
|
981
|
+
} else templateExpressionDepths[innermost] -= 1;
|
|
982
|
+
}
|
|
934
983
|
index += 1;
|
|
935
984
|
}
|
|
936
985
|
return characters.join("");
|
|
937
986
|
};
|
|
987
|
+
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
988
|
+
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
938
989
|
//#endregion
|
|
939
990
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
940
991
|
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
941
|
-
const
|
|
992
|
+
const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
993
|
+
const getScannableContent = (file, ignoreStringLiterals = false) => {
|
|
942
994
|
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
943
|
-
const
|
|
995
|
+
const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
|
|
996
|
+
const cachedContent = cache.get(file);
|
|
944
997
|
if (cachedContent !== void 0) return cachedContent;
|
|
945
|
-
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
946
|
-
|
|
998
|
+
const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
|
|
999
|
+
cache.set(file, strippedContent);
|
|
947
1000
|
return strippedContent;
|
|
948
1001
|
};
|
|
949
|
-
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
1002
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
|
|
950
1003
|
if (!shouldScan(file)) return [];
|
|
951
|
-
const content = getScannableContent(file);
|
|
1004
|
+
const content = getScannableContent(file, ignoreStringLiterals);
|
|
952
1005
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
953
1006
|
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
954
1007
|
if (matchedPattern === void 0) return [];
|
|
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
973
1026
|
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
974
1027
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
975
1028
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1029
|
+
ignoreStringLiterals: true,
|
|
976
1030
|
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
977
1031
|
})
|
|
978
1032
|
});
|
|
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1915
|
});
|
|
1862
1916
|
//#endregion
|
|
1863
1917
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1918
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1919
|
const anchorHasContent = defineRule({
|
|
1866
1920
|
id: "anchor-has-content",
|
|
1867
1921
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1931
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1932
|
context.report({
|
|
1879
1933
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1934
|
+
message: MESSAGE$59
|
|
1881
1935
|
});
|
|
1882
1936
|
} })
|
|
1883
1937
|
});
|
|
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2325
|
};
|
|
2272
2326
|
//#endregion
|
|
2273
2327
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2328
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2329
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2330
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2331
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2343
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2344
|
context.report({
|
|
2291
2345
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2346
|
+
message: MESSAGE$58
|
|
2293
2347
|
});
|
|
2294
2348
|
return;
|
|
2295
2349
|
}
|
|
2296
2350
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2351
|
context.report({
|
|
2298
2352
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2353
|
+
message: MESSAGE$58
|
|
2300
2354
|
});
|
|
2301
2355
|
} })
|
|
2302
2356
|
});
|
|
@@ -2965,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
|
|
|
2965
3019
|
"widget",
|
|
2966
3020
|
"window"
|
|
2967
3021
|
]);
|
|
2968
|
-
const PRESENTATION_ROLES$
|
|
3022
|
+
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
2969
3023
|
//#endregion
|
|
2970
3024
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2971
3025
|
const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
|
|
@@ -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$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4276
4330
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
4331
|
const STORAGE_GLOBALS = new Set([
|
|
4278
4332
|
"window",
|
|
@@ -4306,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4306
4360
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
4361
|
context.report({
|
|
4308
4362
|
node,
|
|
4309
|
-
message: MESSAGE$
|
|
4363
|
+
message: MESSAGE$57
|
|
4310
4364
|
});
|
|
4311
4365
|
},
|
|
4312
4366
|
AssignmentExpression(node) {
|
|
@@ -4317,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4317
4371
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
4372
|
context.report({
|
|
4319
4373
|
node: target,
|
|
4320
|
-
message: MESSAGE$
|
|
4374
|
+
message: MESSAGE$57
|
|
4321
4375
|
});
|
|
4322
4376
|
}
|
|
4323
4377
|
})
|
|
@@ -4656,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
4656
4710
|
}
|
|
4657
4711
|
});
|
|
4658
4712
|
//#endregion
|
|
4713
|
+
//#region src/plugin/utils/is-presentation-role.ts
|
|
4714
|
+
const isPresentationRole = (openingElement) => {
|
|
4715
|
+
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
4716
|
+
if (!roleAttribute) return false;
|
|
4717
|
+
const value = getJsxPropStringValue(roleAttribute);
|
|
4718
|
+
return value !== null && PRESENTATION_ROLES$1.has(value);
|
|
4719
|
+
};
|
|
4720
|
+
//#endregion
|
|
4659
4721
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
4660
4722
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
4661
4723
|
"stopPropagation",
|
|
@@ -4693,8 +4755,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4693
4755
|
};
|
|
4694
4756
|
//#endregion
|
|
4695
4757
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
|
-
const
|
|
4697
|
-
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4758
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4759
|
const KEY_HANDLERS = [
|
|
4699
4760
|
"onKeyUp",
|
|
4700
4761
|
"onKeyDown",
|
|
@@ -4718,15 +4779,11 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4718
4779
|
if (!onClick) return;
|
|
4719
4780
|
if (isPureEventBlockerHandler(onClick)) return;
|
|
4720
4781
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
4721
|
-
|
|
4722
|
-
if (roleAttribute) {
|
|
4723
|
-
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
4724
|
-
if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
|
|
4725
|
-
}
|
|
4782
|
+
if (isPresentationRole(node)) return;
|
|
4726
4783
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4784
|
context.report({
|
|
4728
4785
|
node: node.name,
|
|
4729
|
-
message: MESSAGE$
|
|
4786
|
+
message: MESSAGE$56
|
|
4730
4787
|
});
|
|
4731
4788
|
} };
|
|
4732
4789
|
}
|
|
@@ -4841,7 +4898,7 @@ const isReactComponentName = (name) => {
|
|
|
4841
4898
|
};
|
|
4842
4899
|
//#endregion
|
|
4843
4900
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4844
|
-
const MESSAGE$
|
|
4901
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4845
4902
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4846
4903
|
const DEFAULT_LABELLING_PROPS = [
|
|
4847
4904
|
"alt",
|
|
@@ -5002,7 +5059,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5002
5059
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5003
5060
|
context.report({
|
|
5004
5061
|
node: opening,
|
|
5005
|
-
message: MESSAGE$
|
|
5062
|
+
message: MESSAGE$55
|
|
5006
5063
|
});
|
|
5007
5064
|
} };
|
|
5008
5065
|
}
|
|
@@ -5131,7 +5188,6 @@ const dangerousHtmlSink = defineRule({
|
|
|
5131
5188
|
return findings;
|
|
5132
5189
|
}
|
|
5133
5190
|
});
|
|
5134
|
-
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5135
5191
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5136
5192
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5137
5193
|
"continue",
|
|
@@ -5430,10 +5486,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5430
5486
|
});
|
|
5431
5487
|
//#endregion
|
|
5432
5488
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5433
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5489
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5434
5490
|
//#endregion
|
|
5435
5491
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5436
|
-
const MESSAGE$
|
|
5492
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5437
5493
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5438
5494
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5439
5495
|
"aria-label",
|
|
@@ -5452,11 +5508,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5452
5508
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5453
5509
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5454
5510
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5455
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5511
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5456
5512
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5457
5513
|
context.report({
|
|
5458
5514
|
node: node.name,
|
|
5459
|
-
message: MESSAGE$
|
|
5515
|
+
message: MESSAGE$54
|
|
5460
5516
|
});
|
|
5461
5517
|
} })
|
|
5462
5518
|
});
|
|
@@ -5495,7 +5551,7 @@ const isEs6Component = (node) => {
|
|
|
5495
5551
|
};
|
|
5496
5552
|
//#endregion
|
|
5497
5553
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5498
|
-
const MESSAGE$
|
|
5554
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5499
5555
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5500
5556
|
"observer",
|
|
5501
5557
|
"lazy",
|
|
@@ -5694,11 +5750,11 @@ const displayName = defineRule({
|
|
|
5694
5750
|
category: "Architecture",
|
|
5695
5751
|
create: (context) => {
|
|
5696
5752
|
const settings = resolveSettings$44(context.settings);
|
|
5697
|
-
const ignoreNamed = settings.ignoreTranspilerName
|
|
5753
|
+
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
5698
5754
|
const reportAt = (node) => {
|
|
5699
5755
|
context.report({
|
|
5700
5756
|
node,
|
|
5701
|
-
message: MESSAGE$
|
|
5757
|
+
message: MESSAGE$53
|
|
5702
5758
|
});
|
|
5703
5759
|
};
|
|
5704
5760
|
return {
|
|
@@ -7846,7 +7902,7 @@ const forbidElements = defineRule({
|
|
|
7846
7902
|
});
|
|
7847
7903
|
//#endregion
|
|
7848
7904
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7849
|
-
const MESSAGE$
|
|
7905
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7850
7906
|
const forwardRefUsesRef = defineRule({
|
|
7851
7907
|
id: "forward-ref-uses-ref",
|
|
7852
7908
|
title: "forwardRef without ref parameter",
|
|
@@ -7866,7 +7922,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7866
7922
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7867
7923
|
context.report({
|
|
7868
7924
|
node: inner,
|
|
7869
|
-
message: MESSAGE$
|
|
7925
|
+
message: MESSAGE$52
|
|
7870
7926
|
});
|
|
7871
7927
|
} })
|
|
7872
7928
|
});
|
|
@@ -7903,7 +7959,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7903
7959
|
});
|
|
7904
7960
|
//#endregion
|
|
7905
7961
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7906
|
-
const MESSAGE$
|
|
7962
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7907
7963
|
const DEFAULT_HEADING_TAGS = [
|
|
7908
7964
|
"h1",
|
|
7909
7965
|
"h2",
|
|
@@ -7936,7 +7992,7 @@ const headingHasContent = defineRule({
|
|
|
7936
7992
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7937
7993
|
context.report({
|
|
7938
7994
|
node,
|
|
7939
|
-
message: MESSAGE$
|
|
7995
|
+
message: MESSAGE$51
|
|
7940
7996
|
});
|
|
7941
7997
|
} };
|
|
7942
7998
|
}
|
|
@@ -8074,7 +8130,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8074
8130
|
});
|
|
8075
8131
|
//#endregion
|
|
8076
8132
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8077
|
-
const MESSAGE$
|
|
8133
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8078
8134
|
const resolveSettings$38 = (settings) => {
|
|
8079
8135
|
const reactDoctor = settings?.["react-doctor"];
|
|
8080
8136
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8117,26 +8173,17 @@ const htmlHasLang = defineRule({
|
|
|
8117
8173
|
return { JSXOpeningElement(node) {
|
|
8118
8174
|
const tag = getElementType(node, context.settings);
|
|
8119
8175
|
if (!tagSet.has(tag)) return;
|
|
8120
|
-
const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
8121
8176
|
const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
|
|
8122
8177
|
if (!lang) {
|
|
8123
8178
|
context.report({
|
|
8124
8179
|
node: node.name,
|
|
8125
|
-
message: MESSAGE$
|
|
8126
|
-
});
|
|
8127
|
-
return;
|
|
8128
|
-
}
|
|
8129
|
-
const verdict = evaluateLang(lang.value);
|
|
8130
|
-
if (verdict === "missing" || verdict === "empty") {
|
|
8131
|
-
context.report({
|
|
8132
|
-
node: lang,
|
|
8133
|
-
message: MESSAGE$55
|
|
8180
|
+
message: MESSAGE$50
|
|
8134
8181
|
});
|
|
8135
8182
|
return;
|
|
8136
8183
|
}
|
|
8137
|
-
if (
|
|
8138
|
-
node:
|
|
8139
|
-
message: MESSAGE$
|
|
8184
|
+
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8185
|
+
node: lang,
|
|
8186
|
+
message: MESSAGE$50
|
|
8140
8187
|
});
|
|
8141
8188
|
} };
|
|
8142
8189
|
}
|
|
@@ -8350,7 +8397,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8350
8397
|
});
|
|
8351
8398
|
//#endregion
|
|
8352
8399
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8353
|
-
const MESSAGE$
|
|
8400
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8354
8401
|
const evaluateTitleValue = (value) => {
|
|
8355
8402
|
if (!value) return "missing";
|
|
8356
8403
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8390,14 +8437,14 @@ const iframeHasTitle = defineRule({
|
|
|
8390
8437
|
if (!titleAttr) {
|
|
8391
8438
|
if (hasSpread || tag === "iframe") context.report({
|
|
8392
8439
|
node: node.name,
|
|
8393
|
-
message: MESSAGE$
|
|
8440
|
+
message: MESSAGE$49
|
|
8394
8441
|
});
|
|
8395
8442
|
return;
|
|
8396
8443
|
}
|
|
8397
8444
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8398
8445
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8399
8446
|
node: titleAttr,
|
|
8400
|
-
message: MESSAGE$
|
|
8447
|
+
message: MESSAGE$49
|
|
8401
8448
|
});
|
|
8402
8449
|
} })
|
|
8403
8450
|
});
|
|
@@ -8501,7 +8548,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8501
8548
|
});
|
|
8502
8549
|
//#endregion
|
|
8503
8550
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8504
|
-
const MESSAGE$
|
|
8551
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8505
8552
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8506
8553
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8507
8554
|
"image",
|
|
@@ -8566,7 +8613,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8566
8613
|
if (!altAttribute) return;
|
|
8567
8614
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8568
8615
|
node: altAttribute,
|
|
8569
|
-
message: MESSAGE$
|
|
8616
|
+
message: MESSAGE$48
|
|
8570
8617
|
});
|
|
8571
8618
|
} };
|
|
8572
8619
|
}
|
|
@@ -8849,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
|
|
|
8849
8896
|
//#region src/plugin/utils/is-non-interactive-role.ts
|
|
8850
8897
|
const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
|
|
8851
8898
|
//#endregion
|
|
8852
|
-
//#region src/plugin/utils/is-presentation-role.ts
|
|
8853
|
-
const isPresentationRole = (openingElement) => {
|
|
8854
|
-
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
8855
|
-
if (!roleAttribute) return false;
|
|
8856
|
-
const value = getJsxPropStringValue(roleAttribute);
|
|
8857
|
-
return value !== null && PRESENTATION_ROLES$2.has(value);
|
|
8858
|
-
};
|
|
8859
|
-
//#endregion
|
|
8860
8899
|
//#region src/plugin/rules/a11y/interactive-supports-focus.ts
|
|
8861
8900
|
const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
|
|
8862
8901
|
const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
|
|
@@ -10631,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10631
10670
|
return false;
|
|
10632
10671
|
};
|
|
10633
10672
|
//#endregion
|
|
10673
|
+
//#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
|
|
10674
|
+
const ascendThroughJsxValueWrappers = (node) => {
|
|
10675
|
+
let current = node;
|
|
10676
|
+
while (current.parent) {
|
|
10677
|
+
const parent = current.parent;
|
|
10678
|
+
if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
|
|
10679
|
+
current = parent;
|
|
10680
|
+
}
|
|
10681
|
+
return current;
|
|
10682
|
+
};
|
|
10683
|
+
const isNonChildrenJsxAttributeValue = (node) => {
|
|
10684
|
+
const container = ascendThroughJsxValueWrappers(node).parent;
|
|
10685
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
|
|
10686
|
+
const attribute = container.parent;
|
|
10687
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
|
|
10688
|
+
return getJsxAttributeName(attribute.name) !== "children";
|
|
10689
|
+
};
|
|
10690
|
+
//#endregion
|
|
10634
10691
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10635
10692
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10636
10693
|
"map",
|
|
@@ -10669,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10669
10726
|
const arrayParent = parent.parent;
|
|
10670
10727
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10671
10728
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10729
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10672
10730
|
return { kind: "array" };
|
|
10673
10731
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10674
10732
|
const callee = parent.callee;
|
|
@@ -10681,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10681
10739
|
if (!targetArg) return null;
|
|
10682
10740
|
let walker = current;
|
|
10683
10741
|
while (walker && walker !== parent) {
|
|
10684
|
-
if (walker === targetArg)
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10742
|
+
if (walker === targetArg) {
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
kind: "iterator",
|
|
10746
|
+
callExpression: parent
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10688
10749
|
walker = walker.parent ?? null;
|
|
10689
10750
|
}
|
|
10690
10751
|
return null;
|
|
@@ -10923,7 +10984,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10923
10984
|
});
|
|
10924
10985
|
//#endregion
|
|
10925
10986
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10926
|
-
const MESSAGE$
|
|
10987
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10927
10988
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10928
10989
|
"code",
|
|
10929
10990
|
"pre",
|
|
@@ -10959,7 +11020,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10959
11020
|
if (isInsideLiteralTextTag(node)) return;
|
|
10960
11021
|
context.report({
|
|
10961
11022
|
node,
|
|
10962
|
-
message: MESSAGE$
|
|
11023
|
+
message: MESSAGE$47
|
|
10963
11024
|
});
|
|
10964
11025
|
} })
|
|
10965
11026
|
});
|
|
@@ -10990,7 +11051,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10990
11051
|
};
|
|
10991
11052
|
//#endregion
|
|
10992
11053
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10993
|
-
const MESSAGE$
|
|
11054
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10994
11055
|
const CONTEXT_MODULES$1 = [
|
|
10995
11056
|
"react",
|
|
10996
11057
|
"use-context-selector",
|
|
@@ -11088,7 +11149,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11088
11149
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11089
11150
|
context.report({
|
|
11090
11151
|
node: attribute,
|
|
11091
|
-
message: MESSAGE$
|
|
11152
|
+
message: MESSAGE$46
|
|
11092
11153
|
});
|
|
11093
11154
|
}
|
|
11094
11155
|
}
|
|
@@ -11174,7 +11235,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11174
11235
|
};
|
|
11175
11236
|
//#endregion
|
|
11176
11237
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11177
|
-
const MESSAGE$
|
|
11238
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11178
11239
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11179
11240
|
"icon",
|
|
11180
11241
|
"Icon",
|
|
@@ -11443,7 +11504,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11443
11504
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11444
11505
|
context.report({
|
|
11445
11506
|
node,
|
|
11446
|
-
message: MESSAGE$
|
|
11507
|
+
message: MESSAGE$45
|
|
11447
11508
|
});
|
|
11448
11509
|
}
|
|
11449
11510
|
};
|
|
@@ -11731,7 +11792,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11731
11792
|
];
|
|
11732
11793
|
//#endregion
|
|
11733
11794
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11734
|
-
const MESSAGE$
|
|
11795
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11735
11796
|
const isDataArrayPropName = (propName) => {
|
|
11736
11797
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11737
11798
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11815,7 +11876,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11815
11876
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11816
11877
|
context.report({
|
|
11817
11878
|
node,
|
|
11818
|
-
message: MESSAGE$
|
|
11879
|
+
message: MESSAGE$44
|
|
11819
11880
|
});
|
|
11820
11881
|
}
|
|
11821
11882
|
};
|
|
@@ -12073,7 +12134,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12073
12134
|
]);
|
|
12074
12135
|
//#endregion
|
|
12075
12136
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12076
|
-
const MESSAGE$
|
|
12137
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12077
12138
|
const isAccessorPredicateName = (propName) => {
|
|
12078
12139
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12079
12140
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12279,7 +12340,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12279
12340
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12280
12341
|
context.report({
|
|
12281
12342
|
node,
|
|
12282
|
-
message: MESSAGE$
|
|
12343
|
+
message: MESSAGE$43
|
|
12283
12344
|
});
|
|
12284
12345
|
}
|
|
12285
12346
|
};
|
|
@@ -12499,7 +12560,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12499
12560
|
];
|
|
12500
12561
|
//#endregion
|
|
12501
12562
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12502
|
-
const MESSAGE$
|
|
12563
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12503
12564
|
const isConfigObjectPropName = (propName) => {
|
|
12504
12565
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12505
12566
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12587,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12587
12648
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12588
12649
|
context.report({
|
|
12589
12650
|
node,
|
|
12590
|
-
message: MESSAGE$
|
|
12651
|
+
message: MESSAGE$42
|
|
12591
12652
|
});
|
|
12592
12653
|
}
|
|
12593
12654
|
};
|
|
@@ -12595,7 +12656,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12595
12656
|
});
|
|
12596
12657
|
//#endregion
|
|
12597
12658
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12598
|
-
const MESSAGE$
|
|
12659
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12599
12660
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12600
12661
|
const resolveSettings$28 = (settings) => {
|
|
12601
12662
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12636,7 +12697,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12636
12697
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12637
12698
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12638
12699
|
node: attribute,
|
|
12639
|
-
message: MESSAGE$
|
|
12700
|
+
message: MESSAGE$41
|
|
12640
12701
|
});
|
|
12641
12702
|
}
|
|
12642
12703
|
} };
|
|
@@ -12951,7 +13012,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12951
13012
|
});
|
|
12952
13013
|
//#endregion
|
|
12953
13014
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12954
|
-
const MESSAGE$
|
|
13015
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12955
13016
|
const resolveSettings$25 = (settings) => {
|
|
12956
13017
|
const reactDoctor = settings?.["react-doctor"];
|
|
12957
13018
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12992,7 +13053,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12992
13053
|
}
|
|
12993
13054
|
context.report({
|
|
12994
13055
|
node: attribute,
|
|
12995
|
-
message: MESSAGE$
|
|
13056
|
+
message: MESSAGE$40
|
|
12996
13057
|
});
|
|
12997
13058
|
}
|
|
12998
13059
|
} };
|
|
@@ -13220,7 +13281,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13220
13281
|
});
|
|
13221
13282
|
//#endregion
|
|
13222
13283
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13223
|
-
const MESSAGE$
|
|
13284
|
+
const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13224
13285
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13225
13286
|
"aa",
|
|
13226
13287
|
"ab",
|
|
@@ -13432,7 +13493,7 @@ const lang = defineRule({
|
|
|
13432
13493
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13433
13494
|
context.report({
|
|
13434
13495
|
node: langAttr,
|
|
13435
|
-
message: MESSAGE$
|
|
13496
|
+
message: MESSAGE$39
|
|
13436
13497
|
});
|
|
13437
13498
|
return;
|
|
13438
13499
|
}
|
|
@@ -13441,7 +13502,7 @@ const lang = defineRule({
|
|
|
13441
13502
|
if (value === null) return;
|
|
13442
13503
|
if (!isValidLangTag(value)) context.report({
|
|
13443
13504
|
node: langAttr,
|
|
13444
|
-
message: MESSAGE$
|
|
13505
|
+
message: MESSAGE$39
|
|
13445
13506
|
});
|
|
13446
13507
|
} })
|
|
13447
13508
|
});
|
|
@@ -13467,6 +13528,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13467
13528
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13468
13529
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13469
13530
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13531
|
+
ignoreStringLiterals: true,
|
|
13470
13532
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13471
13533
|
})
|
|
13472
13534
|
});
|
|
@@ -13485,7 +13547,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13485
13547
|
});
|
|
13486
13548
|
//#endregion
|
|
13487
13549
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13488
|
-
const MESSAGE$
|
|
13550
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13489
13551
|
const DEFAULT_AUDIO = ["audio"];
|
|
13490
13552
|
const DEFAULT_VIDEO = ["video"];
|
|
13491
13553
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13526,7 +13588,7 @@ const mediaHasCaption = defineRule({
|
|
|
13526
13588
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13527
13589
|
context.report({
|
|
13528
13590
|
node: node.name,
|
|
13529
|
-
message: MESSAGE$
|
|
13591
|
+
message: MESSAGE$38
|
|
13530
13592
|
});
|
|
13531
13593
|
return;
|
|
13532
13594
|
}
|
|
@@ -13543,7 +13605,7 @@ const mediaHasCaption = defineRule({
|
|
|
13543
13605
|
return kindValue.value.toLowerCase() === "captions";
|
|
13544
13606
|
})) context.report({
|
|
13545
13607
|
node: node.name,
|
|
13546
|
-
message: MESSAGE$
|
|
13608
|
+
message: MESSAGE$38
|
|
13547
13609
|
});
|
|
13548
13610
|
} };
|
|
13549
13611
|
}
|
|
@@ -15344,7 +15406,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15344
15406
|
});
|
|
15345
15407
|
//#endregion
|
|
15346
15408
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15347
|
-
const MESSAGE$
|
|
15409
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15348
15410
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15349
15411
|
const noAccessKey = defineRule({
|
|
15350
15412
|
id: "no-access-key",
|
|
@@ -15361,7 +15423,7 @@ const noAccessKey = defineRule({
|
|
|
15361
15423
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15362
15424
|
context.report({
|
|
15363
15425
|
node: accessKey,
|
|
15364
|
-
message: MESSAGE$
|
|
15426
|
+
message: MESSAGE$37
|
|
15365
15427
|
});
|
|
15366
15428
|
return;
|
|
15367
15429
|
}
|
|
@@ -15371,7 +15433,7 @@ const noAccessKey = defineRule({
|
|
|
15371
15433
|
if (isUndefinedIdentifier(expression)) return;
|
|
15372
15434
|
context.report({
|
|
15373
15435
|
node: accessKey,
|
|
15374
|
-
message: MESSAGE$
|
|
15436
|
+
message: MESSAGE$37
|
|
15375
15437
|
});
|
|
15376
15438
|
}
|
|
15377
15439
|
} })
|
|
@@ -15853,41 +15915,8 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15853
15915
|
} })
|
|
15854
15916
|
});
|
|
15855
15917
|
//#endregion
|
|
15856
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15857
|
-
const getStringFromClassNameAttr = (node) => {
|
|
15858
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15859
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15860
|
-
if (!classAttr?.value) return null;
|
|
15861
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15862
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15863
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15864
|
-
return null;
|
|
15865
|
-
};
|
|
15866
|
-
//#endregion
|
|
15867
|
-
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15868
|
-
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15869
|
-
const noArbitraryPxFontSize = defineRule({
|
|
15870
|
-
id: "no-arbitrary-px-font-size",
|
|
15871
|
-
title: "Pixel arbitrary font size",
|
|
15872
|
-
tags: ["design", "test-noise"],
|
|
15873
|
-
severity: "warn",
|
|
15874
|
-
category: "Accessibility",
|
|
15875
|
-
recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
|
|
15876
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
15877
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
15878
|
-
if (!classNameValue) return;
|
|
15879
|
-
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15880
|
-
const rem = parseFloat(match[1]) / 16;
|
|
15881
|
-
context.report({
|
|
15882
|
-
node,
|
|
15883
|
-
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15884
|
-
});
|
|
15885
|
-
}
|
|
15886
|
-
} })
|
|
15887
|
-
});
|
|
15888
|
-
//#endregion
|
|
15889
15918
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15890
|
-
const MESSAGE$
|
|
15919
|
+
const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15891
15920
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15892
15921
|
id: "no-aria-hidden-on-focusable",
|
|
15893
15922
|
title: "aria-hidden on focusable element",
|
|
@@ -15914,7 +15943,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15914
15943
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15915
15944
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15916
15945
|
node: ariaHidden,
|
|
15917
|
-
message: MESSAGE$
|
|
15946
|
+
message: MESSAGE$36
|
|
15918
15947
|
});
|
|
15919
15948
|
} })
|
|
15920
15949
|
});
|
|
@@ -16282,7 +16311,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16282
16311
|
});
|
|
16283
16312
|
//#endregion
|
|
16284
16313
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16285
|
-
const MESSAGE$
|
|
16314
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16286
16315
|
const SECOND_INDEX_METHODS = new Set([
|
|
16287
16316
|
"every",
|
|
16288
16317
|
"filter",
|
|
@@ -16486,7 +16515,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16486
16515
|
}
|
|
16487
16516
|
context.report({
|
|
16488
16517
|
node: keyAttribute,
|
|
16489
|
-
message: MESSAGE$
|
|
16518
|
+
message: MESSAGE$35
|
|
16490
16519
|
});
|
|
16491
16520
|
},
|
|
16492
16521
|
CallExpression(node) {
|
|
@@ -16506,7 +16535,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16506
16535
|
if (propName !== "key") continue;
|
|
16507
16536
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16508
16537
|
node: property,
|
|
16509
|
-
message: MESSAGE$
|
|
16538
|
+
message: MESSAGE$35
|
|
16510
16539
|
});
|
|
16511
16540
|
}
|
|
16512
16541
|
}
|
|
@@ -16514,7 +16543,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16514
16543
|
});
|
|
16515
16544
|
//#endregion
|
|
16516
16545
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16517
|
-
const MESSAGE$
|
|
16546
|
+
const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16518
16547
|
const noAsyncEffectCallback = defineRule({
|
|
16519
16548
|
id: "no-async-effect-callback",
|
|
16520
16549
|
title: "Async effect callback",
|
|
@@ -16528,13 +16557,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16528
16557
|
if (!callback.async) return;
|
|
16529
16558
|
context.report({
|
|
16530
16559
|
node: callback,
|
|
16531
|
-
message: MESSAGE$
|
|
16560
|
+
message: MESSAGE$34
|
|
16532
16561
|
});
|
|
16533
16562
|
} })
|
|
16534
16563
|
});
|
|
16535
16564
|
//#endregion
|
|
16536
16565
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16537
|
-
const MESSAGE$
|
|
16566
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16538
16567
|
const resolveSettings$21 = (settings) => {
|
|
16539
16568
|
const reactDoctor = settings?.["react-doctor"];
|
|
16540
16569
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16590,45 +16619,12 @@ const noAutofocus = defineRule({
|
|
|
16590
16619
|
}
|
|
16591
16620
|
context.report({
|
|
16592
16621
|
node: autoFocusAttribute,
|
|
16593
|
-
message: MESSAGE$
|
|
16622
|
+
message: MESSAGE$33
|
|
16594
16623
|
});
|
|
16595
16624
|
} };
|
|
16596
16625
|
}
|
|
16597
16626
|
});
|
|
16598
16627
|
//#endregion
|
|
16599
|
-
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16600
|
-
const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
|
|
16601
|
-
const resolveStaticBoolean = (attribute) => {
|
|
16602
|
-
const value = attribute.value;
|
|
16603
|
-
if (!value) return true;
|
|
16604
|
-
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16605
|
-
if (isNodeOfType(literal, "Literal")) {
|
|
16606
|
-
if (literal.value === true || literal.value === "true") return true;
|
|
16607
|
-
if (literal.value === false || literal.value === "false") return false;
|
|
16608
|
-
}
|
|
16609
|
-
return null;
|
|
16610
|
-
};
|
|
16611
|
-
const noAutoplayWithoutMuted = defineRule({
|
|
16612
|
-
id: "no-autoplay-without-muted",
|
|
16613
|
-
title: "Autoplaying media without muted",
|
|
16614
|
-
severity: "warn",
|
|
16615
|
-
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16616
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
16617
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16618
|
-
const tagName = node.name.name;
|
|
16619
|
-
if (tagName !== "video" && tagName !== "audio") return;
|
|
16620
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16621
|
-
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16622
|
-
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16623
|
-
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16624
|
-
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16625
|
-
context.report({
|
|
16626
|
-
node: node.name,
|
|
16627
|
-
message: MESSAGE$37
|
|
16628
|
-
});
|
|
16629
|
-
} })
|
|
16630
|
-
});
|
|
16631
|
-
//#endregion
|
|
16632
16628
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16633
16629
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16634
16630
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -17127,7 +17123,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17127
17123
|
});
|
|
17128
17124
|
//#endregion
|
|
17129
17125
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17130
|
-
const MESSAGE$
|
|
17126
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17131
17127
|
const noChildrenProp = defineRule({
|
|
17132
17128
|
id: "no-children-prop",
|
|
17133
17129
|
title: "Children passed as a prop",
|
|
@@ -17139,7 +17135,7 @@ const noChildrenProp = defineRule({
|
|
|
17139
17135
|
if (node.name.name !== "children") return;
|
|
17140
17136
|
context.report({
|
|
17141
17137
|
node: node.name,
|
|
17142
|
-
message: MESSAGE$
|
|
17138
|
+
message: MESSAGE$32
|
|
17143
17139
|
});
|
|
17144
17140
|
},
|
|
17145
17141
|
CallExpression(node) {
|
|
@@ -17152,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
17152
17148
|
const propertyKey = property.key;
|
|
17153
17149
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17154
17150
|
node: propertyKey,
|
|
17155
|
-
message: MESSAGE$
|
|
17151
|
+
message: MESSAGE$32
|
|
17156
17152
|
});
|
|
17157
17153
|
}
|
|
17158
17154
|
}
|
|
@@ -17160,7 +17156,7 @@ const noChildrenProp = defineRule({
|
|
|
17160
17156
|
});
|
|
17161
17157
|
//#endregion
|
|
17162
17158
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17163
|
-
const MESSAGE$
|
|
17159
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17164
17160
|
const noCloneElement = defineRule({
|
|
17165
17161
|
id: "no-clone-element",
|
|
17166
17162
|
title: "cloneElement makes child props fragile",
|
|
@@ -17173,7 +17169,7 @@ const noCloneElement = defineRule({
|
|
|
17173
17169
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17174
17170
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17175
17171
|
node: callee,
|
|
17176
|
-
message: MESSAGE$
|
|
17172
|
+
message: MESSAGE$31
|
|
17177
17173
|
});
|
|
17178
17174
|
return;
|
|
17179
17175
|
}
|
|
@@ -17186,7 +17182,7 @@ const noCloneElement = defineRule({
|
|
|
17186
17182
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17187
17183
|
context.report({
|
|
17188
17184
|
node: callee,
|
|
17189
|
-
message: MESSAGE$
|
|
17185
|
+
message: MESSAGE$31
|
|
17190
17186
|
});
|
|
17191
17187
|
}
|
|
17192
17188
|
} })
|
|
@@ -17235,7 +17231,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17235
17231
|
};
|
|
17236
17232
|
//#endregion
|
|
17237
17233
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17238
|
-
const MESSAGE$
|
|
17234
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17239
17235
|
const CONTEXT_MODULES = [
|
|
17240
17236
|
"react",
|
|
17241
17237
|
"use-context-selector",
|
|
@@ -17271,13 +17267,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17271
17267
|
if (!componentOrHookName) return;
|
|
17272
17268
|
context.report({
|
|
17273
17269
|
node,
|
|
17274
|
-
message: `${MESSAGE$
|
|
17270
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17275
17271
|
});
|
|
17276
17272
|
} })
|
|
17277
17273
|
});
|
|
17278
17274
|
//#endregion
|
|
17279
17275
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17280
|
-
const MESSAGE$
|
|
17276
|
+
const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17281
17277
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17282
17278
|
id: "no-create-ref-in-function-component",
|
|
17283
17279
|
title: "createRef in function component",
|
|
@@ -17296,7 +17292,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17296
17292
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17297
17293
|
context.report({
|
|
17298
17294
|
node,
|
|
17299
|
-
message: MESSAGE$
|
|
17295
|
+
message: MESSAGE$29
|
|
17300
17296
|
});
|
|
17301
17297
|
} })
|
|
17302
17298
|
});
|
|
@@ -17436,12 +17432,13 @@ const noCreateStoreInRender = defineRule({
|
|
|
17436
17432
|
});
|
|
17437
17433
|
//#endregion
|
|
17438
17434
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17439
|
-
const MESSAGE$
|
|
17435
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17440
17436
|
const noDanger = defineRule({
|
|
17441
17437
|
id: "no-danger",
|
|
17442
17438
|
title: "Raw HTML injection can run unsafe markup",
|
|
17443
17439
|
severity: "warn",
|
|
17444
17440
|
category: "Security",
|
|
17441
|
+
defaultEnabled: false,
|
|
17445
17442
|
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
17446
17443
|
create: (context) => ({
|
|
17447
17444
|
JSXOpeningElement(node) {
|
|
@@ -17449,7 +17446,7 @@ const noDanger = defineRule({
|
|
|
17449
17446
|
if (!propAttribute) return;
|
|
17450
17447
|
context.report({
|
|
17451
17448
|
node: propAttribute.name,
|
|
17452
|
-
message: MESSAGE$
|
|
17449
|
+
message: MESSAGE$28
|
|
17453
17450
|
});
|
|
17454
17451
|
},
|
|
17455
17452
|
CallExpression(node) {
|
|
@@ -17461,7 +17458,7 @@ const noDanger = defineRule({
|
|
|
17461
17458
|
const propertyKey = property.key;
|
|
17462
17459
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17463
17460
|
node: propertyKey,
|
|
17464
|
-
message: MESSAGE$
|
|
17461
|
+
message: MESSAGE$28
|
|
17465
17462
|
});
|
|
17466
17463
|
}
|
|
17467
17464
|
}
|
|
@@ -17469,7 +17466,7 @@ const noDanger = defineRule({
|
|
|
17469
17466
|
});
|
|
17470
17467
|
//#endregion
|
|
17471
17468
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17472
|
-
const MESSAGE$
|
|
17469
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17473
17470
|
const isLineBreak = (child) => {
|
|
17474
17471
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17475
17472
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17539,7 +17536,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17539
17536
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17540
17537
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17541
17538
|
node: opening,
|
|
17542
|
-
message: MESSAGE$
|
|
17539
|
+
message: MESSAGE$27
|
|
17543
17540
|
});
|
|
17544
17541
|
},
|
|
17545
17542
|
CallExpression(node) {
|
|
@@ -17551,7 +17548,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17551
17548
|
if (!propsShape.hasDangerously) return;
|
|
17552
17549
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17553
17550
|
node,
|
|
17554
|
-
message: MESSAGE$
|
|
17551
|
+
message: MESSAGE$27
|
|
17555
17552
|
});
|
|
17556
17553
|
}
|
|
17557
17554
|
})
|
|
@@ -17716,37 +17713,6 @@ const noDefaultProps = defineRule({
|
|
|
17716
17713
|
} })
|
|
17717
17714
|
});
|
|
17718
17715
|
//#endregion
|
|
17719
|
-
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17720
|
-
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17721
|
-
//#endregion
|
|
17722
|
-
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17723
|
-
const renameDeprecatedToken = (token) => {
|
|
17724
|
-
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17725
|
-
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17726
|
-
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17727
|
-
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17728
|
-
return null;
|
|
17729
|
-
};
|
|
17730
|
-
const noDeprecatedTailwindClass = defineRule({
|
|
17731
|
-
id: "no-deprecated-tailwind-class",
|
|
17732
|
-
title: "Deprecated Tailwind v4 utility",
|
|
17733
|
-
tags: ["design", "test-noise"],
|
|
17734
|
-
severity: "warn",
|
|
17735
|
-
requires: ["tailwind:4"],
|
|
17736
|
-
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17737
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
17738
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
17739
|
-
if (!classNameValue) return;
|
|
17740
|
-
for (const token of getClassNameTokens(classNameValue)) {
|
|
17741
|
-
const replacement = renameDeprecatedToken(token);
|
|
17742
|
-
if (replacement) context.report({
|
|
17743
|
-
node,
|
|
17744
|
-
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17745
|
-
});
|
|
17746
|
-
}
|
|
17747
|
-
} })
|
|
17748
|
-
});
|
|
17749
|
-
//#endregion
|
|
17750
17716
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17751
17717
|
const isInitialOnlyPropName = (propName) => {
|
|
17752
17718
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -18159,7 +18125,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18159
18125
|
//#endregion
|
|
18160
18126
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18161
18127
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18162
|
-
const MESSAGE$
|
|
18128
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18163
18129
|
const resolveSettings$20 = (settings) => {
|
|
18164
18130
|
const reactDoctor = settings?.["react-doctor"];
|
|
18165
18131
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18178,7 +18144,7 @@ const noDidMountSetState = defineRule({
|
|
|
18178
18144
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18179
18145
|
context.report({
|
|
18180
18146
|
node: node.callee,
|
|
18181
|
-
message: MESSAGE$
|
|
18147
|
+
message: MESSAGE$26
|
|
18182
18148
|
});
|
|
18183
18149
|
} };
|
|
18184
18150
|
}
|
|
@@ -18186,7 +18152,7 @@ const noDidMountSetState = defineRule({
|
|
|
18186
18152
|
//#endregion
|
|
18187
18153
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18188
18154
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18189
|
-
const MESSAGE$
|
|
18155
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18190
18156
|
const resolveSettings$19 = (settings) => {
|
|
18191
18157
|
const reactDoctor = settings?.["react-doctor"];
|
|
18192
18158
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18205,7 +18171,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18205
18171
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18206
18172
|
context.report({
|
|
18207
18173
|
node: node.callee,
|
|
18208
|
-
message: MESSAGE$
|
|
18174
|
+
message: MESSAGE$25
|
|
18209
18175
|
});
|
|
18210
18176
|
} };
|
|
18211
18177
|
}
|
|
@@ -18228,7 +18194,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18228
18194
|
};
|
|
18229
18195
|
//#endregion
|
|
18230
18196
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18231
|
-
const MESSAGE$
|
|
18197
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18232
18198
|
const shouldIgnoreMutation = (node) => {
|
|
18233
18199
|
let isConstructor = false;
|
|
18234
18200
|
let isInsideCallExpression = false;
|
|
@@ -18250,7 +18216,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18250
18216
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18251
18217
|
context.report({
|
|
18252
18218
|
node: reportNode,
|
|
18253
|
-
message: MESSAGE$
|
|
18219
|
+
message: MESSAGE$24
|
|
18254
18220
|
});
|
|
18255
18221
|
};
|
|
18256
18222
|
const noDirectMutationState = defineRule({
|
|
@@ -18461,7 +18427,7 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18461
18427
|
});
|
|
18462
18428
|
//#endregion
|
|
18463
18429
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18464
|
-
const MESSAGE$
|
|
18430
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18465
18431
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18466
18432
|
const noDocumentWrite = defineRule({
|
|
18467
18433
|
id: "no-document-write",
|
|
@@ -18475,7 +18441,7 @@ const noDocumentWrite = defineRule({
|
|
|
18475
18441
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18476
18442
|
context.report({
|
|
18477
18443
|
node,
|
|
18478
|
-
message: MESSAGE$
|
|
18444
|
+
message: MESSAGE$23
|
|
18479
18445
|
});
|
|
18480
18446
|
} })
|
|
18481
18447
|
});
|
|
@@ -19858,7 +19824,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19858
19824
|
"ReactDOM",
|
|
19859
19825
|
"ReactDom"
|
|
19860
19826
|
]);
|
|
19861
|
-
const MESSAGE$
|
|
19827
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19862
19828
|
const noFindDomNode = defineRule({
|
|
19863
19829
|
id: "no-find-dom-node",
|
|
19864
19830
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19869,7 +19835,7 @@ const noFindDomNode = defineRule({
|
|
|
19869
19835
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19870
19836
|
context.report({
|
|
19871
19837
|
node: callee,
|
|
19872
|
-
message: MESSAGE$
|
|
19838
|
+
message: MESSAGE$22
|
|
19873
19839
|
});
|
|
19874
19840
|
return;
|
|
19875
19841
|
}
|
|
@@ -19880,7 +19846,7 @@ const noFindDomNode = defineRule({
|
|
|
19880
19846
|
if (callee.property.name !== "findDOMNode") return;
|
|
19881
19847
|
context.report({
|
|
19882
19848
|
node: callee.property,
|
|
19883
|
-
message: MESSAGE$
|
|
19849
|
+
message: MESSAGE$22
|
|
19884
19850
|
});
|
|
19885
19851
|
}
|
|
19886
19852
|
} })
|
|
@@ -19921,41 +19887,6 @@ const noFullLodashImport = defineRule({
|
|
|
19921
19887
|
} })
|
|
19922
19888
|
});
|
|
19923
19889
|
//#endregion
|
|
19924
|
-
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19925
|
-
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19926
|
-
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19927
|
-
const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
|
|
19928
|
-
const noFullViewportWidth = defineRule({
|
|
19929
|
-
id: "no-full-viewport-width",
|
|
19930
|
-
title: "Full viewport width causes overflow",
|
|
19931
|
-
tags: ["design", "test-noise"],
|
|
19932
|
-
severity: "warn",
|
|
19933
|
-
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19934
|
-
create: (context) => ({
|
|
19935
|
-
JSXAttribute(node) {
|
|
19936
|
-
const expression = getInlineStyleExpression(node);
|
|
19937
|
-
if (!expression) return;
|
|
19938
|
-
for (const property of expression.properties ?? []) {
|
|
19939
|
-
const key = getStylePropertyKey(property);
|
|
19940
|
-
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19941
|
-
const value = getStylePropertyStringValue(property);
|
|
19942
|
-
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19943
|
-
node: property,
|
|
19944
|
-
message: MESSAGE$25
|
|
19945
|
-
});
|
|
19946
|
-
}
|
|
19947
|
-
},
|
|
19948
|
-
JSXOpeningElement(node) {
|
|
19949
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
19950
|
-
if (!classNameValue) return;
|
|
19951
|
-
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
19952
|
-
node,
|
|
19953
|
-
message: MESSAGE$25
|
|
19954
|
-
});
|
|
19955
|
-
}
|
|
19956
|
-
})
|
|
19957
|
-
});
|
|
19958
|
-
//#endregion
|
|
19959
19890
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19960
19891
|
const noGenericHandlerNames = defineRule({
|
|
19961
19892
|
id: "no-generic-handler-names",
|
|
@@ -20018,7 +19949,7 @@ const noGiantComponent = defineRule({
|
|
|
20018
19949
|
});
|
|
20019
19950
|
//#endregion
|
|
20020
19951
|
//#region src/plugin/constants/style.ts
|
|
20021
|
-
const LAYOUT_PROPERTIES
|
|
19952
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
20022
19953
|
"width",
|
|
20023
19954
|
"height",
|
|
20024
19955
|
"top",
|
|
@@ -20088,6 +20019,17 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
20088
20019
|
} })
|
|
20089
20020
|
});
|
|
20090
20021
|
//#endregion
|
|
20022
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
20023
|
+
const getStringFromClassNameAttr = (node) => {
|
|
20024
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
20025
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
20026
|
+
if (!classAttr?.value) return null;
|
|
20027
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
20028
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
20029
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
20030
|
+
return null;
|
|
20031
|
+
};
|
|
20032
|
+
//#endregion
|
|
20091
20033
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
20092
20034
|
const noGradientText = defineRule({
|
|
20093
20035
|
id: "no-gradient-text",
|
|
@@ -20146,7 +20088,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20146
20088
|
});
|
|
20147
20089
|
//#endregion
|
|
20148
20090
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20149
|
-
const MESSAGE$
|
|
20091
|
+
const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20150
20092
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20151
20093
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20152
20094
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20160,7 +20102,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20160
20102
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20161
20103
|
context.report({
|
|
20162
20104
|
node: node.name,
|
|
20163
|
-
message: MESSAGE$
|
|
20105
|
+
message: MESSAGE$21
|
|
20164
20106
|
});
|
|
20165
20107
|
} })
|
|
20166
20108
|
});
|
|
@@ -20257,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
20257
20199
|
severity: "warn",
|
|
20258
20200
|
tags: ["test-noise", "react-jsx-only"],
|
|
20259
20201
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
20260
|
-
create: (context) =>
|
|
20261
|
-
|
|
20262
|
-
|
|
20263
|
-
|
|
20264
|
-
|
|
20265
|
-
|
|
20266
|
-
|
|
20267
|
-
|
|
20268
|
-
|
|
20202
|
+
create: (context) => {
|
|
20203
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20204
|
+
return { JSXAttribute(node) {
|
|
20205
|
+
const expression = getInlineStyleExpression(node);
|
|
20206
|
+
if (!expression) return;
|
|
20207
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20208
|
+
if (propertyCount < 8) return;
|
|
20209
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20210
|
+
context.report({
|
|
20211
|
+
node: expression,
|
|
20212
|
+
message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
|
|
20213
|
+
});
|
|
20214
|
+
} };
|
|
20215
|
+
}
|
|
20269
20216
|
});
|
|
20270
20217
|
//#endregion
|
|
20271
20218
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -20395,7 +20342,7 @@ const noIsMounted = defineRule({
|
|
|
20395
20342
|
});
|
|
20396
20343
|
//#endregion
|
|
20397
20344
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20398
|
-
const MESSAGE$
|
|
20345
|
+
const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20399
20346
|
const isJsonMethodCall = (node, method) => {
|
|
20400
20347
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20401
20348
|
const callee = node.callee;
|
|
@@ -20412,13 +20359,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20412
20359
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20413
20360
|
context.report({
|
|
20414
20361
|
node,
|
|
20415
|
-
message: MESSAGE$
|
|
20362
|
+
message: MESSAGE$20
|
|
20416
20363
|
});
|
|
20417
20364
|
} })
|
|
20418
20365
|
});
|
|
20419
20366
|
//#endregion
|
|
20420
20367
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20421
|
-
const MESSAGE$
|
|
20368
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20422
20369
|
const isJsxElementTypeReference = (node) => {
|
|
20423
20370
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20424
20371
|
const typeName = node.typeName;
|
|
@@ -20435,7 +20382,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20435
20382
|
if (!typeAnnotation) return;
|
|
20436
20383
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20437
20384
|
node: typeAnnotation,
|
|
20438
|
-
message: MESSAGE$
|
|
20385
|
+
message: MESSAGE$19
|
|
20439
20386
|
});
|
|
20440
20387
|
};
|
|
20441
20388
|
const noJsxElementType = defineRule({
|
|
@@ -20545,7 +20492,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20545
20492
|
let propertyName = null;
|
|
20546
20493
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20547
20494
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20548
|
-
if (propertyName && LAYOUT_PROPERTIES
|
|
20495
|
+
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20549
20496
|
node: property,
|
|
20550
20497
|
message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
|
|
20551
20498
|
});
|
|
@@ -20735,134 +20682,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20735
20682
|
} })
|
|
20736
20683
|
});
|
|
20737
20684
|
//#endregion
|
|
20738
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20739
|
-
const getStylePropertyNumberValue = (property) => {
|
|
20740
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
20741
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20742
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20743
|
-
return null;
|
|
20744
|
-
};
|
|
20745
|
-
//#endregion
|
|
20746
|
-
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20747
|
-
const linearizeChannel = (channel) => {
|
|
20748
|
-
const normalized = channel / 255;
|
|
20749
|
-
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20750
|
-
};
|
|
20751
|
-
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20752
|
-
const getWcagContrastRatio = (foreground, background) => {
|
|
20753
|
-
const foregroundLuminance = relativeLuminance(foreground);
|
|
20754
|
-
const backgroundLuminance = relativeLuminance(background);
|
|
20755
|
-
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20756
|
-
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20757
|
-
return (lighter + .05) / (darker + .05);
|
|
20758
|
-
};
|
|
20759
|
-
//#endregion
|
|
20760
|
-
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20761
|
-
const UNRESOLVABLE = new Set([
|
|
20762
|
-
"transparent",
|
|
20763
|
-
"currentcolor",
|
|
20764
|
-
"inherit",
|
|
20765
|
-
"initial",
|
|
20766
|
-
"unset",
|
|
20767
|
-
"revert",
|
|
20768
|
-
"none"
|
|
20769
|
-
]);
|
|
20770
|
-
const resolveOpaqueColor = (raw) => {
|
|
20771
|
-
const value = raw.trim().toLowerCase();
|
|
20772
|
-
if (UNRESOLVABLE.has(value)) return null;
|
|
20773
|
-
if (value === "white") return {
|
|
20774
|
-
red: 255,
|
|
20775
|
-
green: 255,
|
|
20776
|
-
blue: 255
|
|
20777
|
-
};
|
|
20778
|
-
if (value === "black") return {
|
|
20779
|
-
red: 0,
|
|
20780
|
-
green: 0,
|
|
20781
|
-
blue: 0
|
|
20782
|
-
};
|
|
20783
|
-
if (value.startsWith("var(")) return null;
|
|
20784
|
-
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20785
|
-
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20786
|
-
if (value.startsWith("rgb")) {
|
|
20787
|
-
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20788
|
-
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20789
|
-
}
|
|
20790
|
-
return parseColorToRgb(value);
|
|
20791
|
-
};
|
|
20792
|
-
const toPx = (property) => {
|
|
20793
|
-
const numberValue = getStylePropertyNumberValue(property);
|
|
20794
|
-
if (numberValue !== null) return numberValue;
|
|
20795
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20796
|
-
if (stringValue === null) return null;
|
|
20797
|
-
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20798
|
-
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20799
|
-
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20800
|
-
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20801
|
-
return null;
|
|
20802
|
-
};
|
|
20803
|
-
const isBoldWeight = (property) => {
|
|
20804
|
-
const numberValue = getStylePropertyNumberValue(property);
|
|
20805
|
-
if (numberValue !== null) return numberValue >= 700;
|
|
20806
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20807
|
-
if (stringValue === null) return false;
|
|
20808
|
-
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20809
|
-
const numericWeight = Number(stringValue);
|
|
20810
|
-
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20811
|
-
};
|
|
20812
|
-
const noLowContrastInlineStyle = defineRule({
|
|
20813
|
-
id: "no-low-contrast-inline-style",
|
|
20814
|
-
title: "Low-contrast text in inline style",
|
|
20815
|
-
tags: ["test-noise"],
|
|
20816
|
-
severity: "warn",
|
|
20817
|
-
category: "Accessibility",
|
|
20818
|
-
recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
|
|
20819
|
-
create: (context) => ({ JSXAttribute(node) {
|
|
20820
|
-
const expression = getInlineStyleExpression(node);
|
|
20821
|
-
if (!expression) return;
|
|
20822
|
-
const properties = expression.properties ?? [];
|
|
20823
|
-
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20824
|
-
let foreground = null;
|
|
20825
|
-
let backgroundColorRaw = null;
|
|
20826
|
-
let backgroundShorthandRaw = null;
|
|
20827
|
-
let backgroundIsUnknown = false;
|
|
20828
|
-
let fontSizePx = null;
|
|
20829
|
-
let isBold = false;
|
|
20830
|
-
for (const property of properties) {
|
|
20831
|
-
const key = getStylePropertyKey(property);
|
|
20832
|
-
if (!key) continue;
|
|
20833
|
-
if (key === "backgroundImage") {
|
|
20834
|
-
backgroundIsUnknown = true;
|
|
20835
|
-
continue;
|
|
20836
|
-
}
|
|
20837
|
-
if (key === "fontSize" && property.type === "Property") {
|
|
20838
|
-
fontSizePx = toPx(property);
|
|
20839
|
-
continue;
|
|
20840
|
-
}
|
|
20841
|
-
if (key === "fontWeight" && property.type === "Property") {
|
|
20842
|
-
isBold = isBoldWeight(property);
|
|
20843
|
-
continue;
|
|
20844
|
-
}
|
|
20845
|
-
const stringValue = getStylePropertyStringValue(property);
|
|
20846
|
-
if (key === "color") {
|
|
20847
|
-
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20848
|
-
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20849
|
-
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20850
|
-
else backgroundShorthandRaw = stringValue;
|
|
20851
|
-
}
|
|
20852
|
-
if (backgroundIsUnknown) return;
|
|
20853
|
-
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20854
|
-
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20855
|
-
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20856
|
-
if (!foreground || !background) return;
|
|
20857
|
-
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20858
|
-
const ratio = getWcagContrastRatio(foreground, background);
|
|
20859
|
-
if (ratio < threshold) context.report({
|
|
20860
|
-
node,
|
|
20861
|
-
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
20862
|
-
});
|
|
20863
|
-
} })
|
|
20864
|
-
});
|
|
20865
|
-
//#endregion
|
|
20866
20685
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20867
20686
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20868
20687
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -21018,7 +20837,7 @@ const noMoment = defineRule({
|
|
|
21018
20837
|
});
|
|
21019
20838
|
//#endregion
|
|
21020
20839
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
21021
|
-
const MESSAGE$
|
|
20840
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
21022
20841
|
const resolveSettings$16 = (settings) => {
|
|
21023
20842
|
const reactDoctor = settings?.["react-doctor"];
|
|
21024
20843
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21340,7 +21159,7 @@ const noMultiComp = defineRule({
|
|
|
21340
21159
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21341
21160
|
for (const component of flagged.slice(1)) context.report({
|
|
21342
21161
|
node: component.reportNode,
|
|
21343
|
-
message: MESSAGE$
|
|
21162
|
+
message: MESSAGE$18
|
|
21344
21163
|
});
|
|
21345
21164
|
} };
|
|
21346
21165
|
}
|
|
@@ -21508,7 +21327,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21508
21327
|
};
|
|
21509
21328
|
//#endregion
|
|
21510
21329
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21511
|
-
const MESSAGE$
|
|
21330
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21512
21331
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21513
21332
|
"copyWithin",
|
|
21514
21333
|
"fill",
|
|
@@ -21718,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21718
21537
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21719
21538
|
context.report({
|
|
21720
21539
|
node: options.crossFileConsumerCallSite,
|
|
21721
|
-
message: `${MESSAGE$
|
|
21540
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21722
21541
|
});
|
|
21723
21542
|
return;
|
|
21724
21543
|
}
|
|
@@ -21727,7 +21546,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21727
21546
|
reportedNodes.add(mutation.node);
|
|
21728
21547
|
context.report({
|
|
21729
21548
|
node: mutation.node,
|
|
21730
|
-
message: MESSAGE$
|
|
21549
|
+
message: MESSAGE$17
|
|
21731
21550
|
});
|
|
21732
21551
|
}
|
|
21733
21552
|
};
|
|
@@ -21999,7 +21818,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21999
21818
|
});
|
|
22000
21819
|
//#endregion
|
|
22001
21820
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
22002
|
-
const MESSAGE$
|
|
21821
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
22003
21822
|
const resolveSettings$14 = (settings) => {
|
|
22004
21823
|
const reactDoctor = settings?.["react-doctor"];
|
|
22005
21824
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -22027,7 +21846,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22027
21846
|
if (numeric === null) {
|
|
22028
21847
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
22029
21848
|
node: tabIndex,
|
|
22030
|
-
message: MESSAGE$
|
|
21849
|
+
message: MESSAGE$16
|
|
22031
21850
|
});
|
|
22032
21851
|
return;
|
|
22033
21852
|
}
|
|
@@ -22040,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22040
21859
|
if (!roleAttribute) {
|
|
22041
21860
|
context.report({
|
|
22042
21861
|
node: tabIndex,
|
|
22043
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
22044
21863
|
});
|
|
22045
21864
|
return;
|
|
22046
21865
|
}
|
|
@@ -22054,12 +21873,20 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
22054
21873
|
}
|
|
22055
21874
|
context.report({
|
|
22056
21875
|
node: tabIndex,
|
|
22057
|
-
message: MESSAGE$
|
|
21876
|
+
message: MESSAGE$16
|
|
22058
21877
|
});
|
|
22059
21878
|
} };
|
|
22060
21879
|
}
|
|
22061
21880
|
});
|
|
22062
21881
|
//#endregion
|
|
21882
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21883
|
+
const getStylePropertyNumberValue = (property) => {
|
|
21884
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
21885
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21886
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21887
|
+
return null;
|
|
21888
|
+
};
|
|
21889
|
+
//#endregion
|
|
22063
21890
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
22064
21891
|
const noOutlineNone = defineRule({
|
|
22065
21892
|
id: "no-outline-none",
|
|
@@ -22737,7 +22564,7 @@ const noRandomKey = defineRule({
|
|
|
22737
22564
|
});
|
|
22738
22565
|
//#endregion
|
|
22739
22566
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22740
|
-
const MESSAGE$
|
|
22567
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22741
22568
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22742
22569
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22743
22570
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22763,13 +22590,13 @@ const noReactChildren = defineRule({
|
|
|
22763
22590
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22764
22591
|
context.report({
|
|
22765
22592
|
node: calleeOuter,
|
|
22766
|
-
message: MESSAGE$
|
|
22593
|
+
message: MESSAGE$15
|
|
22767
22594
|
});
|
|
22768
22595
|
return;
|
|
22769
22596
|
}
|
|
22770
22597
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22771
22598
|
node: calleeOuter,
|
|
22772
|
-
message: MESSAGE$
|
|
22599
|
+
message: MESSAGE$15
|
|
22773
22600
|
});
|
|
22774
22601
|
} })
|
|
22775
22602
|
});
|
|
@@ -22880,86 +22707,6 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22880
22707
|
})
|
|
22881
22708
|
});
|
|
22882
22709
|
//#endregion
|
|
22883
|
-
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22884
|
-
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22885
|
-
"div",
|
|
22886
|
-
"p",
|
|
22887
|
-
"section",
|
|
22888
|
-
"article",
|
|
22889
|
-
"main",
|
|
22890
|
-
"header",
|
|
22891
|
-
"footer",
|
|
22892
|
-
"nav",
|
|
22893
|
-
"aside",
|
|
22894
|
-
"figure",
|
|
22895
|
-
"figcaption",
|
|
22896
|
-
"blockquote",
|
|
22897
|
-
"form",
|
|
22898
|
-
"fieldset",
|
|
22899
|
-
"address",
|
|
22900
|
-
"pre",
|
|
22901
|
-
"ul",
|
|
22902
|
-
"ol",
|
|
22903
|
-
"dl",
|
|
22904
|
-
"dt",
|
|
22905
|
-
"dd",
|
|
22906
|
-
"h1",
|
|
22907
|
-
"h2",
|
|
22908
|
-
"h3",
|
|
22909
|
-
"h4",
|
|
22910
|
-
"h5",
|
|
22911
|
-
"h6"
|
|
22912
|
-
]);
|
|
22913
|
-
const INLINE_DEFAULT_TAGS = new Set([
|
|
22914
|
-
"span",
|
|
22915
|
-
"a",
|
|
22916
|
-
"b",
|
|
22917
|
-
"i",
|
|
22918
|
-
"em",
|
|
22919
|
-
"strong",
|
|
22920
|
-
"small",
|
|
22921
|
-
"code",
|
|
22922
|
-
"abbr",
|
|
22923
|
-
"cite",
|
|
22924
|
-
"label",
|
|
22925
|
-
"mark",
|
|
22926
|
-
"q",
|
|
22927
|
-
"s",
|
|
22928
|
-
"u",
|
|
22929
|
-
"sub",
|
|
22930
|
-
"sup",
|
|
22931
|
-
"kbd",
|
|
22932
|
-
"samp",
|
|
22933
|
-
"var",
|
|
22934
|
-
"time"
|
|
22935
|
-
]);
|
|
22936
|
-
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22937
|
-
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22938
|
-
const noRedundantDisplayClass = defineRule({
|
|
22939
|
-
id: "no-redundant-display-class",
|
|
22940
|
-
title: "Redundant display utility",
|
|
22941
|
-
tags: ["design", "test-noise"],
|
|
22942
|
-
severity: "warn",
|
|
22943
|
-
recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
|
|
22944
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
22945
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
22946
|
-
const tagName = node.name.name;
|
|
22947
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
22948
|
-
if (!classNameValue) return;
|
|
22949
|
-
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
22950
|
-
context.report({
|
|
22951
|
-
node,
|
|
22952
|
-
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22953
|
-
});
|
|
22954
|
-
return;
|
|
22955
|
-
}
|
|
22956
|
-
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
22957
|
-
node,
|
|
22958
|
-
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22959
|
-
});
|
|
22960
|
-
} })
|
|
22961
|
-
});
|
|
22962
|
-
//#endregion
|
|
22963
22710
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22964
22711
|
const ELEMENT_ROLE_PAIRS = [
|
|
22965
22712
|
["a", "link"],
|
|
@@ -23172,7 +22919,7 @@ const noRenderPropChildren = defineRule({
|
|
|
23172
22919
|
});
|
|
23173
22920
|
//#endregion
|
|
23174
22921
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
23175
|
-
const MESSAGE$
|
|
22922
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
23176
22923
|
const isReactDomRenderCall = (node) => {
|
|
23177
22924
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
23178
22925
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -23196,7 +22943,7 @@ const noRenderReturnValue = defineRule({
|
|
|
23196
22943
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
23197
22944
|
context.report({
|
|
23198
22945
|
node: node.callee,
|
|
23199
|
-
message: MESSAGE$
|
|
22946
|
+
message: MESSAGE$14
|
|
23200
22947
|
});
|
|
23201
22948
|
} })
|
|
23202
22949
|
});
|
|
@@ -23894,7 +23641,7 @@ const getParentComponent = (node) => {
|
|
|
23894
23641
|
};
|
|
23895
23642
|
//#endregion
|
|
23896
23643
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23897
|
-
const MESSAGE$
|
|
23644
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23898
23645
|
const noSetState = defineRule({
|
|
23899
23646
|
id: "no-set-state",
|
|
23900
23647
|
title: "Local class state forbidden",
|
|
@@ -23909,7 +23656,7 @@ const noSetState = defineRule({
|
|
|
23909
23656
|
if (!getParentComponent(node)) return;
|
|
23910
23657
|
context.report({
|
|
23911
23658
|
node: node.callee,
|
|
23912
|
-
message: MESSAGE$
|
|
23659
|
+
message: MESSAGE$13
|
|
23913
23660
|
});
|
|
23914
23661
|
} })
|
|
23915
23662
|
});
|
|
@@ -24071,7 +23818,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
24071
23818
|
};
|
|
24072
23819
|
//#endregion
|
|
24073
23820
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
24074
|
-
const MESSAGE$
|
|
23821
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
24075
23822
|
const DEFAULT_HANDLERS = [
|
|
24076
23823
|
"onClick",
|
|
24077
23824
|
"onMouseDown",
|
|
@@ -24131,7 +23878,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
24131
23878
|
if (!roleAttribute || !roleAttribute.value) {
|
|
24132
23879
|
context.report({
|
|
24133
23880
|
node: node.name,
|
|
24134
|
-
message: MESSAGE$
|
|
23881
|
+
message: MESSAGE$12
|
|
24135
23882
|
});
|
|
24136
23883
|
return;
|
|
24137
23884
|
}
|
|
@@ -24141,14 +23888,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
24141
23888
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
24142
23889
|
context.report({
|
|
24143
23890
|
node: node.name,
|
|
24144
|
-
message: MESSAGE$
|
|
23891
|
+
message: MESSAGE$12
|
|
24145
23892
|
});
|
|
24146
23893
|
return;
|
|
24147
23894
|
}
|
|
24148
23895
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
24149
23896
|
context.report({
|
|
24150
23897
|
node: node.name,
|
|
24151
|
-
message: MESSAGE$
|
|
23898
|
+
message: MESSAGE$12
|
|
24152
23899
|
});
|
|
24153
23900
|
} };
|
|
24154
23901
|
}
|
|
@@ -24251,43 +23998,8 @@ const noStringRefs = defineRule({
|
|
|
24251
23998
|
}
|
|
24252
23999
|
});
|
|
24253
24000
|
//#endregion
|
|
24254
|
-
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24255
|
-
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24256
|
-
if (token.includes(":")) return false;
|
|
24257
|
-
if (!token.startsWith(prefix)) return false;
|
|
24258
|
-
const value = token.slice(prefix.length);
|
|
24259
|
-
if (value === "" || value === "current") return false;
|
|
24260
|
-
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24261
|
-
return true;
|
|
24262
|
-
});
|
|
24263
|
-
const isCurrentColor = (attribute) => {
|
|
24264
|
-
const value = getJsxPropStringValue(attribute);
|
|
24265
|
-
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24266
|
-
};
|
|
24267
|
-
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24268
|
-
id: "no-svg-currentcolor-with-fill-class",
|
|
24269
|
-
title: "currentColor fights a fill/stroke class",
|
|
24270
|
-
tags: ["design", "test-noise"],
|
|
24271
|
-
severity: "warn",
|
|
24272
|
-
recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
|
|
24273
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24274
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24275
|
-
if (!classNameValue) return;
|
|
24276
|
-
for (const paint of ["fill", "stroke"]) {
|
|
24277
|
-
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24278
|
-
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24279
|
-
context.report({
|
|
24280
|
-
node: attribute,
|
|
24281
|
-
message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
|
|
24282
|
-
});
|
|
24283
|
-
return;
|
|
24284
|
-
}
|
|
24285
|
-
}
|
|
24286
|
-
} })
|
|
24287
|
-
});
|
|
24288
|
-
//#endregion
|
|
24289
24001
|
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24290
|
-
const MESSAGE$
|
|
24002
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
24291
24003
|
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24292
24004
|
const noSyncXhr = defineRule({
|
|
24293
24005
|
id: "no-sync-xhr",
|
|
@@ -24302,103 +24014,13 @@ const noSyncXhr = defineRule({
|
|
|
24302
24014
|
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24303
24015
|
context.report({
|
|
24304
24016
|
node,
|
|
24305
|
-
message: MESSAGE$
|
|
24306
|
-
});
|
|
24307
|
-
} })
|
|
24308
|
-
});
|
|
24309
|
-
//#endregion
|
|
24310
|
-
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24311
|
-
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24312
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
24313
|
-
"width",
|
|
24314
|
-
"height",
|
|
24315
|
-
"min-width",
|
|
24316
|
-
"max-width",
|
|
24317
|
-
"min-height",
|
|
24318
|
-
"max-height",
|
|
24319
|
-
"top",
|
|
24320
|
-
"left",
|
|
24321
|
-
"right",
|
|
24322
|
-
"bottom",
|
|
24323
|
-
"inset",
|
|
24324
|
-
"inset-block",
|
|
24325
|
-
"inset-inline",
|
|
24326
|
-
"margin",
|
|
24327
|
-
"margin-top",
|
|
24328
|
-
"margin-right",
|
|
24329
|
-
"margin-bottom",
|
|
24330
|
-
"margin-left",
|
|
24331
|
-
"margin-block",
|
|
24332
|
-
"margin-inline",
|
|
24333
|
-
"padding",
|
|
24334
|
-
"padding-top",
|
|
24335
|
-
"padding-right",
|
|
24336
|
-
"padding-bottom",
|
|
24337
|
-
"padding-left",
|
|
24338
|
-
"padding-block",
|
|
24339
|
-
"padding-inline"
|
|
24340
|
-
]);
|
|
24341
|
-
const noTailwindLayoutTransition = defineRule({
|
|
24342
|
-
id: "no-tailwind-layout-transition",
|
|
24343
|
-
title: "Animating a layout property",
|
|
24344
|
-
tags: ["design", "test-noise"],
|
|
24345
|
-
severity: "warn",
|
|
24346
|
-
category: "Performance",
|
|
24347
|
-
recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
|
|
24348
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24349
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24350
|
-
if (!classNameValue) return;
|
|
24351
|
-
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24352
|
-
const animatedProperties = transitionMatch[1];
|
|
24353
|
-
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24354
|
-
if (layoutProperty) context.report({
|
|
24355
|
-
node,
|
|
24356
|
-
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24357
|
-
});
|
|
24358
|
-
}
|
|
24359
|
-
} })
|
|
24360
|
-
});
|
|
24361
|
-
//#endregion
|
|
24362
|
-
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24363
|
-
const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
|
|
24364
|
-
const targetIsBlank = (attribute) => {
|
|
24365
|
-
const stringValue = getJsxPropStringValue(attribute);
|
|
24366
|
-
if (stringValue !== null) return stringValue === "_blank";
|
|
24367
|
-
const value = attribute.value;
|
|
24368
|
-
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24369
|
-
const expression = value.expression;
|
|
24370
|
-
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24371
|
-
}
|
|
24372
|
-
return false;
|
|
24373
|
-
};
|
|
24374
|
-
const noTargetBlankWithoutRel = defineRule({
|
|
24375
|
-
id: "no-target-blank-without-rel",
|
|
24376
|
-
title: "target=_blank without rel=noopener",
|
|
24377
|
-
severity: "warn",
|
|
24378
|
-
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24379
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24380
|
-
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24381
|
-
const tagName = node.name.name;
|
|
24382
|
-
if (tagName !== "a" && tagName !== "area") return;
|
|
24383
|
-
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24384
|
-
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24385
|
-
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24386
|
-
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24387
|
-
if (relAttribute) {
|
|
24388
|
-
const relValue = getJsxPropStringValue(relAttribute);
|
|
24389
|
-
if (relValue === null) return;
|
|
24390
|
-
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24391
|
-
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24392
|
-
}
|
|
24393
|
-
context.report({
|
|
24394
|
-
node: node.name,
|
|
24395
|
-
message: MESSAGE$13
|
|
24017
|
+
message: MESSAGE$11
|
|
24396
24018
|
});
|
|
24397
24019
|
} })
|
|
24398
24020
|
});
|
|
24399
24021
|
//#endregion
|
|
24400
24022
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
24401
|
-
const MESSAGE$
|
|
24023
|
+
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
24402
24024
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
24403
24025
|
let ancestor = node.parent;
|
|
24404
24026
|
while (ancestor) {
|
|
@@ -24467,7 +24089,7 @@ const noThisInSfc = defineRule({
|
|
|
24467
24089
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
24468
24090
|
context.report({
|
|
24469
24091
|
node,
|
|
24470
|
-
message: MESSAGE$
|
|
24092
|
+
message: MESSAGE$10
|
|
24471
24093
|
});
|
|
24472
24094
|
} };
|
|
24473
24095
|
}
|
|
@@ -24505,39 +24127,26 @@ const noTinyText = defineRule({
|
|
|
24505
24127
|
});
|
|
24506
24128
|
//#endregion
|
|
24507
24129
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24508
|
-
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24509
|
-
const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
|
|
24510
24130
|
const noTransitionAll = defineRule({
|
|
24511
24131
|
id: "no-transition-all",
|
|
24512
24132
|
title: "transition: all animates everything",
|
|
24513
24133
|
tags: ["test-noise"],
|
|
24514
24134
|
severity: "warn",
|
|
24515
24135
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
24516
|
-
create: (context) => ({
|
|
24517
|
-
|
|
24518
|
-
|
|
24519
|
-
|
|
24520
|
-
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
node: property,
|
|
24528
|
-
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24529
|
-
});
|
|
24530
|
-
}
|
|
24531
|
-
},
|
|
24532
|
-
JSXOpeningElement(node) {
|
|
24533
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
24534
|
-
if (!classNameValue) return;
|
|
24535
|
-
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24536
|
-
node,
|
|
24537
|
-
message: TAILWIND_MESSAGE
|
|
24136
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
24137
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24138
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24139
|
+
const expression = node.value.expression;
|
|
24140
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24141
|
+
for (const property of expression.properties ?? []) {
|
|
24142
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24143
|
+
if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
|
|
24144
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
|
|
24145
|
+
node: property,
|
|
24146
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24538
24147
|
});
|
|
24539
24148
|
}
|
|
24540
|
-
})
|
|
24149
|
+
} })
|
|
24541
24150
|
});
|
|
24542
24151
|
//#endregion
|
|
24543
24152
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -24581,6 +24190,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
24581
24190
|
}
|
|
24582
24191
|
return stateNames;
|
|
24583
24192
|
};
|
|
24193
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
24584
24194
|
const noUncontrolledInput = defineRule({
|
|
24585
24195
|
id: "no-uncontrolled-input",
|
|
24586
24196
|
title: "Uncontrolled input value",
|
|
@@ -24684,38 +24294,6 @@ const noUnescapedEntities = defineRule({
|
|
|
24684
24294
|
} })
|
|
24685
24295
|
});
|
|
24686
24296
|
//#endregion
|
|
24687
|
-
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24688
|
-
const UNINFORMATIVE_LABELS = new Set([
|
|
24689
|
-
"icon",
|
|
24690
|
-
"button",
|
|
24691
|
-
"image",
|
|
24692
|
-
"img",
|
|
24693
|
-
"link",
|
|
24694
|
-
"graphic",
|
|
24695
|
-
"svg",
|
|
24696
|
-
"picture",
|
|
24697
|
-
"element",
|
|
24698
|
-
"field",
|
|
24699
|
-
"input"
|
|
24700
|
-
]);
|
|
24701
|
-
const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
|
|
24702
|
-
const noUninformativeAriaLabel = defineRule({
|
|
24703
|
-
id: "no-uninformative-aria-label",
|
|
24704
|
-
title: "Uninformative aria-label",
|
|
24705
|
-
severity: "warn",
|
|
24706
|
-
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24707
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
24708
|
-
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24709
|
-
if (!ariaLabel) return;
|
|
24710
|
-
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24711
|
-
if (labelValue === null) return;
|
|
24712
|
-
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24713
|
-
node: ariaLabel,
|
|
24714
|
-
message: MESSAGE$11
|
|
24715
|
-
});
|
|
24716
|
-
} })
|
|
24717
|
-
});
|
|
24718
|
-
//#endregion
|
|
24719
24297
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24720
24298
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24721
24299
|
"activedescendant",
|
|
@@ -25850,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
25850
25428
|
visit(root);
|
|
25851
25429
|
return found;
|
|
25852
25430
|
};
|
|
25853
|
-
const classExtendsReactComponent$1 = (classNode) => {
|
|
25854
|
-
const superClass = classNode.superClass;
|
|
25855
|
-
if (!superClass) return false;
|
|
25856
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
25857
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
25858
|
-
return false;
|
|
25859
|
-
};
|
|
25860
25431
|
const isReactClassComponent = (classNode) => {
|
|
25861
|
-
if (
|
|
25432
|
+
if (isEs6Component(classNode)) return true;
|
|
25862
25433
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
25863
25434
|
};
|
|
25864
25435
|
const findEnclosingComponent = (node) => {
|
|
@@ -26018,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26018
25589
|
create: (context) => {
|
|
26019
25590
|
const settings = resolveSettings$8(context.settings);
|
|
26020
25591
|
const renderPropRegex = compileGlob(settings.propNamePattern);
|
|
26021
|
-
const reportCandidate = (candidateNode, reportNode
|
|
25592
|
+
const reportCandidate = (candidateNode, reportNode) => {
|
|
26022
25593
|
if (isFirstArgumentOfHocCall(candidateNode)) return;
|
|
26023
25594
|
if (isReturnOfMapCallback(candidateNode)) return;
|
|
26024
25595
|
const propInfo = isComponentDeclaredInProp(candidateNode);
|
|
@@ -26039,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26039
25610
|
const inferredName = inferFunctionLikeName(node);
|
|
26040
25611
|
const propInfo = isComponentDeclaredInProp(node);
|
|
26041
25612
|
if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
|
|
26042
|
-
reportCandidate(node, node
|
|
25613
|
+
reportCandidate(node, node);
|
|
26043
25614
|
};
|
|
26044
25615
|
return {
|
|
26045
25616
|
FunctionDeclaration: checkFunctionLike,
|
|
@@ -26049,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
|
|
|
26049
25620
|
if (!node.id) return;
|
|
26050
25621
|
if (!isReactComponentName(node.id.name)) return;
|
|
26051
25622
|
if (!isReactClassComponent(node)) return;
|
|
26052
|
-
reportCandidate(node, node
|
|
25623
|
+
reportCandidate(node, node);
|
|
26053
25624
|
},
|
|
26054
25625
|
ClassExpression(node) {
|
|
26055
25626
|
const inferredName = node.id?.name ?? inferFunctionLikeName(node);
|
|
26056
25627
|
if (!inferredName || !isReactComponentName(inferredName)) return;
|
|
26057
25628
|
if (!isReactClassComponent(node)) return;
|
|
26058
|
-
reportCandidate(node, node
|
|
25629
|
+
reportCandidate(node, node);
|
|
26059
25630
|
},
|
|
26060
25631
|
CallExpression(node) {
|
|
26061
25632
|
if (!isHocCallee$1(node)) return;
|
|
26062
25633
|
if (!hocCallContainsComponent(node)) return;
|
|
26063
|
-
reportCandidate(node, node
|
|
25634
|
+
reportCandidate(node, node);
|
|
26064
25635
|
}
|
|
26065
25636
|
};
|
|
26066
25637
|
}
|
|
@@ -26187,7 +25758,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
26187
25758
|
//#endregion
|
|
26188
25759
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
26189
25760
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
26190
|
-
const MESSAGE$
|
|
25761
|
+
const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
26191
25762
|
const resolveSettings$7 = (settings) => {
|
|
26192
25763
|
const reactDoctor = settings?.["react-doctor"];
|
|
26193
25764
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -26221,7 +25792,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
26221
25792
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
26222
25793
|
context.report({
|
|
26223
25794
|
node: node.callee,
|
|
26224
|
-
message: MESSAGE$
|
|
25795
|
+
message: MESSAGE$9
|
|
26225
25796
|
});
|
|
26226
25797
|
} };
|
|
26227
25798
|
}
|
|
@@ -26500,13 +26071,6 @@ const skipTsExpression = (expression) => {
|
|
|
26500
26071
|
if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
|
|
26501
26072
|
return expression;
|
|
26502
26073
|
};
|
|
26503
|
-
const classExtendsReactComponent = (classNode) => {
|
|
26504
|
-
const superClass = classNode.superClass;
|
|
26505
|
-
if (!superClass) return false;
|
|
26506
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
26507
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
26508
|
-
return false;
|
|
26509
|
-
};
|
|
26510
26074
|
const isReactCreateContext = (initializer) => {
|
|
26511
26075
|
if (!initializer) return false;
|
|
26512
26076
|
const expression = skipTsExpression(initializer);
|
|
@@ -26697,7 +26261,7 @@ const onlyExportComponents = defineRule({
|
|
|
26697
26261
|
if (stripped.id) {
|
|
26698
26262
|
const idNode = stripped.id;
|
|
26699
26263
|
isExportedNodeIds.add(stripped);
|
|
26700
|
-
if (isReactComponentName(idNode.name) &&
|
|
26264
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
26701
26265
|
else exports.push({
|
|
26702
26266
|
kind: "non-component",
|
|
26703
26267
|
reportNode: idNode
|
|
@@ -26757,7 +26321,7 @@ const onlyExportComponents = defineRule({
|
|
|
26757
26321
|
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
26758
26322
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
26759
26323
|
isExportedNodeIds.add(declaration);
|
|
26760
|
-
if (isReactComponentName(declaration.id.name) &&
|
|
26324
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
26761
26325
|
else exports.push({
|
|
26762
26326
|
kind: "non-component",
|
|
26763
26327
|
reportNode: declaration.id
|
|
@@ -27099,7 +26663,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
27099
26663
|
});
|
|
27100
26664
|
//#endregion
|
|
27101
26665
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
27102
|
-
const MESSAGE$
|
|
26666
|
+
const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
27103
26667
|
const preactPreferOndblclick = defineRule({
|
|
27104
26668
|
id: "preact-prefer-ondblclick",
|
|
27105
26669
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -27114,7 +26678,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
27114
26678
|
if (!onDoubleClickAttribute) return;
|
|
27115
26679
|
context.report({
|
|
27116
26680
|
node: onDoubleClickAttribute,
|
|
27117
|
-
message: MESSAGE$
|
|
26681
|
+
message: MESSAGE$8
|
|
27118
26682
|
});
|
|
27119
26683
|
} })
|
|
27120
26684
|
});
|
|
@@ -27154,42 +26718,6 @@ const preactPreferOninput = defineRule({
|
|
|
27154
26718
|
} })
|
|
27155
26719
|
});
|
|
27156
26720
|
//#endregion
|
|
27157
|
-
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27158
|
-
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27159
|
-
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27160
|
-
const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
|
|
27161
|
-
const preferDvhOverVh = defineRule({
|
|
27162
|
-
id: "prefer-dvh-over-vh",
|
|
27163
|
-
title: "Use dvh instead of vh for full height",
|
|
27164
|
-
tags: ["design", "test-noise"],
|
|
27165
|
-
severity: "warn",
|
|
27166
|
-
requires: ["tailwind:3.4"],
|
|
27167
|
-
recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
|
|
27168
|
-
create: (context) => ({
|
|
27169
|
-
JSXAttribute(node) {
|
|
27170
|
-
const expression = getInlineStyleExpression(node);
|
|
27171
|
-
if (!expression) return;
|
|
27172
|
-
for (const property of expression.properties ?? []) {
|
|
27173
|
-
const key = getStylePropertyKey(property);
|
|
27174
|
-
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27175
|
-
const value = getStylePropertyStringValue(property);
|
|
27176
|
-
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27177
|
-
node: property,
|
|
27178
|
-
message: MESSAGE$8
|
|
27179
|
-
});
|
|
27180
|
-
}
|
|
27181
|
-
},
|
|
27182
|
-
JSXOpeningElement(node) {
|
|
27183
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
27184
|
-
if (!classNameValue) return;
|
|
27185
|
-
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27186
|
-
node,
|
|
27187
|
-
message: MESSAGE$8
|
|
27188
|
-
});
|
|
27189
|
-
}
|
|
27190
|
-
})
|
|
27191
|
-
});
|
|
27192
|
-
//#endregion
|
|
27193
26721
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
27194
26722
|
const preferDynamicImport = defineRule({
|
|
27195
26723
|
id: "prefer-dynamic-import",
|
|
@@ -27781,26 +27309,6 @@ const preferTagOverRole = defineRule({
|
|
|
27781
27309
|
} })
|
|
27782
27310
|
});
|
|
27783
27311
|
//#endregion
|
|
27784
|
-
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27785
|
-
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27786
|
-
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27787
|
-
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27788
|
-
const preferTruncateShorthand = defineRule({
|
|
27789
|
-
id: "prefer-truncate-shorthand",
|
|
27790
|
-
title: "Use truncate shorthand",
|
|
27791
|
-
tags: ["design", "test-noise"],
|
|
27792
|
-
severity: "warn",
|
|
27793
|
-
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27794
|
-
create: (context) => ({ JSXOpeningElement(node) {
|
|
27795
|
-
const classNameValue = getStringFromClassNameAttr(node);
|
|
27796
|
-
if (!classNameValue) return;
|
|
27797
|
-
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27798
|
-
node,
|
|
27799
|
-
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27800
|
-
});
|
|
27801
|
-
} })
|
|
27802
|
-
});
|
|
27803
|
-
//#endregion
|
|
27804
27312
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27805
27313
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27806
27314
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -35917,6 +35425,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
35917
35425
|
CallExpression(node) {
|
|
35918
35426
|
if (!isServerSideFile) return;
|
|
35919
35427
|
if (!isFetchCall(node)) return;
|
|
35428
|
+
if (isMutatingFetchCall(node)) return;
|
|
35920
35429
|
const optionsArg = node.arguments?.[1];
|
|
35921
35430
|
if (optionsArg && objectExpressionHasNextRevalidate(optionsArg)) return;
|
|
35922
35431
|
const urlArg = node.arguments?.[0];
|
|
@@ -36072,13 +35581,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
36072
35581
|
const collectDeclaredNames = (declaration) => {
|
|
36073
35582
|
const names = /* @__PURE__ */ new Set();
|
|
36074
35583
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
36075
|
-
for (const declarator of declaration.declarations ?? [])
|
|
36076
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
36077
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
36078
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
36079
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
36080
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
36081
|
-
}
|
|
35584
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
36082
35585
|
return names;
|
|
36083
35586
|
};
|
|
36084
35587
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -36088,11 +35591,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
36088
35591
|
};
|
|
36089
35592
|
const declarationReadsAnyName = (declaration, names) => {
|
|
36090
35593
|
if (names.size === 0) return false;
|
|
35594
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
36091
35595
|
let didRead = false;
|
|
36092
|
-
|
|
36093
|
-
if (
|
|
36094
|
-
|
|
36095
|
-
|
|
35596
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35597
|
+
if (!declarator.init) continue;
|
|
35598
|
+
walkAst(declarator.init, (child) => {
|
|
35599
|
+
if (didRead) return;
|
|
35600
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35601
|
+
});
|
|
35602
|
+
}
|
|
36096
35603
|
return didRead;
|
|
36097
35604
|
};
|
|
36098
35605
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -39477,17 +38984,6 @@ const reactDoctorRules = [
|
|
|
39477
38984
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
39478
38985
|
}
|
|
39479
38986
|
},
|
|
39480
|
-
{
|
|
39481
|
-
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39482
|
-
id: "no-arbitrary-px-font-size",
|
|
39483
|
-
source: "react-doctor",
|
|
39484
|
-
originallyExternal: false,
|
|
39485
|
-
rule: {
|
|
39486
|
-
...noArbitraryPxFontSize,
|
|
39487
|
-
framework: "global",
|
|
39488
|
-
category: "Accessibility"
|
|
39489
|
-
}
|
|
39490
|
-
},
|
|
39491
38987
|
{
|
|
39492
38988
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
39493
38989
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -39547,18 +39043,6 @@ const reactDoctorRules = [
|
|
|
39547
39043
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
39548
39044
|
}
|
|
39549
39045
|
},
|
|
39550
|
-
{
|
|
39551
|
-
key: "react-doctor/no-autoplay-without-muted",
|
|
39552
|
-
id: "no-autoplay-without-muted",
|
|
39553
|
-
source: "react-doctor",
|
|
39554
|
-
originallyExternal: false,
|
|
39555
|
-
rule: {
|
|
39556
|
-
...noAutoplayWithoutMuted,
|
|
39557
|
-
framework: "global",
|
|
39558
|
-
category: "Accessibility",
|
|
39559
|
-
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39560
|
-
}
|
|
39561
|
-
},
|
|
39562
39046
|
{
|
|
39563
39047
|
key: "react-doctor/no-barrel-import",
|
|
39564
39048
|
id: "no-barrel-import",
|
|
@@ -39712,17 +39196,6 @@ const reactDoctorRules = [
|
|
|
39712
39196
|
category: "Maintainability"
|
|
39713
39197
|
}
|
|
39714
39198
|
},
|
|
39715
|
-
{
|
|
39716
|
-
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39717
|
-
id: "no-deprecated-tailwind-class",
|
|
39718
|
-
source: "react-doctor",
|
|
39719
|
-
originallyExternal: false,
|
|
39720
|
-
rule: {
|
|
39721
|
-
...noDeprecatedTailwindClass,
|
|
39722
|
-
framework: "global",
|
|
39723
|
-
category: "Maintainability"
|
|
39724
|
-
}
|
|
39725
|
-
},
|
|
39726
39199
|
{
|
|
39727
39200
|
key: "react-doctor/no-derived-state",
|
|
39728
39201
|
id: "no-derived-state",
|
|
@@ -39994,17 +39467,6 @@ const reactDoctorRules = [
|
|
|
39994
39467
|
category: "Performance"
|
|
39995
39468
|
}
|
|
39996
39469
|
},
|
|
39997
|
-
{
|
|
39998
|
-
key: "react-doctor/no-full-viewport-width",
|
|
39999
|
-
id: "no-full-viewport-width",
|
|
40000
|
-
source: "react-doctor",
|
|
40001
|
-
originallyExternal: false,
|
|
40002
|
-
rule: {
|
|
40003
|
-
...noFullViewportWidth,
|
|
40004
|
-
framework: "global",
|
|
40005
|
-
category: "Maintainability"
|
|
40006
|
-
}
|
|
40007
|
-
},
|
|
40008
39470
|
{
|
|
40009
39471
|
key: "react-doctor/no-generic-handler-names",
|
|
40010
39472
|
id: "no-generic-handler-names",
|
|
@@ -40244,17 +39706,6 @@ const reactDoctorRules = [
|
|
|
40244
39706
|
category: "Performance"
|
|
40245
39707
|
}
|
|
40246
39708
|
},
|
|
40247
|
-
{
|
|
40248
|
-
key: "react-doctor/no-low-contrast-inline-style",
|
|
40249
|
-
id: "no-low-contrast-inline-style",
|
|
40250
|
-
source: "react-doctor",
|
|
40251
|
-
originallyExternal: false,
|
|
40252
|
-
rule: {
|
|
40253
|
-
...noLowContrastInlineStyle,
|
|
40254
|
-
framework: "global",
|
|
40255
|
-
category: "Accessibility"
|
|
40256
|
-
}
|
|
40257
|
-
},
|
|
40258
39709
|
{
|
|
40259
39710
|
key: "react-doctor/no-many-boolean-props",
|
|
40260
39711
|
id: "no-many-boolean-props",
|
|
@@ -40532,17 +39983,6 @@ const reactDoctorRules = [
|
|
|
40532
39983
|
category: "Maintainability"
|
|
40533
39984
|
}
|
|
40534
39985
|
},
|
|
40535
|
-
{
|
|
40536
|
-
key: "react-doctor/no-redundant-display-class",
|
|
40537
|
-
id: "no-redundant-display-class",
|
|
40538
|
-
source: "react-doctor",
|
|
40539
|
-
originallyExternal: false,
|
|
40540
|
-
rule: {
|
|
40541
|
-
...noRedundantDisplayClass,
|
|
40542
|
-
framework: "global",
|
|
40543
|
-
category: "Maintainability"
|
|
40544
|
-
}
|
|
40545
|
-
},
|
|
40546
39986
|
{
|
|
40547
39987
|
key: "react-doctor/no-redundant-roles",
|
|
40548
39988
|
id: "no-redundant-roles",
|
|
@@ -40719,17 +40159,6 @@ const reactDoctorRules = [
|
|
|
40719
40159
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
40720
40160
|
}
|
|
40721
40161
|
},
|
|
40722
|
-
{
|
|
40723
|
-
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40724
|
-
id: "no-svg-currentcolor-with-fill-class",
|
|
40725
|
-
source: "react-doctor",
|
|
40726
|
-
originallyExternal: false,
|
|
40727
|
-
rule: {
|
|
40728
|
-
...noSvgCurrentcolorWithFillClass,
|
|
40729
|
-
framework: "global",
|
|
40730
|
-
category: "Maintainability"
|
|
40731
|
-
}
|
|
40732
|
-
},
|
|
40733
40162
|
{
|
|
40734
40163
|
key: "react-doctor/no-sync-xhr",
|
|
40735
40164
|
id: "no-sync-xhr",
|
|
@@ -40741,29 +40170,6 @@ const reactDoctorRules = [
|
|
|
40741
40170
|
category: "Performance"
|
|
40742
40171
|
}
|
|
40743
40172
|
},
|
|
40744
|
-
{
|
|
40745
|
-
key: "react-doctor/no-tailwind-layout-transition",
|
|
40746
|
-
id: "no-tailwind-layout-transition",
|
|
40747
|
-
source: "react-doctor",
|
|
40748
|
-
originallyExternal: false,
|
|
40749
|
-
rule: {
|
|
40750
|
-
...noTailwindLayoutTransition,
|
|
40751
|
-
framework: "global",
|
|
40752
|
-
category: "Performance"
|
|
40753
|
-
}
|
|
40754
|
-
},
|
|
40755
|
-
{
|
|
40756
|
-
key: "react-doctor/no-target-blank-without-rel",
|
|
40757
|
-
id: "no-target-blank-without-rel",
|
|
40758
|
-
source: "react-doctor",
|
|
40759
|
-
originallyExternal: false,
|
|
40760
|
-
rule: {
|
|
40761
|
-
...noTargetBlankWithoutRel,
|
|
40762
|
-
framework: "global",
|
|
40763
|
-
category: "Accessibility",
|
|
40764
|
-
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40765
|
-
}
|
|
40766
|
-
},
|
|
40767
40173
|
{
|
|
40768
40174
|
key: "react-doctor/no-this-in-sfc",
|
|
40769
40175
|
id: "no-this-in-sfc",
|
|
@@ -40833,18 +40239,6 @@ const reactDoctorRules = [
|
|
|
40833
40239
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
40834
40240
|
}
|
|
40835
40241
|
},
|
|
40836
|
-
{
|
|
40837
|
-
key: "react-doctor/no-uninformative-aria-label",
|
|
40838
|
-
id: "no-uninformative-aria-label",
|
|
40839
|
-
source: "react-doctor",
|
|
40840
|
-
originallyExternal: false,
|
|
40841
|
-
rule: {
|
|
40842
|
-
...noUninformativeAriaLabel,
|
|
40843
|
-
framework: "global",
|
|
40844
|
-
category: "Accessibility",
|
|
40845
|
-
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40846
|
-
}
|
|
40847
|
-
},
|
|
40848
40242
|
{
|
|
40849
40243
|
key: "react-doctor/no-unknown-property",
|
|
40850
40244
|
id: "no-unknown-property",
|
|
@@ -41054,17 +40448,6 @@ const reactDoctorRules = [
|
|
|
41054
40448
|
category: "Bugs"
|
|
41055
40449
|
}
|
|
41056
40450
|
},
|
|
41057
|
-
{
|
|
41058
|
-
key: "react-doctor/prefer-dvh-over-vh",
|
|
41059
|
-
id: "prefer-dvh-over-vh",
|
|
41060
|
-
source: "react-doctor",
|
|
41061
|
-
originallyExternal: false,
|
|
41062
|
-
rule: {
|
|
41063
|
-
...preferDvhOverVh,
|
|
41064
|
-
framework: "global",
|
|
41065
|
-
category: "Maintainability"
|
|
41066
|
-
}
|
|
41067
|
-
},
|
|
41068
40451
|
{
|
|
41069
40452
|
key: "react-doctor/prefer-dynamic-import",
|
|
41070
40453
|
id: "prefer-dynamic-import",
|
|
@@ -41169,17 +40552,6 @@ const reactDoctorRules = [
|
|
|
41169
40552
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
41170
40553
|
}
|
|
41171
40554
|
},
|
|
41172
|
-
{
|
|
41173
|
-
key: "react-doctor/prefer-truncate-shorthand",
|
|
41174
|
-
id: "prefer-truncate-shorthand",
|
|
41175
|
-
source: "react-doctor",
|
|
41176
|
-
originallyExternal: false,
|
|
41177
|
-
rule: {
|
|
41178
|
-
...preferTruncateShorthand,
|
|
41179
|
-
framework: "global",
|
|
41180
|
-
category: "Maintainability"
|
|
41181
|
-
}
|
|
41182
|
-
},
|
|
41183
40555
|
{
|
|
41184
40556
|
key: "react-doctor/prefer-use-effect-event",
|
|
41185
40557
|
id: "prefer-use-effect-event",
|
|
@@ -42906,32 +42278,6 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
42906
42278
|
}
|
|
42907
42279
|
return unconditional;
|
|
42908
42280
|
};
|
|
42909
|
-
const computeDominatesExit = (cfg) => {
|
|
42910
|
-
const reachableToExit = /* @__PURE__ */ new Set();
|
|
42911
|
-
const queue = [cfg.exit];
|
|
42912
|
-
while (queue.length > 0) {
|
|
42913
|
-
const block = queue.shift();
|
|
42914
|
-
if (reachableToExit.has(block)) continue;
|
|
42915
|
-
reachableToExit.add(block);
|
|
42916
|
-
for (const edge of block.predecessors) queue.push(edge.from);
|
|
42917
|
-
}
|
|
42918
|
-
const dominatesExit = /* @__PURE__ */ new Set();
|
|
42919
|
-
const visit = (block) => {
|
|
42920
|
-
if (block === cfg.exit) return true;
|
|
42921
|
-
if (dominatesExit.has(block)) return true;
|
|
42922
|
-
if (block.successors.length === 0) return false;
|
|
42923
|
-
dominatesExit.add(block);
|
|
42924
|
-
let allReach = true;
|
|
42925
|
-
for (const edge of block.successors) if (!visit(edge.to)) {
|
|
42926
|
-
allReach = false;
|
|
42927
|
-
break;
|
|
42928
|
-
}
|
|
42929
|
-
if (!allReach) dominatesExit.delete(block);
|
|
42930
|
-
return allReach;
|
|
42931
|
-
};
|
|
42932
|
-
for (const block of cfg.blocks) visit(block);
|
|
42933
|
-
return dominatesExit;
|
|
42934
|
-
};
|
|
42935
42281
|
const analyzeControlFlow = (program) => {
|
|
42936
42282
|
nextBlockId = 0;
|
|
42937
42283
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
@@ -42939,8 +42285,7 @@ const analyzeControlFlow = (program) => {
|
|
|
42939
42285
|
const cfg = buildFunctionCfg(functionNode, body);
|
|
42940
42286
|
functionCfgs.set(functionNode, {
|
|
42941
42287
|
cfg,
|
|
42942
|
-
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42943
|
-
dominatesExitSet: computeDominatesExit(cfg)
|
|
42288
|
+
unconditionalSet: computeUnconditionalSet(cfg)
|
|
42944
42289
|
});
|
|
42945
42290
|
};
|
|
42946
42291
|
if (isNodeOfType(program, "Program")) buildFor(program, {
|
|
@@ -42983,20 +42328,10 @@ const analyzeControlFlow = (program) => {
|
|
|
42983
42328
|
if (!block) return true;
|
|
42984
42329
|
return entry.unconditionalSet.has(block);
|
|
42985
42330
|
};
|
|
42986
|
-
const dominatesExit = (node) => {
|
|
42987
|
-
const owner = enclosingFunction(node);
|
|
42988
|
-
if (!owner) return true;
|
|
42989
|
-
const entry = functionCfgs.get(owner);
|
|
42990
|
-
if (!entry) return true;
|
|
42991
|
-
const block = entry.cfg.blockOf(node);
|
|
42992
|
-
if (!block) return true;
|
|
42993
|
-
return entry.dominatesExitSet.has(block);
|
|
42994
|
-
};
|
|
42995
42331
|
return {
|
|
42996
42332
|
cfgFor,
|
|
42997
42333
|
enclosingFunction,
|
|
42998
|
-
isUnconditionalFromEntry
|
|
42999
|
-
dominatesExit
|
|
42334
|
+
isUnconditionalFromEntry
|
|
43000
42335
|
};
|
|
43001
42336
|
};
|
|
43002
42337
|
//#endregion
|
|
@@ -43021,8 +42356,7 @@ const buildFallbackScopes = () => ({
|
|
|
43021
42356
|
const FALLBACK_CFG = {
|
|
43022
42357
|
cfgFor: () => null,
|
|
43023
42358
|
enclosingFunction: () => null,
|
|
43024
|
-
isUnconditionalFromEntry: () => false
|
|
43025
|
-
dominatesExit: () => false
|
|
42359
|
+
isUnconditionalFromEntry: () => false
|
|
43026
42360
|
};
|
|
43027
42361
|
const wrapWithSemanticContext = (rule) => ({
|
|
43028
42362
|
...rule,
|