oxlint-plugin-react-doctor 0.5.6-dev.b08ca1c → 0.5.6-dev.b317164
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 +126 -0
- package/dist/index.js +312 -137
- 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
|
});
|
|
@@ -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
|
})
|
|
@@ -4694,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4694
4748
|
//#endregion
|
|
4695
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4697
|
-
const MESSAGE$
|
|
4751
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4752
|
const KEY_HANDLERS = [
|
|
4699
4753
|
"onKeyUp",
|
|
4700
4754
|
"onKeyDown",
|
|
@@ -4726,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4726
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4781
|
context.report({
|
|
4728
4782
|
node: node.name,
|
|
4729
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4730
4784
|
});
|
|
4731
4785
|
} };
|
|
4732
4786
|
}
|
|
@@ -4841,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4841
4895
|
};
|
|
4842
4896
|
//#endregion
|
|
4843
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4844
|
-
const MESSAGE$
|
|
4898
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4845
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4846
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4847
4901
|
"alt",
|
|
@@ -5002,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5002
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5003
5057
|
context.report({
|
|
5004
5058
|
node: opening,
|
|
5005
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
5006
5060
|
});
|
|
5007
5061
|
} };
|
|
5008
5062
|
}
|
|
@@ -5432,7 +5486,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
5432
5486
|
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5433
5487
|
//#endregion
|
|
5434
5488
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5435
|
-
const MESSAGE$
|
|
5489
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5436
5490
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5437
5491
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5438
5492
|
"aria-label",
|
|
@@ -5455,7 +5509,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5455
5509
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5456
5510
|
context.report({
|
|
5457
5511
|
node: node.name,
|
|
5458
|
-
message: MESSAGE$
|
|
5512
|
+
message: MESSAGE$54
|
|
5459
5513
|
});
|
|
5460
5514
|
} })
|
|
5461
5515
|
});
|
|
@@ -5494,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5494
5548
|
};
|
|
5495
5549
|
//#endregion
|
|
5496
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5497
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5498
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5499
5553
|
"observer",
|
|
5500
5554
|
"lazy",
|
|
@@ -5697,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5697
5751
|
const reportAt = (node) => {
|
|
5698
5752
|
context.report({
|
|
5699
5753
|
node,
|
|
5700
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5701
5755
|
});
|
|
5702
5756
|
};
|
|
5703
5757
|
return {
|
|
@@ -7845,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7845
7899
|
});
|
|
7846
7900
|
//#endregion
|
|
7847
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7848
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7849
7903
|
const forwardRefUsesRef = defineRule({
|
|
7850
7904
|
id: "forward-ref-uses-ref",
|
|
7851
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7865,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7865
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7866
7920
|
context.report({
|
|
7867
7921
|
node: inner,
|
|
7868
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7869
7923
|
});
|
|
7870
7924
|
} })
|
|
7871
7925
|
});
|
|
@@ -7902,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7902
7956
|
});
|
|
7903
7957
|
//#endregion
|
|
7904
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7905
|
-
const MESSAGE$
|
|
7959
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7906
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7907
7961
|
"h1",
|
|
7908
7962
|
"h2",
|
|
@@ -7935,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7935
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7936
7990
|
context.report({
|
|
7937
7991
|
node,
|
|
7938
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7939
7993
|
});
|
|
7940
7994
|
} };
|
|
7941
7995
|
}
|
|
@@ -8073,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8073
8127
|
});
|
|
8074
8128
|
//#endregion
|
|
8075
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8076
|
-
const MESSAGE$
|
|
8130
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8077
8131
|
const resolveSettings$38 = (settings) => {
|
|
8078
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
8079
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8121,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
8121
8175
|
if (!lang) {
|
|
8122
8176
|
context.report({
|
|
8123
8177
|
node: node.name,
|
|
8124
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
8125
8179
|
});
|
|
8126
8180
|
return;
|
|
8127
8181
|
}
|
|
@@ -8129,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
8129
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
8130
8184
|
context.report({
|
|
8131
8185
|
node: lang,
|
|
8132
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
8133
8187
|
});
|
|
8134
8188
|
return;
|
|
8135
8189
|
}
|
|
8136
8190
|
if (hasSpread && !lang) context.report({
|
|
8137
8191
|
node: node.name,
|
|
8138
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
8139
8193
|
});
|
|
8140
8194
|
} };
|
|
8141
8195
|
}
|
|
@@ -8349,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8349
8403
|
});
|
|
8350
8404
|
//#endregion
|
|
8351
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8352
|
-
const MESSAGE$
|
|
8406
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8353
8407
|
const evaluateTitleValue = (value) => {
|
|
8354
8408
|
if (!value) return "missing";
|
|
8355
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8389,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8389
8443
|
if (!titleAttr) {
|
|
8390
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8391
8445
|
node: node.name,
|
|
8392
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8393
8447
|
});
|
|
8394
8448
|
return;
|
|
8395
8449
|
}
|
|
8396
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8397
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8398
8452
|
node: titleAttr,
|
|
8399
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8400
8454
|
});
|
|
8401
8455
|
} })
|
|
8402
8456
|
});
|
|
@@ -8500,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8500
8554
|
});
|
|
8501
8555
|
//#endregion
|
|
8502
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8503
|
-
const MESSAGE$
|
|
8557
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8504
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8505
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8506
8560
|
"image",
|
|
@@ -8565,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8565
8619
|
if (!altAttribute) return;
|
|
8566
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8567
8621
|
node: altAttribute,
|
|
8568
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8569
8623
|
});
|
|
8570
8624
|
} };
|
|
8571
8625
|
}
|
|
@@ -10922,7 +10976,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10922
10976
|
});
|
|
10923
10977
|
//#endregion
|
|
10924
10978
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10925
|
-
const MESSAGE$
|
|
10979
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10926
10980
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10927
10981
|
"code",
|
|
10928
10982
|
"pre",
|
|
@@ -10958,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10958
11012
|
if (isInsideLiteralTextTag(node)) return;
|
|
10959
11013
|
context.report({
|
|
10960
11014
|
node,
|
|
10961
|
-
message: MESSAGE$
|
|
11015
|
+
message: MESSAGE$47
|
|
10962
11016
|
});
|
|
10963
11017
|
} })
|
|
10964
11018
|
});
|
|
@@ -10989,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10989
11043
|
};
|
|
10990
11044
|
//#endregion
|
|
10991
11045
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10992
|
-
const MESSAGE$
|
|
11046
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10993
11047
|
const CONTEXT_MODULES$1 = [
|
|
10994
11048
|
"react",
|
|
10995
11049
|
"use-context-selector",
|
|
@@ -11087,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11087
11141
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11088
11142
|
context.report({
|
|
11089
11143
|
node: attribute,
|
|
11090
|
-
message: MESSAGE$
|
|
11144
|
+
message: MESSAGE$46
|
|
11091
11145
|
});
|
|
11092
11146
|
}
|
|
11093
11147
|
}
|
|
@@ -11173,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11173
11227
|
};
|
|
11174
11228
|
//#endregion
|
|
11175
11229
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11176
|
-
const MESSAGE$
|
|
11230
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11177
11231
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11178
11232
|
"icon",
|
|
11179
11233
|
"Icon",
|
|
@@ -11442,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11442
11496
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11443
11497
|
context.report({
|
|
11444
11498
|
node,
|
|
11445
|
-
message: MESSAGE$
|
|
11499
|
+
message: MESSAGE$45
|
|
11446
11500
|
});
|
|
11447
11501
|
}
|
|
11448
11502
|
};
|
|
@@ -11730,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11730
11784
|
];
|
|
11731
11785
|
//#endregion
|
|
11732
11786
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11733
|
-
const MESSAGE$
|
|
11787
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11734
11788
|
const isDataArrayPropName = (propName) => {
|
|
11735
11789
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11736
11790
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11814,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11814
11868
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11815
11869
|
context.report({
|
|
11816
11870
|
node,
|
|
11817
|
-
message: MESSAGE$
|
|
11871
|
+
message: MESSAGE$44
|
|
11818
11872
|
});
|
|
11819
11873
|
}
|
|
11820
11874
|
};
|
|
@@ -12072,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12072
12126
|
]);
|
|
12073
12127
|
//#endregion
|
|
12074
12128
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12075
|
-
const MESSAGE$
|
|
12129
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12076
12130
|
const isAccessorPredicateName = (propName) => {
|
|
12077
12131
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12078
12132
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12278,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12278
12332
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12279
12333
|
context.report({
|
|
12280
12334
|
node,
|
|
12281
|
-
message: MESSAGE$
|
|
12335
|
+
message: MESSAGE$43
|
|
12282
12336
|
});
|
|
12283
12337
|
}
|
|
12284
12338
|
};
|
|
@@ -12498,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12498
12552
|
];
|
|
12499
12553
|
//#endregion
|
|
12500
12554
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12501
|
-
const MESSAGE$
|
|
12555
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12502
12556
|
const isConfigObjectPropName = (propName) => {
|
|
12503
12557
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12504
12558
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12586,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12586
12640
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12587
12641
|
context.report({
|
|
12588
12642
|
node,
|
|
12589
|
-
message: MESSAGE$
|
|
12643
|
+
message: MESSAGE$42
|
|
12590
12644
|
});
|
|
12591
12645
|
}
|
|
12592
12646
|
};
|
|
@@ -12594,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12594
12648
|
});
|
|
12595
12649
|
//#endregion
|
|
12596
12650
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12597
|
-
const MESSAGE$
|
|
12651
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12598
12652
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12599
12653
|
const resolveSettings$28 = (settings) => {
|
|
12600
12654
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12635,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12635
12689
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12636
12690
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12637
12691
|
node: attribute,
|
|
12638
|
-
message: MESSAGE$
|
|
12692
|
+
message: MESSAGE$41
|
|
12639
12693
|
});
|
|
12640
12694
|
}
|
|
12641
12695
|
} };
|
|
@@ -12950,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12950
13004
|
});
|
|
12951
13005
|
//#endregion
|
|
12952
13006
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12953
|
-
const MESSAGE$
|
|
13007
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12954
13008
|
const resolveSettings$25 = (settings) => {
|
|
12955
13009
|
const reactDoctor = settings?.["react-doctor"];
|
|
12956
13010
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12991,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12991
13045
|
}
|
|
12992
13046
|
context.report({
|
|
12993
13047
|
node: attribute,
|
|
12994
|
-
message: MESSAGE$
|
|
13048
|
+
message: MESSAGE$40
|
|
12995
13049
|
});
|
|
12996
13050
|
}
|
|
12997
13051
|
} };
|
|
@@ -13219,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13219
13273
|
});
|
|
13220
13274
|
//#endregion
|
|
13221
13275
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13222
|
-
const MESSAGE$
|
|
13276
|
+
const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13223
13277
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13224
13278
|
"aa",
|
|
13225
13279
|
"ab",
|
|
@@ -13431,7 +13485,7 @@ const lang = defineRule({
|
|
|
13431
13485
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13432
13486
|
context.report({
|
|
13433
13487
|
node: langAttr,
|
|
13434
|
-
message: MESSAGE$
|
|
13488
|
+
message: MESSAGE$39
|
|
13435
13489
|
});
|
|
13436
13490
|
return;
|
|
13437
13491
|
}
|
|
@@ -13440,7 +13494,7 @@ const lang = defineRule({
|
|
|
13440
13494
|
if (value === null) return;
|
|
13441
13495
|
if (!isValidLangTag(value)) context.report({
|
|
13442
13496
|
node: langAttr,
|
|
13443
|
-
message: MESSAGE$
|
|
13497
|
+
message: MESSAGE$39
|
|
13444
13498
|
});
|
|
13445
13499
|
} })
|
|
13446
13500
|
});
|
|
@@ -13466,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13466
13520
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13467
13521
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13468
13522
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13523
|
+
ignoreStringLiterals: true,
|
|
13469
13524
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13470
13525
|
})
|
|
13471
13526
|
});
|
|
@@ -13484,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13484
13539
|
});
|
|
13485
13540
|
//#endregion
|
|
13486
13541
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13487
|
-
const MESSAGE$
|
|
13542
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13488
13543
|
const DEFAULT_AUDIO = ["audio"];
|
|
13489
13544
|
const DEFAULT_VIDEO = ["video"];
|
|
13490
13545
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13525,7 +13580,7 @@ const mediaHasCaption = defineRule({
|
|
|
13525
13580
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13526
13581
|
context.report({
|
|
13527
13582
|
node: node.name,
|
|
13528
|
-
message: MESSAGE$
|
|
13583
|
+
message: MESSAGE$38
|
|
13529
13584
|
});
|
|
13530
13585
|
return;
|
|
13531
13586
|
}
|
|
@@ -13542,7 +13597,7 @@ const mediaHasCaption = defineRule({
|
|
|
13542
13597
|
return kindValue.value.toLowerCase() === "captions";
|
|
13543
13598
|
})) context.report({
|
|
13544
13599
|
node: node.name,
|
|
13545
|
-
message: MESSAGE$
|
|
13600
|
+
message: MESSAGE$38
|
|
13546
13601
|
});
|
|
13547
13602
|
} };
|
|
13548
13603
|
}
|
|
@@ -15343,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15343
15398
|
});
|
|
15344
15399
|
//#endregion
|
|
15345
15400
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15346
|
-
const MESSAGE$
|
|
15401
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15347
15402
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15348
15403
|
const noAccessKey = defineRule({
|
|
15349
15404
|
id: "no-access-key",
|
|
@@ -15360,7 +15415,7 @@ const noAccessKey = defineRule({
|
|
|
15360
15415
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15361
15416
|
context.report({
|
|
15362
15417
|
node: accessKey,
|
|
15363
|
-
message: MESSAGE$
|
|
15418
|
+
message: MESSAGE$37
|
|
15364
15419
|
});
|
|
15365
15420
|
return;
|
|
15366
15421
|
}
|
|
@@ -15370,7 +15425,7 @@ const noAccessKey = defineRule({
|
|
|
15370
15425
|
if (isUndefinedIdentifier(expression)) return;
|
|
15371
15426
|
context.report({
|
|
15372
15427
|
node: accessKey,
|
|
15373
|
-
message: MESSAGE$
|
|
15428
|
+
message: MESSAGE$37
|
|
15374
15429
|
});
|
|
15375
15430
|
}
|
|
15376
15431
|
} })
|
|
@@ -15853,7 +15908,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15853
15908
|
});
|
|
15854
15909
|
//#endregion
|
|
15855
15910
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15856
|
-
const MESSAGE$
|
|
15911
|
+
const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15857
15912
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15858
15913
|
id: "no-aria-hidden-on-focusable",
|
|
15859
15914
|
title: "aria-hidden on focusable element",
|
|
@@ -15880,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15880
15935
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15881
15936
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15882
15937
|
node: ariaHidden,
|
|
15883
|
-
message: MESSAGE$
|
|
15938
|
+
message: MESSAGE$36
|
|
15884
15939
|
});
|
|
15885
15940
|
} })
|
|
15886
15941
|
});
|
|
@@ -16248,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16248
16303
|
});
|
|
16249
16304
|
//#endregion
|
|
16250
16305
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16251
|
-
const MESSAGE$
|
|
16306
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16252
16307
|
const SECOND_INDEX_METHODS = new Set([
|
|
16253
16308
|
"every",
|
|
16254
16309
|
"filter",
|
|
@@ -16452,7 +16507,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16452
16507
|
}
|
|
16453
16508
|
context.report({
|
|
16454
16509
|
node: keyAttribute,
|
|
16455
|
-
message: MESSAGE$
|
|
16510
|
+
message: MESSAGE$35
|
|
16456
16511
|
});
|
|
16457
16512
|
},
|
|
16458
16513
|
CallExpression(node) {
|
|
@@ -16472,7 +16527,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16472
16527
|
if (propName !== "key") continue;
|
|
16473
16528
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16474
16529
|
node: property,
|
|
16475
|
-
message: MESSAGE$
|
|
16530
|
+
message: MESSAGE$35
|
|
16476
16531
|
});
|
|
16477
16532
|
}
|
|
16478
16533
|
}
|
|
@@ -16480,7 +16535,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16480
16535
|
});
|
|
16481
16536
|
//#endregion
|
|
16482
16537
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16483
|
-
const MESSAGE$
|
|
16538
|
+
const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16484
16539
|
const noAsyncEffectCallback = defineRule({
|
|
16485
16540
|
id: "no-async-effect-callback",
|
|
16486
16541
|
title: "Async effect callback",
|
|
@@ -16494,13 +16549,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16494
16549
|
if (!callback.async) return;
|
|
16495
16550
|
context.report({
|
|
16496
16551
|
node: callback,
|
|
16497
|
-
message: MESSAGE$
|
|
16552
|
+
message: MESSAGE$34
|
|
16498
16553
|
});
|
|
16499
16554
|
} })
|
|
16500
16555
|
});
|
|
16501
16556
|
//#endregion
|
|
16502
16557
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16503
|
-
const MESSAGE$
|
|
16558
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16504
16559
|
const resolveSettings$21 = (settings) => {
|
|
16505
16560
|
const reactDoctor = settings?.["react-doctor"];
|
|
16506
16561
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16556,7 +16611,7 @@ const noAutofocus = defineRule({
|
|
|
16556
16611
|
}
|
|
16557
16612
|
context.report({
|
|
16558
16613
|
node: autoFocusAttribute,
|
|
16559
|
-
message: MESSAGE$
|
|
16614
|
+
message: MESSAGE$33
|
|
16560
16615
|
});
|
|
16561
16616
|
} };
|
|
16562
16617
|
}
|
|
@@ -17060,7 +17115,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17060
17115
|
});
|
|
17061
17116
|
//#endregion
|
|
17062
17117
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17063
|
-
const MESSAGE$
|
|
17118
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17064
17119
|
const noChildrenProp = defineRule({
|
|
17065
17120
|
id: "no-children-prop",
|
|
17066
17121
|
title: "Children passed as a prop",
|
|
@@ -17072,7 +17127,7 @@ const noChildrenProp = defineRule({
|
|
|
17072
17127
|
if (node.name.name !== "children") return;
|
|
17073
17128
|
context.report({
|
|
17074
17129
|
node: node.name,
|
|
17075
|
-
message: MESSAGE$
|
|
17130
|
+
message: MESSAGE$32
|
|
17076
17131
|
});
|
|
17077
17132
|
},
|
|
17078
17133
|
CallExpression(node) {
|
|
@@ -17085,7 +17140,7 @@ const noChildrenProp = defineRule({
|
|
|
17085
17140
|
const propertyKey = property.key;
|
|
17086
17141
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17087
17142
|
node: propertyKey,
|
|
17088
|
-
message: MESSAGE$
|
|
17143
|
+
message: MESSAGE$32
|
|
17089
17144
|
});
|
|
17090
17145
|
}
|
|
17091
17146
|
}
|
|
@@ -17093,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
17093
17148
|
});
|
|
17094
17149
|
//#endregion
|
|
17095
17150
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17096
|
-
const MESSAGE$
|
|
17151
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17097
17152
|
const noCloneElement = defineRule({
|
|
17098
17153
|
id: "no-clone-element",
|
|
17099
17154
|
title: "cloneElement makes child props fragile",
|
|
@@ -17106,7 +17161,7 @@ const noCloneElement = defineRule({
|
|
|
17106
17161
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17107
17162
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17108
17163
|
node: callee,
|
|
17109
|
-
message: MESSAGE$
|
|
17164
|
+
message: MESSAGE$31
|
|
17110
17165
|
});
|
|
17111
17166
|
return;
|
|
17112
17167
|
}
|
|
@@ -17119,7 +17174,7 @@ const noCloneElement = defineRule({
|
|
|
17119
17174
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17120
17175
|
context.report({
|
|
17121
17176
|
node: callee,
|
|
17122
|
-
message: MESSAGE$
|
|
17177
|
+
message: MESSAGE$31
|
|
17123
17178
|
});
|
|
17124
17179
|
}
|
|
17125
17180
|
} })
|
|
@@ -17168,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17168
17223
|
};
|
|
17169
17224
|
//#endregion
|
|
17170
17225
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17171
|
-
const MESSAGE$
|
|
17226
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17172
17227
|
const CONTEXT_MODULES = [
|
|
17173
17228
|
"react",
|
|
17174
17229
|
"use-context-selector",
|
|
@@ -17204,13 +17259,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17204
17259
|
if (!componentOrHookName) return;
|
|
17205
17260
|
context.report({
|
|
17206
17261
|
node,
|
|
17207
|
-
message: `${MESSAGE$
|
|
17262
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17208
17263
|
});
|
|
17209
17264
|
} })
|
|
17210
17265
|
});
|
|
17211
17266
|
//#endregion
|
|
17212
17267
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17213
|
-
const MESSAGE$
|
|
17268
|
+
const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17214
17269
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17215
17270
|
id: "no-create-ref-in-function-component",
|
|
17216
17271
|
title: "createRef in function component",
|
|
@@ -17229,7 +17284,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17229
17284
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17230
17285
|
context.report({
|
|
17231
17286
|
node,
|
|
17232
|
-
message: MESSAGE$
|
|
17287
|
+
message: MESSAGE$29
|
|
17233
17288
|
});
|
|
17234
17289
|
} })
|
|
17235
17290
|
});
|
|
@@ -17369,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17369
17424
|
});
|
|
17370
17425
|
//#endregion
|
|
17371
17426
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17372
|
-
const MESSAGE$
|
|
17427
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17373
17428
|
const noDanger = defineRule({
|
|
17374
17429
|
id: "no-danger",
|
|
17375
17430
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17382,7 +17437,7 @@ const noDanger = defineRule({
|
|
|
17382
17437
|
if (!propAttribute) return;
|
|
17383
17438
|
context.report({
|
|
17384
17439
|
node: propAttribute.name,
|
|
17385
|
-
message: MESSAGE$
|
|
17440
|
+
message: MESSAGE$28
|
|
17386
17441
|
});
|
|
17387
17442
|
},
|
|
17388
17443
|
CallExpression(node) {
|
|
@@ -17394,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17394
17449
|
const propertyKey = property.key;
|
|
17395
17450
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17396
17451
|
node: propertyKey,
|
|
17397
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$28
|
|
17398
17453
|
});
|
|
17399
17454
|
}
|
|
17400
17455
|
}
|
|
@@ -17402,7 +17457,7 @@ const noDanger = defineRule({
|
|
|
17402
17457
|
});
|
|
17403
17458
|
//#endregion
|
|
17404
17459
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17405
|
-
const MESSAGE$
|
|
17460
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17406
17461
|
const isLineBreak = (child) => {
|
|
17407
17462
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17408
17463
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17472,7 +17527,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17472
17527
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17473
17528
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17474
17529
|
node: opening,
|
|
17475
|
-
message: MESSAGE$
|
|
17530
|
+
message: MESSAGE$27
|
|
17476
17531
|
});
|
|
17477
17532
|
},
|
|
17478
17533
|
CallExpression(node) {
|
|
@@ -17484,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17484
17539
|
if (!propsShape.hasDangerously) return;
|
|
17485
17540
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17486
17541
|
node,
|
|
17487
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$27
|
|
17488
17543
|
});
|
|
17489
17544
|
}
|
|
17490
17545
|
})
|
|
@@ -18061,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18061
18116
|
//#endregion
|
|
18062
18117
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18063
18118
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18064
|
-
const MESSAGE$
|
|
18119
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18065
18120
|
const resolveSettings$20 = (settings) => {
|
|
18066
18121
|
const reactDoctor = settings?.["react-doctor"];
|
|
18067
18122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18080,7 +18135,7 @@ const noDidMountSetState = defineRule({
|
|
|
18080
18135
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18081
18136
|
context.report({
|
|
18082
18137
|
node: node.callee,
|
|
18083
|
-
message: MESSAGE$
|
|
18138
|
+
message: MESSAGE$26
|
|
18084
18139
|
});
|
|
18085
18140
|
} };
|
|
18086
18141
|
}
|
|
@@ -18088,7 +18143,7 @@ const noDidMountSetState = defineRule({
|
|
|
18088
18143
|
//#endregion
|
|
18089
18144
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18090
18145
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18091
|
-
const MESSAGE$
|
|
18146
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18092
18147
|
const resolveSettings$19 = (settings) => {
|
|
18093
18148
|
const reactDoctor = settings?.["react-doctor"];
|
|
18094
18149
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18107,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18107
18162
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18108
18163
|
context.report({
|
|
18109
18164
|
node: node.callee,
|
|
18110
|
-
message: MESSAGE$
|
|
18165
|
+
message: MESSAGE$25
|
|
18111
18166
|
});
|
|
18112
18167
|
} };
|
|
18113
18168
|
}
|
|
@@ -18130,7 +18185,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18130
18185
|
};
|
|
18131
18186
|
//#endregion
|
|
18132
18187
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18133
|
-
const MESSAGE$
|
|
18188
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18134
18189
|
const shouldIgnoreMutation = (node) => {
|
|
18135
18190
|
let isConstructor = false;
|
|
18136
18191
|
let isInsideCallExpression = false;
|
|
@@ -18152,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18152
18207
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18153
18208
|
context.report({
|
|
18154
18209
|
node: reportNode,
|
|
18155
|
-
message: MESSAGE$
|
|
18210
|
+
message: MESSAGE$24
|
|
18156
18211
|
});
|
|
18157
18212
|
};
|
|
18158
18213
|
const noDirectMutationState = defineRule({
|
|
@@ -18362,6 +18417,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18362
18417
|
} })
|
|
18363
18418
|
});
|
|
18364
18419
|
//#endregion
|
|
18420
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18421
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18422
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18423
|
+
const noDocumentWrite = defineRule({
|
|
18424
|
+
id: "no-document-write",
|
|
18425
|
+
title: "document.write/writeln",
|
|
18426
|
+
severity: "warn",
|
|
18427
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18428
|
+
create: (context) => ({ CallExpression(node) {
|
|
18429
|
+
const callee = node.callee;
|
|
18430
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18431
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18432
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18433
|
+
context.report({
|
|
18434
|
+
node,
|
|
18435
|
+
message: MESSAGE$23
|
|
18436
|
+
});
|
|
18437
|
+
} })
|
|
18438
|
+
});
|
|
18439
|
+
//#endregion
|
|
18365
18440
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18366
18441
|
const noDynamicImportPath = defineRule({
|
|
18367
18442
|
id: "no-dynamic-import-path",
|
|
@@ -19740,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19740
19815
|
"ReactDOM",
|
|
19741
19816
|
"ReactDom"
|
|
19742
19817
|
]);
|
|
19743
|
-
const MESSAGE$
|
|
19818
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19744
19819
|
const noFindDomNode = defineRule({
|
|
19745
19820
|
id: "no-find-dom-node",
|
|
19746
19821
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19751,7 +19826,7 @@ const noFindDomNode = defineRule({
|
|
|
19751
19826
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19752
19827
|
context.report({
|
|
19753
19828
|
node: callee,
|
|
19754
|
-
message: MESSAGE$
|
|
19829
|
+
message: MESSAGE$22
|
|
19755
19830
|
});
|
|
19756
19831
|
return;
|
|
19757
19832
|
}
|
|
@@ -19762,7 +19837,7 @@ const noFindDomNode = defineRule({
|
|
|
19762
19837
|
if (callee.property.name !== "findDOMNode") return;
|
|
19763
19838
|
context.report({
|
|
19764
19839
|
node: callee.property,
|
|
19765
|
-
message: MESSAGE$
|
|
19840
|
+
message: MESSAGE$22
|
|
19766
19841
|
});
|
|
19767
19842
|
}
|
|
19768
19843
|
} })
|
|
@@ -20004,7 +20079,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20004
20079
|
});
|
|
20005
20080
|
//#endregion
|
|
20006
20081
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20007
|
-
const MESSAGE$
|
|
20082
|
+
const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20008
20083
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20009
20084
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20010
20085
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20018,7 +20093,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20018
20093
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20019
20094
|
context.report({
|
|
20020
20095
|
node: node.name,
|
|
20021
|
-
message: MESSAGE$
|
|
20096
|
+
message: MESSAGE$21
|
|
20022
20097
|
});
|
|
20023
20098
|
} })
|
|
20024
20099
|
});
|
|
@@ -20253,7 +20328,7 @@ const noIsMounted = defineRule({
|
|
|
20253
20328
|
});
|
|
20254
20329
|
//#endregion
|
|
20255
20330
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20256
|
-
const MESSAGE$
|
|
20331
|
+
const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20257
20332
|
const isJsonMethodCall = (node, method) => {
|
|
20258
20333
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20259
20334
|
const callee = node.callee;
|
|
@@ -20270,13 +20345,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20270
20345
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20271
20346
|
context.report({
|
|
20272
20347
|
node,
|
|
20273
|
-
message: MESSAGE$
|
|
20348
|
+
message: MESSAGE$20
|
|
20274
20349
|
});
|
|
20275
20350
|
} })
|
|
20276
20351
|
});
|
|
20277
20352
|
//#endregion
|
|
20278
20353
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20279
|
-
const MESSAGE$
|
|
20354
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20280
20355
|
const isJsxElementTypeReference = (node) => {
|
|
20281
20356
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20282
20357
|
const typeName = node.typeName;
|
|
@@ -20293,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20293
20368
|
if (!typeAnnotation) return;
|
|
20294
20369
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20295
20370
|
node: typeAnnotation,
|
|
20296
|
-
message: MESSAGE$
|
|
20371
|
+
message: MESSAGE$19
|
|
20297
20372
|
});
|
|
20298
20373
|
};
|
|
20299
20374
|
const noJsxElementType = defineRule({
|
|
@@ -20748,7 +20823,7 @@ const noMoment = defineRule({
|
|
|
20748
20823
|
});
|
|
20749
20824
|
//#endregion
|
|
20750
20825
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20751
|
-
const MESSAGE$
|
|
20826
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20752
20827
|
const resolveSettings$16 = (settings) => {
|
|
20753
20828
|
const reactDoctor = settings?.["react-doctor"];
|
|
20754
20829
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21070,7 +21145,7 @@ const noMultiComp = defineRule({
|
|
|
21070
21145
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21071
21146
|
for (const component of flagged.slice(1)) context.report({
|
|
21072
21147
|
node: component.reportNode,
|
|
21073
|
-
message: MESSAGE$
|
|
21148
|
+
message: MESSAGE$18
|
|
21074
21149
|
});
|
|
21075
21150
|
} };
|
|
21076
21151
|
}
|
|
@@ -21238,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21238
21313
|
};
|
|
21239
21314
|
//#endregion
|
|
21240
21315
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21241
|
-
const MESSAGE$
|
|
21316
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21242
21317
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21243
21318
|
"copyWithin",
|
|
21244
21319
|
"fill",
|
|
@@ -21448,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21448
21523
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21449
21524
|
context.report({
|
|
21450
21525
|
node: options.crossFileConsumerCallSite,
|
|
21451
|
-
message: `${MESSAGE$
|
|
21526
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21452
21527
|
});
|
|
21453
21528
|
return;
|
|
21454
21529
|
}
|
|
@@ -21457,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21457
21532
|
reportedNodes.add(mutation.node);
|
|
21458
21533
|
context.report({
|
|
21459
21534
|
node: mutation.node,
|
|
21460
|
-
message: MESSAGE$
|
|
21535
|
+
message: MESSAGE$17
|
|
21461
21536
|
});
|
|
21462
21537
|
}
|
|
21463
21538
|
};
|
|
@@ -21729,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21729
21804
|
});
|
|
21730
21805
|
//#endregion
|
|
21731
21806
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21732
|
-
const MESSAGE$
|
|
21807
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21733
21808
|
const resolveSettings$14 = (settings) => {
|
|
21734
21809
|
const reactDoctor = settings?.["react-doctor"];
|
|
21735
21810
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21757,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21757
21832
|
if (numeric === null) {
|
|
21758
21833
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21759
21834
|
node: tabIndex,
|
|
21760
|
-
message: MESSAGE$
|
|
21835
|
+
message: MESSAGE$16
|
|
21761
21836
|
});
|
|
21762
21837
|
return;
|
|
21763
21838
|
}
|
|
@@ -21770,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21770
21845
|
if (!roleAttribute) {
|
|
21771
21846
|
context.report({
|
|
21772
21847
|
node: tabIndex,
|
|
21773
|
-
message: MESSAGE$
|
|
21848
|
+
message: MESSAGE$16
|
|
21774
21849
|
});
|
|
21775
21850
|
return;
|
|
21776
21851
|
}
|
|
@@ -21784,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21784
21859
|
}
|
|
21785
21860
|
context.report({
|
|
21786
21861
|
node: tabIndex,
|
|
21787
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
21788
21863
|
});
|
|
21789
21864
|
} };
|
|
21790
21865
|
}
|
|
@@ -22475,7 +22550,7 @@ const noRandomKey = defineRule({
|
|
|
22475
22550
|
});
|
|
22476
22551
|
//#endregion
|
|
22477
22552
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22478
|
-
const MESSAGE$
|
|
22553
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22479
22554
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22480
22555
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22481
22556
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22501,13 +22576,13 @@ const noReactChildren = defineRule({
|
|
|
22501
22576
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22502
22577
|
context.report({
|
|
22503
22578
|
node: calleeOuter,
|
|
22504
|
-
message: MESSAGE$
|
|
22579
|
+
message: MESSAGE$15
|
|
22505
22580
|
});
|
|
22506
22581
|
return;
|
|
22507
22582
|
}
|
|
22508
22583
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22509
22584
|
node: calleeOuter,
|
|
22510
|
-
message: MESSAGE$
|
|
22585
|
+
message: MESSAGE$15
|
|
22511
22586
|
});
|
|
22512
22587
|
} })
|
|
22513
22588
|
});
|
|
@@ -22830,7 +22905,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22830
22905
|
});
|
|
22831
22906
|
//#endregion
|
|
22832
22907
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22833
|
-
const MESSAGE$
|
|
22908
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22834
22909
|
const isReactDomRenderCall = (node) => {
|
|
22835
22910
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22836
22911
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22854,7 +22929,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22854
22929
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22855
22930
|
context.report({
|
|
22856
22931
|
node: node.callee,
|
|
22857
|
-
message: MESSAGE$
|
|
22932
|
+
message: MESSAGE$14
|
|
22858
22933
|
});
|
|
22859
22934
|
} })
|
|
22860
22935
|
});
|
|
@@ -23552,7 +23627,7 @@ const getParentComponent = (node) => {
|
|
|
23552
23627
|
};
|
|
23553
23628
|
//#endregion
|
|
23554
23629
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23555
|
-
const MESSAGE$
|
|
23630
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23556
23631
|
const noSetState = defineRule({
|
|
23557
23632
|
id: "no-set-state",
|
|
23558
23633
|
title: "Local class state forbidden",
|
|
@@ -23567,7 +23642,7 @@ const noSetState = defineRule({
|
|
|
23567
23642
|
if (!getParentComponent(node)) return;
|
|
23568
23643
|
context.report({
|
|
23569
23644
|
node: node.callee,
|
|
23570
|
-
message: MESSAGE$
|
|
23645
|
+
message: MESSAGE$13
|
|
23571
23646
|
});
|
|
23572
23647
|
} })
|
|
23573
23648
|
});
|
|
@@ -23729,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23729
23804
|
};
|
|
23730
23805
|
//#endregion
|
|
23731
23806
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23732
|
-
const MESSAGE$
|
|
23807
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
23733
23808
|
const DEFAULT_HANDLERS = [
|
|
23734
23809
|
"onClick",
|
|
23735
23810
|
"onMouseDown",
|
|
@@ -23789,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23789
23864
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23790
23865
|
context.report({
|
|
23791
23866
|
node: node.name,
|
|
23792
|
-
message: MESSAGE$
|
|
23867
|
+
message: MESSAGE$12
|
|
23793
23868
|
});
|
|
23794
23869
|
return;
|
|
23795
23870
|
}
|
|
@@ -23799,19 +23874,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23799
23874
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23800
23875
|
context.report({
|
|
23801
23876
|
node: node.name,
|
|
23802
|
-
message: MESSAGE$
|
|
23877
|
+
message: MESSAGE$12
|
|
23803
23878
|
});
|
|
23804
23879
|
return;
|
|
23805
23880
|
}
|
|
23806
23881
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23807
23882
|
context.report({
|
|
23808
23883
|
node: node.name,
|
|
23809
|
-
message: MESSAGE$
|
|
23884
|
+
message: MESSAGE$12
|
|
23810
23885
|
});
|
|
23811
23886
|
} };
|
|
23812
23887
|
}
|
|
23813
23888
|
});
|
|
23814
23889
|
//#endregion
|
|
23890
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
23891
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
23892
|
+
"disabled",
|
|
23893
|
+
"checked",
|
|
23894
|
+
"readonly",
|
|
23895
|
+
"required",
|
|
23896
|
+
"selected",
|
|
23897
|
+
"multiple",
|
|
23898
|
+
"autofocus",
|
|
23899
|
+
"autoplay",
|
|
23900
|
+
"controls",
|
|
23901
|
+
"loop",
|
|
23902
|
+
"muted",
|
|
23903
|
+
"open",
|
|
23904
|
+
"reversed",
|
|
23905
|
+
"default",
|
|
23906
|
+
"novalidate",
|
|
23907
|
+
"formnovalidate",
|
|
23908
|
+
"playsinline",
|
|
23909
|
+
"itemscope",
|
|
23910
|
+
"allowfullscreen"
|
|
23911
|
+
]);
|
|
23912
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
23913
|
+
id: "no-string-false-on-boolean-attribute",
|
|
23914
|
+
title: "String true/false on a boolean attribute",
|
|
23915
|
+
severity: "warn",
|
|
23916
|
+
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
23917
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23918
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23919
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
23920
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
23921
|
+
for (const attribute of node.attributes) {
|
|
23922
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23923
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23924
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
23925
|
+
const value = getJsxPropStringValue(attribute);
|
|
23926
|
+
if (value !== "false" && value !== "true") continue;
|
|
23927
|
+
const attributeName = attribute.name.name;
|
|
23928
|
+
const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
|
|
23929
|
+
context.report({
|
|
23930
|
+
node: attribute,
|
|
23931
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
23932
|
+
});
|
|
23933
|
+
}
|
|
23934
|
+
} })
|
|
23935
|
+
});
|
|
23936
|
+
//#endregion
|
|
23815
23937
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23816
23938
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23817
23939
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23862,6 +23984,27 @@ const noStringRefs = defineRule({
|
|
|
23862
23984
|
}
|
|
23863
23985
|
});
|
|
23864
23986
|
//#endregion
|
|
23987
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
23988
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
23989
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
23990
|
+
const noSyncXhr = defineRule({
|
|
23991
|
+
id: "no-sync-xhr",
|
|
23992
|
+
title: "Synchronous XMLHttpRequest",
|
|
23993
|
+
severity: "warn",
|
|
23994
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
23995
|
+
create: (context) => ({ CallExpression(node) {
|
|
23996
|
+
const callee = node.callee;
|
|
23997
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
23998
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
23999
|
+
const asyncArgument = node.arguments?.[2];
|
|
24000
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24001
|
+
context.report({
|
|
24002
|
+
node,
|
|
24003
|
+
message: MESSAGE$11
|
|
24004
|
+
});
|
|
24005
|
+
} })
|
|
24006
|
+
});
|
|
24007
|
+
//#endregion
|
|
23865
24008
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23866
24009
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23867
24010
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -35437,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35437
35580
|
const collectDeclaredNames = (declaration) => {
|
|
35438
35581
|
const names = /* @__PURE__ */ new Set();
|
|
35439
35582
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35440
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35441
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35442
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35443
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35444
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35445
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35446
|
-
}
|
|
35583
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35447
35584
|
return names;
|
|
35448
35585
|
};
|
|
35449
35586
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35453,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35453
35590
|
};
|
|
35454
35591
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35455
35592
|
if (names.size === 0) return false;
|
|
35593
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35456
35594
|
let didRead = false;
|
|
35457
|
-
|
|
35458
|
-
if (
|
|
35459
|
-
|
|
35460
|
-
|
|
35595
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35596
|
+
if (!declarator.init) continue;
|
|
35597
|
+
walkAst(declarator.init, (child) => {
|
|
35598
|
+
if (didRead) return;
|
|
35599
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35600
|
+
});
|
|
35601
|
+
}
|
|
35461
35602
|
return didRead;
|
|
35462
35603
|
};
|
|
35463
35604
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36717,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36717
36858
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36718
36859
|
scan: scanByPattern({
|
|
36719
36860
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36720
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36861
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36721
36862
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36722
36863
|
})
|
|
36723
36864
|
});
|
|
@@ -39173,6 +39314,17 @@ const reactDoctorRules = [
|
|
|
39173
39314
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39174
39315
|
}
|
|
39175
39316
|
},
|
|
39317
|
+
{
|
|
39318
|
+
key: "react-doctor/no-document-write",
|
|
39319
|
+
id: "no-document-write",
|
|
39320
|
+
source: "react-doctor",
|
|
39321
|
+
originallyExternal: false,
|
|
39322
|
+
rule: {
|
|
39323
|
+
...noDocumentWrite,
|
|
39324
|
+
framework: "global",
|
|
39325
|
+
category: "Performance"
|
|
39326
|
+
}
|
|
39327
|
+
},
|
|
39176
39328
|
{
|
|
39177
39329
|
key: "react-doctor/no-dynamic-import-path",
|
|
39178
39330
|
id: "no-dynamic-import-path",
|
|
@@ -39982,6 +40134,18 @@ const reactDoctorRules = [
|
|
|
39982
40134
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39983
40135
|
}
|
|
39984
40136
|
},
|
|
40137
|
+
{
|
|
40138
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40139
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40140
|
+
source: "react-doctor",
|
|
40141
|
+
originallyExternal: false,
|
|
40142
|
+
rule: {
|
|
40143
|
+
...noStringFalseOnBooleanAttribute,
|
|
40144
|
+
framework: "global",
|
|
40145
|
+
category: "Bugs",
|
|
40146
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40147
|
+
}
|
|
40148
|
+
},
|
|
39985
40149
|
{
|
|
39986
40150
|
key: "react-doctor/no-string-refs",
|
|
39987
40151
|
id: "no-string-refs",
|
|
@@ -39994,6 +40158,17 @@ const reactDoctorRules = [
|
|
|
39994
40158
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39995
40159
|
}
|
|
39996
40160
|
},
|
|
40161
|
+
{
|
|
40162
|
+
key: "react-doctor/no-sync-xhr",
|
|
40163
|
+
id: "no-sync-xhr",
|
|
40164
|
+
source: "react-doctor",
|
|
40165
|
+
originallyExternal: false,
|
|
40166
|
+
rule: {
|
|
40167
|
+
...noSyncXhr,
|
|
40168
|
+
framework: "global",
|
|
40169
|
+
category: "Performance"
|
|
40170
|
+
}
|
|
40171
|
+
},
|
|
39997
40172
|
{
|
|
39998
40173
|
key: "react-doctor/no-this-in-sfc",
|
|
39999
40174
|
id: "no-this-in-sfc",
|