oxlint-plugin-react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.937a7ca
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 +451 -150
- package/package.json +2 -2
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
|
});
|
|
@@ -3104,6 +3158,76 @@ const AUTH_FUNCTION_NAMES = new Set([
|
|
|
3104
3158
|
"getAuth",
|
|
3105
3159
|
"validateSession"
|
|
3106
3160
|
]);
|
|
3161
|
+
const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
|
|
3162
|
+
const AUTH_STANDALONE_NOUN_TOKENS = new Set([
|
|
3163
|
+
"signedin",
|
|
3164
|
+
"loggedin",
|
|
3165
|
+
"signin"
|
|
3166
|
+
]);
|
|
3167
|
+
const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
|
|
3168
|
+
"require",
|
|
3169
|
+
"ensure",
|
|
3170
|
+
"assert",
|
|
3171
|
+
"verify",
|
|
3172
|
+
"validate",
|
|
3173
|
+
"check",
|
|
3174
|
+
"protect",
|
|
3175
|
+
"enforce",
|
|
3176
|
+
"guard",
|
|
3177
|
+
"gate",
|
|
3178
|
+
"restrict",
|
|
3179
|
+
"is",
|
|
3180
|
+
"has",
|
|
3181
|
+
"can",
|
|
3182
|
+
"must"
|
|
3183
|
+
]);
|
|
3184
|
+
const AUTH_GETTER_VERB_TOKENS = new Set([
|
|
3185
|
+
"get",
|
|
3186
|
+
"fetch",
|
|
3187
|
+
"load",
|
|
3188
|
+
"read",
|
|
3189
|
+
"resolve",
|
|
3190
|
+
"retrieve",
|
|
3191
|
+
"use"
|
|
3192
|
+
]);
|
|
3193
|
+
const AUTH_QUALIFIER_TOKENS = new Set([
|
|
3194
|
+
"current",
|
|
3195
|
+
"my",
|
|
3196
|
+
"own"
|
|
3197
|
+
]);
|
|
3198
|
+
const AUTH_STRONG_NOUN_TOKENS = new Set([
|
|
3199
|
+
"session",
|
|
3200
|
+
"sessions",
|
|
3201
|
+
"login",
|
|
3202
|
+
"admin",
|
|
3203
|
+
"admins",
|
|
3204
|
+
"superadmin",
|
|
3205
|
+
"superuser",
|
|
3206
|
+
"role",
|
|
3207
|
+
"roles",
|
|
3208
|
+
"permission",
|
|
3209
|
+
"permissions",
|
|
3210
|
+
"jwt",
|
|
3211
|
+
"identity",
|
|
3212
|
+
"principal",
|
|
3213
|
+
"credential",
|
|
3214
|
+
"credentials"
|
|
3215
|
+
]);
|
|
3216
|
+
const AUTH_WEAK_NOUN_TOKENS = new Set([
|
|
3217
|
+
"user",
|
|
3218
|
+
"users",
|
|
3219
|
+
"account",
|
|
3220
|
+
"accounts",
|
|
3221
|
+
"token",
|
|
3222
|
+
"tokens",
|
|
3223
|
+
"access",
|
|
3224
|
+
"me",
|
|
3225
|
+
"viewer",
|
|
3226
|
+
"caller",
|
|
3227
|
+
"subject",
|
|
3228
|
+
"scope",
|
|
3229
|
+
"scopes"
|
|
3230
|
+
]);
|
|
3107
3231
|
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3108
3232
|
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3109
3233
|
const SECRET_PATTERNS = [
|
|
@@ -4202,7 +4326,7 @@ const asyncParallel = defineRule({
|
|
|
4202
4326
|
});
|
|
4203
4327
|
//#endregion
|
|
4204
4328
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4205
|
-
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.";
|
|
4206
4330
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4207
4331
|
const STORAGE_GLOBALS = new Set([
|
|
4208
4332
|
"window",
|
|
@@ -4236,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4236
4360
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4237
4361
|
context.report({
|
|
4238
4362
|
node,
|
|
4239
|
-
message: MESSAGE$
|
|
4363
|
+
message: MESSAGE$57
|
|
4240
4364
|
});
|
|
4241
4365
|
},
|
|
4242
4366
|
AssignmentExpression(node) {
|
|
@@ -4247,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4247
4371
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4248
4372
|
context.report({
|
|
4249
4373
|
node: target,
|
|
4250
|
-
message: MESSAGE$
|
|
4374
|
+
message: MESSAGE$57
|
|
4251
4375
|
});
|
|
4252
4376
|
}
|
|
4253
4377
|
})
|
|
@@ -4624,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4624
4748
|
//#endregion
|
|
4625
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4626
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4627
|
-
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`.";
|
|
4628
4752
|
const KEY_HANDLERS = [
|
|
4629
4753
|
"onKeyUp",
|
|
4630
4754
|
"onKeyDown",
|
|
@@ -4656,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4656
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4657
4781
|
context.report({
|
|
4658
4782
|
node: node.name,
|
|
4659
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4660
4784
|
});
|
|
4661
4785
|
} };
|
|
4662
4786
|
}
|
|
@@ -4771,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4771
4895
|
};
|
|
4772
4896
|
//#endregion
|
|
4773
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4774
|
-
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`.";
|
|
4775
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4776
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4777
4901
|
"alt",
|
|
@@ -4932,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4932
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4933
5057
|
context.report({
|
|
4934
5058
|
node: opening,
|
|
4935
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
4936
5060
|
});
|
|
4937
5061
|
} };
|
|
4938
5062
|
}
|
|
@@ -5362,7 +5486,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
5362
5486
|
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5363
5487
|
//#endregion
|
|
5364
5488
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5365
|
-
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.";
|
|
5366
5490
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5367
5491
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5368
5492
|
"aria-label",
|
|
@@ -5385,7 +5509,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5385
5509
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5386
5510
|
context.report({
|
|
5387
5511
|
node: node.name,
|
|
5388
|
-
message: MESSAGE$
|
|
5512
|
+
message: MESSAGE$54
|
|
5389
5513
|
});
|
|
5390
5514
|
} })
|
|
5391
5515
|
});
|
|
@@ -5424,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5424
5548
|
};
|
|
5425
5549
|
//#endregion
|
|
5426
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5427
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5428
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5429
5553
|
"observer",
|
|
5430
5554
|
"lazy",
|
|
@@ -5627,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5627
5751
|
const reportAt = (node) => {
|
|
5628
5752
|
context.report({
|
|
5629
5753
|
node,
|
|
5630
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5631
5755
|
});
|
|
5632
5756
|
};
|
|
5633
5757
|
return {
|
|
@@ -7775,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7775
7899
|
});
|
|
7776
7900
|
//#endregion
|
|
7777
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7778
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7779
7903
|
const forwardRefUsesRef = defineRule({
|
|
7780
7904
|
id: "forward-ref-uses-ref",
|
|
7781
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7795,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7795
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7796
7920
|
context.report({
|
|
7797
7921
|
node: inner,
|
|
7798
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7799
7923
|
});
|
|
7800
7924
|
} })
|
|
7801
7925
|
});
|
|
@@ -7832,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7832
7956
|
});
|
|
7833
7957
|
//#endregion
|
|
7834
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7835
|
-
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`.";
|
|
7836
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7837
7961
|
"h1",
|
|
7838
7962
|
"h2",
|
|
@@ -7865,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7865
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7866
7990
|
context.report({
|
|
7867
7991
|
node,
|
|
7868
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7869
7993
|
});
|
|
7870
7994
|
} };
|
|
7871
7995
|
}
|
|
@@ -8003,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8003
8127
|
});
|
|
8004
8128
|
//#endregion
|
|
8005
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8006
|
-
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`.";
|
|
8007
8131
|
const resolveSettings$38 = (settings) => {
|
|
8008
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
8009
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8051,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
8051
8175
|
if (!lang) {
|
|
8052
8176
|
context.report({
|
|
8053
8177
|
node: node.name,
|
|
8054
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
8055
8179
|
});
|
|
8056
8180
|
return;
|
|
8057
8181
|
}
|
|
@@ -8059,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
8059
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
8060
8184
|
context.report({
|
|
8061
8185
|
node: lang,
|
|
8062
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
8063
8187
|
});
|
|
8064
8188
|
return;
|
|
8065
8189
|
}
|
|
8066
8190
|
if (hasSpread && !lang) context.report({
|
|
8067
8191
|
node: node.name,
|
|
8068
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
8069
8193
|
});
|
|
8070
8194
|
} };
|
|
8071
8195
|
}
|
|
@@ -8279,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8279
8403
|
});
|
|
8280
8404
|
//#endregion
|
|
8281
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8282
|
-
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.";
|
|
8283
8407
|
const evaluateTitleValue = (value) => {
|
|
8284
8408
|
if (!value) return "missing";
|
|
8285
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8319,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8319
8443
|
if (!titleAttr) {
|
|
8320
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8321
8445
|
node: node.name,
|
|
8322
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8323
8447
|
});
|
|
8324
8448
|
return;
|
|
8325
8449
|
}
|
|
8326
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8327
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8328
8452
|
node: titleAttr,
|
|
8329
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8330
8454
|
});
|
|
8331
8455
|
} })
|
|
8332
8456
|
});
|
|
@@ -8430,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8430
8554
|
});
|
|
8431
8555
|
//#endregion
|
|
8432
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8433
|
-
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.";
|
|
8434
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8435
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8436
8560
|
"image",
|
|
@@ -8495,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8495
8619
|
if (!altAttribute) return;
|
|
8496
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8497
8621
|
node: altAttribute,
|
|
8498
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8499
8623
|
});
|
|
8500
8624
|
} };
|
|
8501
8625
|
}
|
|
@@ -10852,7 +10976,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10852
10976
|
});
|
|
10853
10977
|
//#endregion
|
|
10854
10978
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10855
|
-
const MESSAGE$
|
|
10979
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10856
10980
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10857
10981
|
"code",
|
|
10858
10982
|
"pre",
|
|
@@ -10888,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10888
11012
|
if (isInsideLiteralTextTag(node)) return;
|
|
10889
11013
|
context.report({
|
|
10890
11014
|
node,
|
|
10891
|
-
message: MESSAGE$
|
|
11015
|
+
message: MESSAGE$47
|
|
10892
11016
|
});
|
|
10893
11017
|
} })
|
|
10894
11018
|
});
|
|
@@ -10919,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10919
11043
|
};
|
|
10920
11044
|
//#endregion
|
|
10921
11045
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10922
|
-
const MESSAGE$
|
|
11046
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10923
11047
|
const CONTEXT_MODULES$1 = [
|
|
10924
11048
|
"react",
|
|
10925
11049
|
"use-context-selector",
|
|
@@ -11017,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11017
11141
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11018
11142
|
context.report({
|
|
11019
11143
|
node: attribute,
|
|
11020
|
-
message: MESSAGE$
|
|
11144
|
+
message: MESSAGE$46
|
|
11021
11145
|
});
|
|
11022
11146
|
}
|
|
11023
11147
|
}
|
|
@@ -11103,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11103
11227
|
};
|
|
11104
11228
|
//#endregion
|
|
11105
11229
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11106
|
-
const MESSAGE$
|
|
11230
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11107
11231
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11108
11232
|
"icon",
|
|
11109
11233
|
"Icon",
|
|
@@ -11372,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11372
11496
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11373
11497
|
context.report({
|
|
11374
11498
|
node,
|
|
11375
|
-
message: MESSAGE$
|
|
11499
|
+
message: MESSAGE$45
|
|
11376
11500
|
});
|
|
11377
11501
|
}
|
|
11378
11502
|
};
|
|
@@ -11660,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11660
11784
|
];
|
|
11661
11785
|
//#endregion
|
|
11662
11786
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11663
|
-
const MESSAGE$
|
|
11787
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11664
11788
|
const isDataArrayPropName = (propName) => {
|
|
11665
11789
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11666
11790
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11744,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11744
11868
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11745
11869
|
context.report({
|
|
11746
11870
|
node,
|
|
11747
|
-
message: MESSAGE$
|
|
11871
|
+
message: MESSAGE$44
|
|
11748
11872
|
});
|
|
11749
11873
|
}
|
|
11750
11874
|
};
|
|
@@ -12002,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12002
12126
|
]);
|
|
12003
12127
|
//#endregion
|
|
12004
12128
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12005
|
-
const MESSAGE$
|
|
12129
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12006
12130
|
const isAccessorPredicateName = (propName) => {
|
|
12007
12131
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12008
12132
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12208,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12208
12332
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12209
12333
|
context.report({
|
|
12210
12334
|
node,
|
|
12211
|
-
message: MESSAGE$
|
|
12335
|
+
message: MESSAGE$43
|
|
12212
12336
|
});
|
|
12213
12337
|
}
|
|
12214
12338
|
};
|
|
@@ -12428,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12428
12552
|
];
|
|
12429
12553
|
//#endregion
|
|
12430
12554
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12431
|
-
const MESSAGE$
|
|
12555
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12432
12556
|
const isConfigObjectPropName = (propName) => {
|
|
12433
12557
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12434
12558
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12516,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12516
12640
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12517
12641
|
context.report({
|
|
12518
12642
|
node,
|
|
12519
|
-
message: MESSAGE$
|
|
12643
|
+
message: MESSAGE$42
|
|
12520
12644
|
});
|
|
12521
12645
|
}
|
|
12522
12646
|
};
|
|
@@ -12524,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12524
12648
|
});
|
|
12525
12649
|
//#endregion
|
|
12526
12650
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12527
|
-
const MESSAGE$
|
|
12651
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12528
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;
|
|
12529
12653
|
const resolveSettings$28 = (settings) => {
|
|
12530
12654
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12565,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12565
12689
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12566
12690
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12567
12691
|
node: attribute,
|
|
12568
|
-
message: MESSAGE$
|
|
12692
|
+
message: MESSAGE$41
|
|
12569
12693
|
});
|
|
12570
12694
|
}
|
|
12571
12695
|
} };
|
|
@@ -12880,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12880
13004
|
});
|
|
12881
13005
|
//#endregion
|
|
12882
13006
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12883
|
-
const MESSAGE$
|
|
13007
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12884
13008
|
const resolveSettings$25 = (settings) => {
|
|
12885
13009
|
const reactDoctor = settings?.["react-doctor"];
|
|
12886
13010
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12921,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12921
13045
|
}
|
|
12922
13046
|
context.report({
|
|
12923
13047
|
node: attribute,
|
|
12924
|
-
message: MESSAGE$
|
|
13048
|
+
message: MESSAGE$40
|
|
12925
13049
|
});
|
|
12926
13050
|
}
|
|
12927
13051
|
} };
|
|
@@ -13149,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13149
13273
|
});
|
|
13150
13274
|
//#endregion
|
|
13151
13275
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13152
|
-
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`.";
|
|
13153
13277
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13154
13278
|
"aa",
|
|
13155
13279
|
"ab",
|
|
@@ -13361,7 +13485,7 @@ const lang = defineRule({
|
|
|
13361
13485
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13362
13486
|
context.report({
|
|
13363
13487
|
node: langAttr,
|
|
13364
|
-
message: MESSAGE$
|
|
13488
|
+
message: MESSAGE$39
|
|
13365
13489
|
});
|
|
13366
13490
|
return;
|
|
13367
13491
|
}
|
|
@@ -13370,7 +13494,7 @@ const lang = defineRule({
|
|
|
13370
13494
|
if (value === null) return;
|
|
13371
13495
|
if (!isValidLangTag(value)) context.report({
|
|
13372
13496
|
node: langAttr,
|
|
13373
|
-
message: MESSAGE$
|
|
13497
|
+
message: MESSAGE$39
|
|
13374
13498
|
});
|
|
13375
13499
|
} })
|
|
13376
13500
|
});
|
|
@@ -13396,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13396
13520
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13397
13521
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13398
13522
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13523
|
+
ignoreStringLiterals: true,
|
|
13399
13524
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13400
13525
|
})
|
|
13401
13526
|
});
|
|
@@ -13414,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13414
13539
|
});
|
|
13415
13540
|
//#endregion
|
|
13416
13541
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13417
|
-
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>`.";
|
|
13418
13543
|
const DEFAULT_AUDIO = ["audio"];
|
|
13419
13544
|
const DEFAULT_VIDEO = ["video"];
|
|
13420
13545
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13455,7 +13580,7 @@ const mediaHasCaption = defineRule({
|
|
|
13455
13580
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13456
13581
|
context.report({
|
|
13457
13582
|
node: node.name,
|
|
13458
|
-
message: MESSAGE$
|
|
13583
|
+
message: MESSAGE$38
|
|
13459
13584
|
});
|
|
13460
13585
|
return;
|
|
13461
13586
|
}
|
|
@@ -13472,7 +13597,7 @@ const mediaHasCaption = defineRule({
|
|
|
13472
13597
|
return kindValue.value.toLowerCase() === "captions";
|
|
13473
13598
|
})) context.report({
|
|
13474
13599
|
node: node.name,
|
|
13475
|
-
message: MESSAGE$
|
|
13600
|
+
message: MESSAGE$38
|
|
13476
13601
|
});
|
|
13477
13602
|
} };
|
|
13478
13603
|
}
|
|
@@ -15273,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15273
15398
|
});
|
|
15274
15399
|
//#endregion
|
|
15275
15400
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15276
|
-
const MESSAGE$
|
|
15401
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15277
15402
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15278
15403
|
const noAccessKey = defineRule({
|
|
15279
15404
|
id: "no-access-key",
|
|
@@ -15290,7 +15415,7 @@ const noAccessKey = defineRule({
|
|
|
15290
15415
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15291
15416
|
context.report({
|
|
15292
15417
|
node: accessKey,
|
|
15293
|
-
message: MESSAGE$
|
|
15418
|
+
message: MESSAGE$37
|
|
15294
15419
|
});
|
|
15295
15420
|
return;
|
|
15296
15421
|
}
|
|
@@ -15300,7 +15425,7 @@ const noAccessKey = defineRule({
|
|
|
15300
15425
|
if (isUndefinedIdentifier(expression)) return;
|
|
15301
15426
|
context.report({
|
|
15302
15427
|
node: accessKey,
|
|
15303
|
-
message: MESSAGE$
|
|
15428
|
+
message: MESSAGE$37
|
|
15304
15429
|
});
|
|
15305
15430
|
}
|
|
15306
15431
|
} })
|
|
@@ -15783,7 +15908,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15783
15908
|
});
|
|
15784
15909
|
//#endregion
|
|
15785
15910
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15786
|
-
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.";
|
|
15787
15912
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15788
15913
|
id: "no-aria-hidden-on-focusable",
|
|
15789
15914
|
title: "aria-hidden on focusable element",
|
|
@@ -15810,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15810
15935
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15811
15936
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15812
15937
|
node: ariaHidden,
|
|
15813
|
-
message: MESSAGE$
|
|
15938
|
+
message: MESSAGE$36
|
|
15814
15939
|
});
|
|
15815
15940
|
} })
|
|
15816
15941
|
});
|
|
@@ -16178,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16178
16303
|
});
|
|
16179
16304
|
//#endregion
|
|
16180
16305
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16181
|
-
const MESSAGE$
|
|
16306
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16182
16307
|
const SECOND_INDEX_METHODS = new Set([
|
|
16183
16308
|
"every",
|
|
16184
16309
|
"filter",
|
|
@@ -16382,7 +16507,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16382
16507
|
}
|
|
16383
16508
|
context.report({
|
|
16384
16509
|
node: keyAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16510
|
+
message: MESSAGE$35
|
|
16386
16511
|
});
|
|
16387
16512
|
},
|
|
16388
16513
|
CallExpression(node) {
|
|
@@ -16402,7 +16527,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16402
16527
|
if (propName !== "key") continue;
|
|
16403
16528
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16404
16529
|
node: property,
|
|
16405
|
-
message: MESSAGE$
|
|
16530
|
+
message: MESSAGE$35
|
|
16406
16531
|
});
|
|
16407
16532
|
}
|
|
16408
16533
|
}
|
|
@@ -16410,7 +16535,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16410
16535
|
});
|
|
16411
16536
|
//#endregion
|
|
16412
16537
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16413
|
-
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.";
|
|
16414
16539
|
const noAsyncEffectCallback = defineRule({
|
|
16415
16540
|
id: "no-async-effect-callback",
|
|
16416
16541
|
title: "Async effect callback",
|
|
@@ -16424,13 +16549,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16424
16549
|
if (!callback.async) return;
|
|
16425
16550
|
context.report({
|
|
16426
16551
|
node: callback,
|
|
16427
|
-
message: MESSAGE$
|
|
16552
|
+
message: MESSAGE$34
|
|
16428
16553
|
});
|
|
16429
16554
|
} })
|
|
16430
16555
|
});
|
|
16431
16556
|
//#endregion
|
|
16432
16557
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16433
|
-
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.";
|
|
16434
16559
|
const resolveSettings$21 = (settings) => {
|
|
16435
16560
|
const reactDoctor = settings?.["react-doctor"];
|
|
16436
16561
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16486,7 +16611,7 @@ const noAutofocus = defineRule({
|
|
|
16486
16611
|
}
|
|
16487
16612
|
context.report({
|
|
16488
16613
|
node: autoFocusAttribute,
|
|
16489
|
-
message: MESSAGE$
|
|
16614
|
+
message: MESSAGE$33
|
|
16490
16615
|
});
|
|
16491
16616
|
} };
|
|
16492
16617
|
}
|
|
@@ -16990,7 +17115,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16990
17115
|
});
|
|
16991
17116
|
//#endregion
|
|
16992
17117
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16993
|
-
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.";
|
|
16994
17119
|
const noChildrenProp = defineRule({
|
|
16995
17120
|
id: "no-children-prop",
|
|
16996
17121
|
title: "Children passed as a prop",
|
|
@@ -17002,7 +17127,7 @@ const noChildrenProp = defineRule({
|
|
|
17002
17127
|
if (node.name.name !== "children") return;
|
|
17003
17128
|
context.report({
|
|
17004
17129
|
node: node.name,
|
|
17005
|
-
message: MESSAGE$
|
|
17130
|
+
message: MESSAGE$32
|
|
17006
17131
|
});
|
|
17007
17132
|
},
|
|
17008
17133
|
CallExpression(node) {
|
|
@@ -17015,7 +17140,7 @@ const noChildrenProp = defineRule({
|
|
|
17015
17140
|
const propertyKey = property.key;
|
|
17016
17141
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17017
17142
|
node: propertyKey,
|
|
17018
|
-
message: MESSAGE$
|
|
17143
|
+
message: MESSAGE$32
|
|
17019
17144
|
});
|
|
17020
17145
|
}
|
|
17021
17146
|
}
|
|
@@ -17023,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
17023
17148
|
});
|
|
17024
17149
|
//#endregion
|
|
17025
17150
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17026
|
-
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.";
|
|
17027
17152
|
const noCloneElement = defineRule({
|
|
17028
17153
|
id: "no-clone-element",
|
|
17029
17154
|
title: "cloneElement makes child props fragile",
|
|
@@ -17036,7 +17161,7 @@ const noCloneElement = defineRule({
|
|
|
17036
17161
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17037
17162
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17038
17163
|
node: callee,
|
|
17039
|
-
message: MESSAGE$
|
|
17164
|
+
message: MESSAGE$31
|
|
17040
17165
|
});
|
|
17041
17166
|
return;
|
|
17042
17167
|
}
|
|
@@ -17049,7 +17174,7 @@ const noCloneElement = defineRule({
|
|
|
17049
17174
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17050
17175
|
context.report({
|
|
17051
17176
|
node: callee,
|
|
17052
|
-
message: MESSAGE$
|
|
17177
|
+
message: MESSAGE$31
|
|
17053
17178
|
});
|
|
17054
17179
|
}
|
|
17055
17180
|
} })
|
|
@@ -17098,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17098
17223
|
};
|
|
17099
17224
|
//#endregion
|
|
17100
17225
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17101
|
-
const MESSAGE$
|
|
17226
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17102
17227
|
const CONTEXT_MODULES = [
|
|
17103
17228
|
"react",
|
|
17104
17229
|
"use-context-selector",
|
|
@@ -17134,13 +17259,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17134
17259
|
if (!componentOrHookName) return;
|
|
17135
17260
|
context.report({
|
|
17136
17261
|
node,
|
|
17137
|
-
message: `${MESSAGE$
|
|
17262
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17138
17263
|
});
|
|
17139
17264
|
} })
|
|
17140
17265
|
});
|
|
17141
17266
|
//#endregion
|
|
17142
17267
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17143
|
-
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.";
|
|
17144
17269
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17145
17270
|
id: "no-create-ref-in-function-component",
|
|
17146
17271
|
title: "createRef in function component",
|
|
@@ -17159,7 +17284,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17159
17284
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17160
17285
|
context.report({
|
|
17161
17286
|
node,
|
|
17162
|
-
message: MESSAGE$
|
|
17287
|
+
message: MESSAGE$29
|
|
17163
17288
|
});
|
|
17164
17289
|
} })
|
|
17165
17290
|
});
|
|
@@ -17299,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17299
17424
|
});
|
|
17300
17425
|
//#endregion
|
|
17301
17426
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17302
|
-
const MESSAGE$
|
|
17427
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17303
17428
|
const noDanger = defineRule({
|
|
17304
17429
|
id: "no-danger",
|
|
17305
17430
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17312,7 +17437,7 @@ const noDanger = defineRule({
|
|
|
17312
17437
|
if (!propAttribute) return;
|
|
17313
17438
|
context.report({
|
|
17314
17439
|
node: propAttribute.name,
|
|
17315
|
-
message: MESSAGE$
|
|
17440
|
+
message: MESSAGE$28
|
|
17316
17441
|
});
|
|
17317
17442
|
},
|
|
17318
17443
|
CallExpression(node) {
|
|
@@ -17324,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17324
17449
|
const propertyKey = property.key;
|
|
17325
17450
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17326
17451
|
node: propertyKey,
|
|
17327
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$28
|
|
17328
17453
|
});
|
|
17329
17454
|
}
|
|
17330
17455
|
}
|
|
@@ -17332,7 +17457,7 @@ const noDanger = defineRule({
|
|
|
17332
17457
|
});
|
|
17333
17458
|
//#endregion
|
|
17334
17459
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17335
|
-
const MESSAGE$
|
|
17460
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17336
17461
|
const isLineBreak = (child) => {
|
|
17337
17462
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17338
17463
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17402,7 +17527,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17402
17527
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17403
17528
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17404
17529
|
node: opening,
|
|
17405
|
-
message: MESSAGE$
|
|
17530
|
+
message: MESSAGE$27
|
|
17406
17531
|
});
|
|
17407
17532
|
},
|
|
17408
17533
|
CallExpression(node) {
|
|
@@ -17414,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17414
17539
|
if (!propsShape.hasDangerously) return;
|
|
17415
17540
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17416
17541
|
node,
|
|
17417
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$27
|
|
17418
17543
|
});
|
|
17419
17544
|
}
|
|
17420
17545
|
})
|
|
@@ -17991,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17991
18116
|
//#endregion
|
|
17992
18117
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17993
18118
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17994
|
-
const MESSAGE$
|
|
18119
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17995
18120
|
const resolveSettings$20 = (settings) => {
|
|
17996
18121
|
const reactDoctor = settings?.["react-doctor"];
|
|
17997
18122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18010,7 +18135,7 @@ const noDidMountSetState = defineRule({
|
|
|
18010
18135
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18011
18136
|
context.report({
|
|
18012
18137
|
node: node.callee,
|
|
18013
|
-
message: MESSAGE$
|
|
18138
|
+
message: MESSAGE$26
|
|
18014
18139
|
});
|
|
18015
18140
|
} };
|
|
18016
18141
|
}
|
|
@@ -18018,7 +18143,7 @@ const noDidMountSetState = defineRule({
|
|
|
18018
18143
|
//#endregion
|
|
18019
18144
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18020
18145
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18021
|
-
const MESSAGE$
|
|
18146
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18022
18147
|
const resolveSettings$19 = (settings) => {
|
|
18023
18148
|
const reactDoctor = settings?.["react-doctor"];
|
|
18024
18149
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18037,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18037
18162
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18038
18163
|
context.report({
|
|
18039
18164
|
node: node.callee,
|
|
18040
|
-
message: MESSAGE$
|
|
18165
|
+
message: MESSAGE$25
|
|
18041
18166
|
});
|
|
18042
18167
|
} };
|
|
18043
18168
|
}
|
|
@@ -18060,7 +18185,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18060
18185
|
};
|
|
18061
18186
|
//#endregion
|
|
18062
18187
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18063
|
-
const MESSAGE$
|
|
18188
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18064
18189
|
const shouldIgnoreMutation = (node) => {
|
|
18065
18190
|
let isConstructor = false;
|
|
18066
18191
|
let isInsideCallExpression = false;
|
|
@@ -18082,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18082
18207
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18083
18208
|
context.report({
|
|
18084
18209
|
node: reportNode,
|
|
18085
|
-
message: MESSAGE$
|
|
18210
|
+
message: MESSAGE$24
|
|
18086
18211
|
});
|
|
18087
18212
|
};
|
|
18088
18213
|
const noDirectMutationState = defineRule({
|
|
@@ -18292,6 +18417,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18292
18417
|
} })
|
|
18293
18418
|
});
|
|
18294
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
|
|
18295
18440
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18296
18441
|
const noDynamicImportPath = defineRule({
|
|
18297
18442
|
id: "no-dynamic-import-path",
|
|
@@ -19670,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19670
19815
|
"ReactDOM",
|
|
19671
19816
|
"ReactDom"
|
|
19672
19817
|
]);
|
|
19673
|
-
const MESSAGE$
|
|
19818
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19674
19819
|
const noFindDomNode = defineRule({
|
|
19675
19820
|
id: "no-find-dom-node",
|
|
19676
19821
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19681,7 +19826,7 @@ const noFindDomNode = defineRule({
|
|
|
19681
19826
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19682
19827
|
context.report({
|
|
19683
19828
|
node: callee,
|
|
19684
|
-
message: MESSAGE$
|
|
19829
|
+
message: MESSAGE$22
|
|
19685
19830
|
});
|
|
19686
19831
|
return;
|
|
19687
19832
|
}
|
|
@@ -19692,7 +19837,7 @@ const noFindDomNode = defineRule({
|
|
|
19692
19837
|
if (callee.property.name !== "findDOMNode") return;
|
|
19693
19838
|
context.report({
|
|
19694
19839
|
node: callee.property,
|
|
19695
|
-
message: MESSAGE$
|
|
19840
|
+
message: MESSAGE$22
|
|
19696
19841
|
});
|
|
19697
19842
|
}
|
|
19698
19843
|
} })
|
|
@@ -19934,7 +20079,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19934
20079
|
});
|
|
19935
20080
|
//#endregion
|
|
19936
20081
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
19937
|
-
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.";
|
|
19938
20083
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
19939
20084
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
19940
20085
|
title: "Lazy image with high fetchPriority",
|
|
@@ -19948,7 +20093,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
19948
20093
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
19949
20094
|
context.report({
|
|
19950
20095
|
node: node.name,
|
|
19951
|
-
message: MESSAGE$
|
|
20096
|
+
message: MESSAGE$21
|
|
19952
20097
|
});
|
|
19953
20098
|
} })
|
|
19954
20099
|
});
|
|
@@ -20045,15 +20190,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
20045
20190
|
severity: "warn",
|
|
20046
20191
|
tags: ["test-noise", "react-jsx-only"],
|
|
20047
20192
|
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.",
|
|
20048
|
-
create: (context) =>
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20193
|
+
create: (context) => {
|
|
20194
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20195
|
+
return { JSXAttribute(node) {
|
|
20196
|
+
const expression = getInlineStyleExpression(node);
|
|
20197
|
+
if (!expression) return;
|
|
20198
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20199
|
+
if (propertyCount < 8) return;
|
|
20200
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20201
|
+
context.report({
|
|
20202
|
+
node: expression,
|
|
20203
|
+
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.`
|
|
20204
|
+
});
|
|
20205
|
+
} };
|
|
20206
|
+
}
|
|
20057
20207
|
});
|
|
20058
20208
|
//#endregion
|
|
20059
20209
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -20183,7 +20333,7 @@ const noIsMounted = defineRule({
|
|
|
20183
20333
|
});
|
|
20184
20334
|
//#endregion
|
|
20185
20335
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20186
|
-
const MESSAGE$
|
|
20336
|
+
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)`.";
|
|
20187
20337
|
const isJsonMethodCall = (node, method) => {
|
|
20188
20338
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20189
20339
|
const callee = node.callee;
|
|
@@ -20200,13 +20350,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20200
20350
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20201
20351
|
context.report({
|
|
20202
20352
|
node,
|
|
20203
|
-
message: MESSAGE$
|
|
20353
|
+
message: MESSAGE$20
|
|
20204
20354
|
});
|
|
20205
20355
|
} })
|
|
20206
20356
|
});
|
|
20207
20357
|
//#endregion
|
|
20208
20358
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20209
|
-
const MESSAGE$
|
|
20359
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20210
20360
|
const isJsxElementTypeReference = (node) => {
|
|
20211
20361
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20212
20362
|
const typeName = node.typeName;
|
|
@@ -20223,7 +20373,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20223
20373
|
if (!typeAnnotation) return;
|
|
20224
20374
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20225
20375
|
node: typeAnnotation,
|
|
20226
|
-
message: MESSAGE$
|
|
20376
|
+
message: MESSAGE$19
|
|
20227
20377
|
});
|
|
20228
20378
|
};
|
|
20229
20379
|
const noJsxElementType = defineRule({
|
|
@@ -20678,7 +20828,7 @@ const noMoment = defineRule({
|
|
|
20678
20828
|
});
|
|
20679
20829
|
//#endregion
|
|
20680
20830
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20681
|
-
const MESSAGE$
|
|
20831
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20682
20832
|
const resolveSettings$16 = (settings) => {
|
|
20683
20833
|
const reactDoctor = settings?.["react-doctor"];
|
|
20684
20834
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21000,7 +21150,7 @@ const noMultiComp = defineRule({
|
|
|
21000
21150
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21001
21151
|
for (const component of flagged.slice(1)) context.report({
|
|
21002
21152
|
node: component.reportNode,
|
|
21003
|
-
message: MESSAGE$
|
|
21153
|
+
message: MESSAGE$18
|
|
21004
21154
|
});
|
|
21005
21155
|
} };
|
|
21006
21156
|
}
|
|
@@ -21168,7 +21318,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21168
21318
|
};
|
|
21169
21319
|
//#endregion
|
|
21170
21320
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21171
|
-
const MESSAGE$
|
|
21321
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21172
21322
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21173
21323
|
"copyWithin",
|
|
21174
21324
|
"fill",
|
|
@@ -21378,7 +21528,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21378
21528
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21379
21529
|
context.report({
|
|
21380
21530
|
node: options.crossFileConsumerCallSite,
|
|
21381
|
-
message: `${MESSAGE$
|
|
21531
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21382
21532
|
});
|
|
21383
21533
|
return;
|
|
21384
21534
|
}
|
|
@@ -21387,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21387
21537
|
reportedNodes.add(mutation.node);
|
|
21388
21538
|
context.report({
|
|
21389
21539
|
node: mutation.node,
|
|
21390
|
-
message: MESSAGE$
|
|
21540
|
+
message: MESSAGE$17
|
|
21391
21541
|
});
|
|
21392
21542
|
}
|
|
21393
21543
|
};
|
|
@@ -21659,7 +21809,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21659
21809
|
});
|
|
21660
21810
|
//#endregion
|
|
21661
21811
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21662
|
-
const MESSAGE$
|
|
21812
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21663
21813
|
const resolveSettings$14 = (settings) => {
|
|
21664
21814
|
const reactDoctor = settings?.["react-doctor"];
|
|
21665
21815
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21687,7 +21837,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21687
21837
|
if (numeric === null) {
|
|
21688
21838
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21689
21839
|
node: tabIndex,
|
|
21690
|
-
message: MESSAGE$
|
|
21840
|
+
message: MESSAGE$16
|
|
21691
21841
|
});
|
|
21692
21842
|
return;
|
|
21693
21843
|
}
|
|
@@ -21700,7 +21850,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21700
21850
|
if (!roleAttribute) {
|
|
21701
21851
|
context.report({
|
|
21702
21852
|
node: tabIndex,
|
|
21703
|
-
message: MESSAGE$
|
|
21853
|
+
message: MESSAGE$16
|
|
21704
21854
|
});
|
|
21705
21855
|
return;
|
|
21706
21856
|
}
|
|
@@ -21714,7 +21864,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21714
21864
|
}
|
|
21715
21865
|
context.report({
|
|
21716
21866
|
node: tabIndex,
|
|
21717
|
-
message: MESSAGE$
|
|
21867
|
+
message: MESSAGE$16
|
|
21718
21868
|
});
|
|
21719
21869
|
} };
|
|
21720
21870
|
}
|
|
@@ -22405,7 +22555,7 @@ const noRandomKey = defineRule({
|
|
|
22405
22555
|
});
|
|
22406
22556
|
//#endregion
|
|
22407
22557
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22408
|
-
const MESSAGE$
|
|
22558
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22409
22559
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22410
22560
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22411
22561
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22431,13 +22581,13 @@ const noReactChildren = defineRule({
|
|
|
22431
22581
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22432
22582
|
context.report({
|
|
22433
22583
|
node: calleeOuter,
|
|
22434
|
-
message: MESSAGE$
|
|
22584
|
+
message: MESSAGE$15
|
|
22435
22585
|
});
|
|
22436
22586
|
return;
|
|
22437
22587
|
}
|
|
22438
22588
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22439
22589
|
node: calleeOuter,
|
|
22440
|
-
message: MESSAGE$
|
|
22590
|
+
message: MESSAGE$15
|
|
22441
22591
|
});
|
|
22442
22592
|
} })
|
|
22443
22593
|
});
|
|
@@ -22760,7 +22910,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22760
22910
|
});
|
|
22761
22911
|
//#endregion
|
|
22762
22912
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22763
|
-
const MESSAGE$
|
|
22913
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22764
22914
|
const isReactDomRenderCall = (node) => {
|
|
22765
22915
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22766
22916
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22784,7 +22934,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22784
22934
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22785
22935
|
context.report({
|
|
22786
22936
|
node: node.callee,
|
|
22787
|
-
message: MESSAGE$
|
|
22937
|
+
message: MESSAGE$14
|
|
22788
22938
|
});
|
|
22789
22939
|
} })
|
|
22790
22940
|
});
|
|
@@ -22944,11 +23094,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22944
23094
|
return "unknown";
|
|
22945
23095
|
};
|
|
22946
23096
|
//#endregion
|
|
22947
|
-
//#region src/plugin/utils/
|
|
22948
|
-
const
|
|
22949
|
-
|
|
23097
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23098
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23099
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23100
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23101
|
+
if (!words) return [];
|
|
23102
|
+
return words.map((word) => word.toLowerCase());
|
|
22950
23103
|
};
|
|
22951
23104
|
//#endregion
|
|
23105
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23106
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23107
|
+
//#endregion
|
|
22952
23108
|
//#region src/plugin/constants/tanstack.ts
|
|
22953
23109
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22954
23110
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23476,7 +23632,7 @@ const getParentComponent = (node) => {
|
|
|
23476
23632
|
};
|
|
23477
23633
|
//#endregion
|
|
23478
23634
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23479
|
-
const MESSAGE$
|
|
23635
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23480
23636
|
const noSetState = defineRule({
|
|
23481
23637
|
id: "no-set-state",
|
|
23482
23638
|
title: "Local class state forbidden",
|
|
@@ -23491,7 +23647,7 @@ const noSetState = defineRule({
|
|
|
23491
23647
|
if (!getParentComponent(node)) return;
|
|
23492
23648
|
context.report({
|
|
23493
23649
|
node: node.callee,
|
|
23494
|
-
message: MESSAGE$
|
|
23650
|
+
message: MESSAGE$13
|
|
23495
23651
|
});
|
|
23496
23652
|
} })
|
|
23497
23653
|
});
|
|
@@ -23653,7 +23809,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23653
23809
|
};
|
|
23654
23810
|
//#endregion
|
|
23655
23811
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23656
|
-
const MESSAGE$
|
|
23812
|
+
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.";
|
|
23657
23813
|
const DEFAULT_HANDLERS = [
|
|
23658
23814
|
"onClick",
|
|
23659
23815
|
"onMouseDown",
|
|
@@ -23713,7 +23869,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23713
23869
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23714
23870
|
context.report({
|
|
23715
23871
|
node: node.name,
|
|
23716
|
-
message: MESSAGE$
|
|
23872
|
+
message: MESSAGE$12
|
|
23717
23873
|
});
|
|
23718
23874
|
return;
|
|
23719
23875
|
}
|
|
@@ -23723,19 +23879,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23723
23879
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23724
23880
|
context.report({
|
|
23725
23881
|
node: node.name,
|
|
23726
|
-
message: MESSAGE$
|
|
23882
|
+
message: MESSAGE$12
|
|
23727
23883
|
});
|
|
23728
23884
|
return;
|
|
23729
23885
|
}
|
|
23730
23886
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23731
23887
|
context.report({
|
|
23732
23888
|
node: node.name,
|
|
23733
|
-
message: MESSAGE$
|
|
23889
|
+
message: MESSAGE$12
|
|
23734
23890
|
});
|
|
23735
23891
|
} };
|
|
23736
23892
|
}
|
|
23737
23893
|
});
|
|
23738
23894
|
//#endregion
|
|
23895
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
23896
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
23897
|
+
"disabled",
|
|
23898
|
+
"checked",
|
|
23899
|
+
"readonly",
|
|
23900
|
+
"required",
|
|
23901
|
+
"selected",
|
|
23902
|
+
"multiple",
|
|
23903
|
+
"autofocus",
|
|
23904
|
+
"autoplay",
|
|
23905
|
+
"controls",
|
|
23906
|
+
"loop",
|
|
23907
|
+
"muted",
|
|
23908
|
+
"open",
|
|
23909
|
+
"reversed",
|
|
23910
|
+
"default",
|
|
23911
|
+
"novalidate",
|
|
23912
|
+
"formnovalidate",
|
|
23913
|
+
"playsinline",
|
|
23914
|
+
"itemscope",
|
|
23915
|
+
"allowfullscreen"
|
|
23916
|
+
]);
|
|
23917
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
23918
|
+
id: "no-string-false-on-boolean-attribute",
|
|
23919
|
+
title: "String true/false on a boolean attribute",
|
|
23920
|
+
severity: "warn",
|
|
23921
|
+
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.",
|
|
23922
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23923
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23924
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
23925
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
23926
|
+
for (const attribute of node.attributes) {
|
|
23927
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23928
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23929
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
23930
|
+
const value = getJsxPropStringValue(attribute);
|
|
23931
|
+
if (value !== "false" && value !== "true") continue;
|
|
23932
|
+
const attributeName = attribute.name.name;
|
|
23933
|
+
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}\``;
|
|
23934
|
+
context.report({
|
|
23935
|
+
node: attribute,
|
|
23936
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
23937
|
+
});
|
|
23938
|
+
}
|
|
23939
|
+
} })
|
|
23940
|
+
});
|
|
23941
|
+
//#endregion
|
|
23739
23942
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23740
23943
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23741
23944
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23786,6 +23989,27 @@ const noStringRefs = defineRule({
|
|
|
23786
23989
|
}
|
|
23787
23990
|
});
|
|
23788
23991
|
//#endregion
|
|
23992
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
23993
|
+
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)`).";
|
|
23994
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
23995
|
+
const noSyncXhr = defineRule({
|
|
23996
|
+
id: "no-sync-xhr",
|
|
23997
|
+
title: "Synchronous XMLHttpRequest",
|
|
23998
|
+
severity: "warn",
|
|
23999
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24000
|
+
create: (context) => ({ CallExpression(node) {
|
|
24001
|
+
const callee = node.callee;
|
|
24002
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24003
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24004
|
+
const asyncArgument = node.arguments?.[2];
|
|
24005
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24006
|
+
context.report({
|
|
24007
|
+
node,
|
|
24008
|
+
message: MESSAGE$11
|
|
24009
|
+
});
|
|
24010
|
+
} })
|
|
24011
|
+
});
|
|
24012
|
+
//#endregion
|
|
23789
24013
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23790
24014
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23791
24015
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -34895,6 +35119,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34895
35119
|
}
|
|
34896
35120
|
});
|
|
34897
35121
|
//#endregion
|
|
35122
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
35123
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
35124
|
+
"signed",
|
|
35125
|
+
"logged",
|
|
35126
|
+
"sign"
|
|
35127
|
+
]);
|
|
35128
|
+
const mergeSignedInTokens = (tokens) => {
|
|
35129
|
+
const mergedTokens = [];
|
|
35130
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
35131
|
+
const currentToken = tokens[tokenIndex];
|
|
35132
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
35133
|
+
mergedTokens.push(`${currentToken}in`);
|
|
35134
|
+
tokenIndex += 1;
|
|
35135
|
+
continue;
|
|
35136
|
+
}
|
|
35137
|
+
mergedTokens.push(currentToken);
|
|
35138
|
+
}
|
|
35139
|
+
return mergedTokens;
|
|
35140
|
+
};
|
|
35141
|
+
const isAuthGuardName = (calleeName) => {
|
|
35142
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
35143
|
+
if (tokens.length === 0) return false;
|
|
35144
|
+
let hasAssertiveVerb = false;
|
|
35145
|
+
let hasGetterVerb = false;
|
|
35146
|
+
let hasQualifier = false;
|
|
35147
|
+
let hasStrongNoun = false;
|
|
35148
|
+
let hasWeakNoun = false;
|
|
35149
|
+
for (const token of tokens) {
|
|
35150
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35151
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35152
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35153
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35154
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35155
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35156
|
+
}
|
|
35157
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35158
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35159
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35160
|
+
return false;
|
|
35161
|
+
};
|
|
35162
|
+
//#endregion
|
|
34898
35163
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34899
35164
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34900
35165
|
if (!node) return false;
|
|
@@ -34937,9 +35202,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34937
35202
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34938
35203
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34939
35204
|
if (!calleeNode) return null;
|
|
34940
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35205
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35206
|
+
const calleeName = calleeNode.name;
|
|
35207
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35208
|
+
}
|
|
34941
35209
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34942
35210
|
const methodName = calleeNode.property.name;
|
|
35211
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34943
35212
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34944
35213
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34945
35214
|
return methodName;
|
|
@@ -35316,13 +35585,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35316
35585
|
const collectDeclaredNames = (declaration) => {
|
|
35317
35586
|
const names = /* @__PURE__ */ new Set();
|
|
35318
35587
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35319
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35320
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35321
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35322
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35323
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35324
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35325
|
-
}
|
|
35588
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35326
35589
|
return names;
|
|
35327
35590
|
};
|
|
35328
35591
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35332,11 +35595,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35332
35595
|
};
|
|
35333
35596
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35334
35597
|
if (names.size === 0) return false;
|
|
35598
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35335
35599
|
let didRead = false;
|
|
35336
|
-
|
|
35337
|
-
if (
|
|
35338
|
-
|
|
35339
|
-
|
|
35600
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35601
|
+
if (!declarator.init) continue;
|
|
35602
|
+
walkAst(declarator.init, (child) => {
|
|
35603
|
+
if (didRead) return;
|
|
35604
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35605
|
+
});
|
|
35606
|
+
}
|
|
35340
35607
|
return didRead;
|
|
35341
35608
|
};
|
|
35342
35609
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36596,7 +36863,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36596
36863
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36597
36864
|
scan: scanByPattern({
|
|
36598
36865
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36599
|
-
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,
|
|
36866
|
+
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,
|
|
36600
36867
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36601
36868
|
})
|
|
36602
36869
|
});
|
|
@@ -39052,6 +39319,17 @@ const reactDoctorRules = [
|
|
|
39052
39319
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39053
39320
|
}
|
|
39054
39321
|
},
|
|
39322
|
+
{
|
|
39323
|
+
key: "react-doctor/no-document-write",
|
|
39324
|
+
id: "no-document-write",
|
|
39325
|
+
source: "react-doctor",
|
|
39326
|
+
originallyExternal: false,
|
|
39327
|
+
rule: {
|
|
39328
|
+
...noDocumentWrite,
|
|
39329
|
+
framework: "global",
|
|
39330
|
+
category: "Performance"
|
|
39331
|
+
}
|
|
39332
|
+
},
|
|
39055
39333
|
{
|
|
39056
39334
|
key: "react-doctor/no-dynamic-import-path",
|
|
39057
39335
|
id: "no-dynamic-import-path",
|
|
@@ -39861,6 +40139,18 @@ const reactDoctorRules = [
|
|
|
39861
40139
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39862
40140
|
}
|
|
39863
40141
|
},
|
|
40142
|
+
{
|
|
40143
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40144
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40145
|
+
source: "react-doctor",
|
|
40146
|
+
originallyExternal: false,
|
|
40147
|
+
rule: {
|
|
40148
|
+
...noStringFalseOnBooleanAttribute,
|
|
40149
|
+
framework: "global",
|
|
40150
|
+
category: "Bugs",
|
|
40151
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40152
|
+
}
|
|
40153
|
+
},
|
|
39864
40154
|
{
|
|
39865
40155
|
key: "react-doctor/no-string-refs",
|
|
39866
40156
|
id: "no-string-refs",
|
|
@@ -39873,6 +40163,17 @@ const reactDoctorRules = [
|
|
|
39873
40163
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39874
40164
|
}
|
|
39875
40165
|
},
|
|
40166
|
+
{
|
|
40167
|
+
key: "react-doctor/no-sync-xhr",
|
|
40168
|
+
id: "no-sync-xhr",
|
|
40169
|
+
source: "react-doctor",
|
|
40170
|
+
originallyExternal: false,
|
|
40171
|
+
rule: {
|
|
40172
|
+
...noSyncXhr,
|
|
40173
|
+
framework: "global",
|
|
40174
|
+
category: "Performance"
|
|
40175
|
+
}
|
|
40176
|
+
},
|
|
39876
40177
|
{
|
|
39877
40178
|
key: "react-doctor/no-this-in-sfc",
|
|
39878
40179
|
id: "no-this-in-sfc",
|