oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.1e260c5
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 +420 -0
- package/dist/index.js +781 -189
- 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 = [
|
|
@@ -4201,6 +4325,58 @@ const asyncParallel = defineRule({
|
|
|
4201
4325
|
}
|
|
4202
4326
|
});
|
|
4203
4327
|
//#endregion
|
|
4328
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
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.";
|
|
4330
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4331
|
+
const STORAGE_GLOBALS = new Set([
|
|
4332
|
+
"window",
|
|
4333
|
+
"globalThis",
|
|
4334
|
+
"self"
|
|
4335
|
+
]);
|
|
4336
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4337
|
+
const isWebStorageObject = (node) => {
|
|
4338
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4339
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4340
|
+
return false;
|
|
4341
|
+
};
|
|
4342
|
+
const staticMemberName = (member) => {
|
|
4343
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4344
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4345
|
+
return null;
|
|
4346
|
+
};
|
|
4347
|
+
const authTokenInWebStorage = defineRule({
|
|
4348
|
+
id: "auth-token-in-web-storage",
|
|
4349
|
+
title: "Auth token in web storage",
|
|
4350
|
+
severity: "warn",
|
|
4351
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4352
|
+
create: (context) => ({
|
|
4353
|
+
CallExpression(node) {
|
|
4354
|
+
const callee = node.callee;
|
|
4355
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4356
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4357
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4358
|
+
const keyArgument = node.arguments?.[0];
|
|
4359
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4360
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4361
|
+
context.report({
|
|
4362
|
+
node,
|
|
4363
|
+
message: MESSAGE$57
|
|
4364
|
+
});
|
|
4365
|
+
},
|
|
4366
|
+
AssignmentExpression(node) {
|
|
4367
|
+
const target = node.left;
|
|
4368
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4369
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4370
|
+
const propertyName = staticMemberName(target);
|
|
4371
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4372
|
+
context.report({
|
|
4373
|
+
node: target,
|
|
4374
|
+
message: MESSAGE$57
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
})
|
|
4378
|
+
});
|
|
4379
|
+
//#endregion
|
|
4204
4380
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4205
4381
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4206
4382
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4572,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4572
4748
|
//#endregion
|
|
4573
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4575
|
-
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`.";
|
|
4576
4752
|
const KEY_HANDLERS = [
|
|
4577
4753
|
"onKeyUp",
|
|
4578
4754
|
"onKeyDown",
|
|
@@ -4604,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4604
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4781
|
context.report({
|
|
4606
4782
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4608
4784
|
});
|
|
4609
4785
|
} };
|
|
4610
4786
|
}
|
|
@@ -4719,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4895
|
};
|
|
4720
4896
|
//#endregion
|
|
4721
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
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`.";
|
|
4723
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4901
|
"alt",
|
|
@@ -4880,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
5057
|
context.report({
|
|
4882
5058
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
4884
5060
|
});
|
|
4885
5061
|
} };
|
|
4886
5062
|
}
|
|
@@ -5306,6 +5482,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5482
|
} })
|
|
5307
5483
|
});
|
|
5308
5484
|
//#endregion
|
|
5485
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5486
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5487
|
+
//#endregion
|
|
5488
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
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.";
|
|
5490
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5491
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5492
|
+
"aria-label",
|
|
5493
|
+
"aria-labelledby",
|
|
5494
|
+
"title"
|
|
5495
|
+
];
|
|
5496
|
+
const dialogHasAccessibleName = defineRule({
|
|
5497
|
+
id: "dialog-has-accessible-name",
|
|
5498
|
+
title: "Dialog without accessible name",
|
|
5499
|
+
severity: "warn",
|
|
5500
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5501
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5502
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5503
|
+
const tagName = node.name.name;
|
|
5504
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5505
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5506
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5507
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5508
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5509
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5510
|
+
context.report({
|
|
5511
|
+
node: node.name,
|
|
5512
|
+
message: MESSAGE$54
|
|
5513
|
+
});
|
|
5514
|
+
} })
|
|
5515
|
+
});
|
|
5516
|
+
//#endregion
|
|
5309
5517
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5518
|
const PRAGMA$2 = "React";
|
|
5311
5519
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5548
|
};
|
|
5341
5549
|
//#endregion
|
|
5342
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5553
|
"observer",
|
|
5346
5554
|
"lazy",
|
|
@@ -5543,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5543
5751
|
const reportAt = (node) => {
|
|
5544
5752
|
context.report({
|
|
5545
5753
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5547
5755
|
});
|
|
5548
5756
|
};
|
|
5549
5757
|
return {
|
|
@@ -7691,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7691
7899
|
});
|
|
7692
7900
|
//#endregion
|
|
7693
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7903
|
const forwardRefUsesRef = defineRule({
|
|
7696
7904
|
id: "forward-ref-uses-ref",
|
|
7697
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7920
|
context.report({
|
|
7713
7921
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7715
7923
|
});
|
|
7716
7924
|
} })
|
|
7717
7925
|
});
|
|
@@ -7748,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7956
|
});
|
|
7749
7957
|
//#endregion
|
|
7750
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
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`.";
|
|
7752
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7961
|
"h1",
|
|
7754
7962
|
"h2",
|
|
@@ -7781,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7990
|
context.report({
|
|
7783
7991
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7785
7993
|
});
|
|
7786
7994
|
} };
|
|
7787
7995
|
}
|
|
@@ -7919,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8127
|
});
|
|
7920
8128
|
//#endregion
|
|
7921
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
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`.";
|
|
7923
8131
|
const resolveSettings$38 = (settings) => {
|
|
7924
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7967,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
7967
8175
|
if (!lang) {
|
|
7968
8176
|
context.report({
|
|
7969
8177
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
7971
8179
|
});
|
|
7972
8180
|
return;
|
|
7973
8181
|
}
|
|
@@ -7975,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
7975
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
7976
8184
|
context.report({
|
|
7977
8185
|
node: lang,
|
|
7978
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
7979
8187
|
});
|
|
7980
8188
|
return;
|
|
7981
8189
|
}
|
|
7982
8190
|
if (hasSpread && !lang) context.report({
|
|
7983
8191
|
node: node.name,
|
|
7984
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
7985
8193
|
});
|
|
7986
8194
|
} };
|
|
7987
8195
|
}
|
|
@@ -8195,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8403
|
});
|
|
8196
8404
|
//#endregion
|
|
8197
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
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.";
|
|
8199
8407
|
const evaluateTitleValue = (value) => {
|
|
8200
8408
|
if (!value) return "missing";
|
|
8201
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8443
|
if (!titleAttr) {
|
|
8236
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8445
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8239
8447
|
});
|
|
8240
8448
|
return;
|
|
8241
8449
|
}
|
|
8242
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8452
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8246
8454
|
});
|
|
8247
8455
|
} })
|
|
8248
8456
|
});
|
|
@@ -8346,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8554
|
});
|
|
8347
8555
|
//#endregion
|
|
8348
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
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.";
|
|
8350
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8560
|
"image",
|
|
@@ -8411,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8619
|
if (!altAttribute) return;
|
|
8412
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8621
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8415
8623
|
});
|
|
8416
8624
|
} };
|
|
8417
8625
|
}
|
|
@@ -10768,7 +10976,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10976
|
});
|
|
10769
10977
|
//#endregion
|
|
10770
10978
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10979
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10980
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10981
|
"code",
|
|
10774
10982
|
"pre",
|
|
@@ -10804,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
11012
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
11013
|
context.report({
|
|
10806
11014
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
11015
|
+
message: MESSAGE$47
|
|
10808
11016
|
});
|
|
10809
11017
|
} })
|
|
10810
11018
|
});
|
|
@@ -10835,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
11043
|
};
|
|
10836
11044
|
//#endregion
|
|
10837
11045
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
11046
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
11047
|
const CONTEXT_MODULES$1 = [
|
|
10840
11048
|
"react",
|
|
10841
11049
|
"use-context-selector",
|
|
@@ -10933,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11141
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11142
|
context.report({
|
|
10935
11143
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11144
|
+
message: MESSAGE$46
|
|
10937
11145
|
});
|
|
10938
11146
|
}
|
|
10939
11147
|
}
|
|
@@ -11019,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11227
|
};
|
|
11020
11228
|
//#endregion
|
|
11021
11229
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11230
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11231
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11232
|
"icon",
|
|
11025
11233
|
"Icon",
|
|
@@ -11288,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11496
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11497
|
context.report({
|
|
11290
11498
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11499
|
+
message: MESSAGE$45
|
|
11292
11500
|
});
|
|
11293
11501
|
}
|
|
11294
11502
|
};
|
|
@@ -11576,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11784
|
];
|
|
11577
11785
|
//#endregion
|
|
11578
11786
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11787
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11788
|
const isDataArrayPropName = (propName) => {
|
|
11581
11789
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11790
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11868
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11869
|
context.report({
|
|
11662
11870
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11871
|
+
message: MESSAGE$44
|
|
11664
11872
|
});
|
|
11665
11873
|
}
|
|
11666
11874
|
};
|
|
@@ -11918,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12126
|
]);
|
|
11919
12127
|
//#endregion
|
|
11920
12128
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12129
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12130
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12131
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12132
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12332
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12333
|
context.report({
|
|
12126
12334
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12335
|
+
message: MESSAGE$43
|
|
12128
12336
|
});
|
|
12129
12337
|
}
|
|
12130
12338
|
};
|
|
@@ -12344,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12552
|
];
|
|
12345
12553
|
//#endregion
|
|
12346
12554
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12555
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12556
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12557
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12558
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12640
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12641
|
context.report({
|
|
12434
12642
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12643
|
+
message: MESSAGE$42
|
|
12436
12644
|
});
|
|
12437
12645
|
}
|
|
12438
12646
|
};
|
|
@@ -12440,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12648
|
});
|
|
12441
12649
|
//#endregion
|
|
12442
12650
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12651
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
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;
|
|
12445
12653
|
const resolveSettings$28 = (settings) => {
|
|
12446
12654
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12689
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12690
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12691
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12692
|
+
message: MESSAGE$41
|
|
12485
12693
|
});
|
|
12486
12694
|
}
|
|
12487
12695
|
} };
|
|
@@ -12796,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
13004
|
});
|
|
12797
13005
|
//#endregion
|
|
12798
13006
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
13007
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12800
13008
|
const resolveSettings$25 = (settings) => {
|
|
12801
13009
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
13010
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
13045
|
}
|
|
12838
13046
|
context.report({
|
|
12839
13047
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
13048
|
+
message: MESSAGE$40
|
|
12841
13049
|
});
|
|
12842
13050
|
}
|
|
12843
13051
|
} };
|
|
@@ -13065,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13273
|
});
|
|
13066
13274
|
//#endregion
|
|
13067
13275
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
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`.";
|
|
13069
13277
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13278
|
"aa",
|
|
13071
13279
|
"ab",
|
|
@@ -13277,7 +13485,7 @@ const lang = defineRule({
|
|
|
13277
13485
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13486
|
context.report({
|
|
13279
13487
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13488
|
+
message: MESSAGE$39
|
|
13281
13489
|
});
|
|
13282
13490
|
return;
|
|
13283
13491
|
}
|
|
@@ -13286,7 +13494,7 @@ const lang = defineRule({
|
|
|
13286
13494
|
if (value === null) return;
|
|
13287
13495
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13496
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13497
|
+
message: MESSAGE$39
|
|
13290
13498
|
});
|
|
13291
13499
|
} })
|
|
13292
13500
|
});
|
|
@@ -13312,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13312
13520
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13313
13521
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13314
13522
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13523
|
+
ignoreStringLiterals: true,
|
|
13315
13524
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13316
13525
|
})
|
|
13317
13526
|
});
|
|
@@ -13330,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13539
|
});
|
|
13331
13540
|
//#endregion
|
|
13332
13541
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
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>`.";
|
|
13334
13543
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13544
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13545
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13580,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13580
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13581
|
context.report({
|
|
13373
13582
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13583
|
+
message: MESSAGE$38
|
|
13375
13584
|
});
|
|
13376
13585
|
return;
|
|
13377
13586
|
}
|
|
@@ -13388,7 +13597,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13597
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13598
|
})) context.report({
|
|
13390
13599
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13600
|
+
message: MESSAGE$38
|
|
13392
13601
|
});
|
|
13393
13602
|
} };
|
|
13394
13603
|
}
|
|
@@ -15189,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15398
|
});
|
|
15190
15399
|
//#endregion
|
|
15191
15400
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15401
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15402
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15403
|
const noAccessKey = defineRule({
|
|
15195
15404
|
id: "no-access-key",
|
|
@@ -15206,7 +15415,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15415
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15416
|
context.report({
|
|
15208
15417
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15418
|
+
message: MESSAGE$37
|
|
15210
15419
|
});
|
|
15211
15420
|
return;
|
|
15212
15421
|
}
|
|
@@ -15216,7 +15425,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15425
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15426
|
context.report({
|
|
15218
15427
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15428
|
+
message: MESSAGE$37
|
|
15220
15429
|
});
|
|
15221
15430
|
}
|
|
15222
15431
|
} })
|
|
@@ -15699,7 +15908,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15699
15908
|
});
|
|
15700
15909
|
//#endregion
|
|
15701
15910
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
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.";
|
|
15703
15912
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15913
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15914
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15935
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15936
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15937
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15938
|
+
message: MESSAGE$36
|
|
15730
15939
|
});
|
|
15731
15940
|
} })
|
|
15732
15941
|
});
|
|
@@ -16094,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16303
|
});
|
|
16095
16304
|
//#endregion
|
|
16096
16305
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16306
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16307
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16308
|
"every",
|
|
16100
16309
|
"filter",
|
|
@@ -16298,7 +16507,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16507
|
}
|
|
16299
16508
|
context.report({
|
|
16300
16509
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16510
|
+
message: MESSAGE$35
|
|
16302
16511
|
});
|
|
16303
16512
|
},
|
|
16304
16513
|
CallExpression(node) {
|
|
@@ -16318,15 +16527,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16527
|
if (propName !== "key") continue;
|
|
16319
16528
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16529
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16530
|
+
message: MESSAGE$35
|
|
16322
16531
|
});
|
|
16323
16532
|
}
|
|
16324
16533
|
}
|
|
16325
16534
|
})
|
|
16326
16535
|
});
|
|
16327
16536
|
//#endregion
|
|
16537
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
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.";
|
|
16539
|
+
const noAsyncEffectCallback = defineRule({
|
|
16540
|
+
id: "no-async-effect-callback",
|
|
16541
|
+
title: "Async effect callback",
|
|
16542
|
+
severity: "warn",
|
|
16543
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16544
|
+
create: (context) => ({ CallExpression(node) {
|
|
16545
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16546
|
+
const callback = getEffectCallback(node);
|
|
16547
|
+
if (!callback) return;
|
|
16548
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16549
|
+
if (!callback.async) return;
|
|
16550
|
+
context.report({
|
|
16551
|
+
node: callback,
|
|
16552
|
+
message: MESSAGE$34
|
|
16553
|
+
});
|
|
16554
|
+
} })
|
|
16555
|
+
});
|
|
16556
|
+
//#endregion
|
|
16328
16557
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
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.";
|
|
16330
16559
|
const resolveSettings$21 = (settings) => {
|
|
16331
16560
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16561
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,7 +16611,7 @@ const noAutofocus = defineRule({
|
|
|
16382
16611
|
}
|
|
16383
16612
|
context.report({
|
|
16384
16613
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16614
|
+
message: MESSAGE$33
|
|
16386
16615
|
});
|
|
16387
16616
|
} };
|
|
16388
16617
|
}
|
|
@@ -16632,6 +16861,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16861
|
}
|
|
16633
16862
|
});
|
|
16634
16863
|
//#endregion
|
|
16864
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16865
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16866
|
+
"FunctionDeclaration",
|
|
16867
|
+
"FunctionExpression",
|
|
16868
|
+
"ArrowFunctionExpression",
|
|
16869
|
+
"ClassDeclaration",
|
|
16870
|
+
"ClassExpression"
|
|
16871
|
+
]);
|
|
16872
|
+
const isReactImport$1 = (symbol) => {
|
|
16873
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16874
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16875
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16876
|
+
return importDeclaration.source.value === "react";
|
|
16877
|
+
};
|
|
16878
|
+
const getImportedName = (symbol) => {
|
|
16879
|
+
if (symbol.kind !== "import") return null;
|
|
16880
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16881
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16882
|
+
};
|
|
16883
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16884
|
+
if (symbol.kind !== "import") return false;
|
|
16885
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16886
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16887
|
+
};
|
|
16888
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16889
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16890
|
+
const symbol = scopes.symbolFor(callee);
|
|
16891
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16892
|
+
};
|
|
16893
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16894
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16895
|
+
if (callee.computed) return false;
|
|
16896
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16897
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16898
|
+
if (callee.property.name !== "createElement") return false;
|
|
16899
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16900
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16901
|
+
};
|
|
16902
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16903
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16904
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16905
|
+
};
|
|
16906
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16907
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16908
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16909
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16910
|
+
const nodeRecord = node;
|
|
16911
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16912
|
+
if (key === "parent") continue;
|
|
16913
|
+
const child = nodeRecord[key];
|
|
16914
|
+
if (Array.isArray(child)) {
|
|
16915
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16916
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16917
|
+
}
|
|
16918
|
+
return false;
|
|
16919
|
+
};
|
|
16920
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16921
|
+
//#endregion
|
|
16922
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16923
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16924
|
+
//#endregion
|
|
16925
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16926
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16927
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16928
|
+
const declaration = symbol.declarationNode;
|
|
16929
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16930
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16931
|
+
return false;
|
|
16932
|
+
};
|
|
16933
|
+
const noCallComponentAsFunction = defineRule({
|
|
16934
|
+
id: "no-call-component-as-function",
|
|
16935
|
+
title: "Component called as a function",
|
|
16936
|
+
severity: "warn",
|
|
16937
|
+
tags: ["test-noise"],
|
|
16938
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16939
|
+
create: (context) => {
|
|
16940
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16941
|
+
const candidateCalls = [];
|
|
16942
|
+
return {
|
|
16943
|
+
JSXOpeningElement(node) {
|
|
16944
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16945
|
+
},
|
|
16946
|
+
CallExpression(node) {
|
|
16947
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16948
|
+
node,
|
|
16949
|
+
callee: node.callee,
|
|
16950
|
+
name: node.callee.name
|
|
16951
|
+
});
|
|
16952
|
+
},
|
|
16953
|
+
"Program:exit"() {
|
|
16954
|
+
for (const candidate of candidateCalls) {
|
|
16955
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16956
|
+
if (!symbol) continue;
|
|
16957
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16958
|
+
node: candidate.node,
|
|
16959
|
+
message: message(candidate.name)
|
|
16960
|
+
});
|
|
16961
|
+
}
|
|
16962
|
+
}
|
|
16963
|
+
};
|
|
16964
|
+
}
|
|
16965
|
+
});
|
|
16966
|
+
//#endregion
|
|
16635
16967
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
16968
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
16969
|
//#endregion
|
|
@@ -16783,7 +17115,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
17115
|
});
|
|
16784
17116
|
//#endregion
|
|
16785
17117
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
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.";
|
|
16787
17119
|
const noChildrenProp = defineRule({
|
|
16788
17120
|
id: "no-children-prop",
|
|
16789
17121
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17127,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17127
|
if (node.name.name !== "children") return;
|
|
16796
17128
|
context.report({
|
|
16797
17129
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17130
|
+
message: MESSAGE$32
|
|
16799
17131
|
});
|
|
16800
17132
|
},
|
|
16801
17133
|
CallExpression(node) {
|
|
@@ -16808,7 +17140,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17140
|
const propertyKey = property.key;
|
|
16809
17141
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17142
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17143
|
+
message: MESSAGE$32
|
|
16812
17144
|
});
|
|
16813
17145
|
}
|
|
16814
17146
|
}
|
|
@@ -16816,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17148
|
});
|
|
16817
17149
|
//#endregion
|
|
16818
17150
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
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.";
|
|
16820
17152
|
const noCloneElement = defineRule({
|
|
16821
17153
|
id: "no-clone-element",
|
|
16822
17154
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17161,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17161
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17162
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17163
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17164
|
+
message: MESSAGE$31
|
|
16833
17165
|
});
|
|
16834
17166
|
return;
|
|
16835
17167
|
}
|
|
@@ -16842,7 +17174,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17174
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17175
|
context.report({
|
|
16844
17176
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17177
|
+
message: MESSAGE$31
|
|
16846
17178
|
});
|
|
16847
17179
|
}
|
|
16848
17180
|
} })
|
|
@@ -16891,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17223
|
};
|
|
16892
17224
|
//#endregion
|
|
16893
17225
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17226
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17227
|
const CONTEXT_MODULES = [
|
|
16896
17228
|
"react",
|
|
16897
17229
|
"use-context-selector",
|
|
@@ -16927,7 +17259,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17259
|
if (!componentOrHookName) return;
|
|
16928
17260
|
context.report({
|
|
16929
17261
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17262
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17263
|
+
});
|
|
17264
|
+
} })
|
|
17265
|
+
});
|
|
17266
|
+
//#endregion
|
|
17267
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
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.";
|
|
17269
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17270
|
+
id: "no-create-ref-in-function-component",
|
|
17271
|
+
title: "createRef in function component",
|
|
17272
|
+
severity: "warn",
|
|
17273
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17274
|
+
create: (context) => ({ CallExpression(node) {
|
|
17275
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17276
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17277
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17278
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17279
|
+
}
|
|
17280
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17281
|
+
if (!enclosingFunction) return;
|
|
17282
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17283
|
+
if (!displayName) return;
|
|
17284
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17285
|
+
context.report({
|
|
17286
|
+
node,
|
|
17287
|
+
message: MESSAGE$29
|
|
16931
17288
|
});
|
|
16932
17289
|
} })
|
|
16933
17290
|
});
|
|
@@ -17067,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17424
|
});
|
|
17068
17425
|
//#endregion
|
|
17069
17426
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17427
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17428
|
const noDanger = defineRule({
|
|
17072
17429
|
id: "no-danger",
|
|
17073
17430
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17080,7 +17437,7 @@ const noDanger = defineRule({
|
|
|
17080
17437
|
if (!propAttribute) return;
|
|
17081
17438
|
context.report({
|
|
17082
17439
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17440
|
+
message: MESSAGE$28
|
|
17084
17441
|
});
|
|
17085
17442
|
},
|
|
17086
17443
|
CallExpression(node) {
|
|
@@ -17092,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17092
17449
|
const propertyKey = property.key;
|
|
17093
17450
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17451
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$28
|
|
17096
17453
|
});
|
|
17097
17454
|
}
|
|
17098
17455
|
}
|
|
@@ -17100,7 +17457,7 @@ const noDanger = defineRule({
|
|
|
17100
17457
|
});
|
|
17101
17458
|
//#endregion
|
|
17102
17459
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17460
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17461
|
const isLineBreak = (child) => {
|
|
17105
17462
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17463
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17527,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17527
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17528
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17529
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17530
|
+
message: MESSAGE$27
|
|
17174
17531
|
});
|
|
17175
17532
|
},
|
|
17176
17533
|
CallExpression(node) {
|
|
@@ -17182,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17539
|
if (!propsShape.hasDangerously) return;
|
|
17183
17540
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17541
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$27
|
|
17186
17543
|
});
|
|
17187
17544
|
}
|
|
17188
17545
|
})
|
|
@@ -17759,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
18116
|
//#endregion
|
|
17760
18117
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
18118
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
18119
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
18120
|
const resolveSettings$20 = (settings) => {
|
|
17764
18121
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
18122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18135,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18135
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18136
|
context.report({
|
|
17780
18137
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18138
|
+
message: MESSAGE$26
|
|
17782
18139
|
});
|
|
17783
18140
|
} };
|
|
17784
18141
|
}
|
|
@@ -17786,7 +18143,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18143
|
//#endregion
|
|
17787
18144
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18145
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18146
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18147
|
const resolveSettings$19 = (settings) => {
|
|
17791
18148
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18149
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18162
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18163
|
context.report({
|
|
17807
18164
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18165
|
+
message: MESSAGE$25
|
|
17809
18166
|
});
|
|
17810
18167
|
} };
|
|
17811
18168
|
}
|
|
@@ -17828,7 +18185,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18185
|
};
|
|
17829
18186
|
//#endregion
|
|
17830
18187
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18188
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18189
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18190
|
let isConstructor = false;
|
|
17834
18191
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18207
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18208
|
context.report({
|
|
17852
18209
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18210
|
+
message: MESSAGE$24
|
|
17854
18211
|
});
|
|
17855
18212
|
};
|
|
17856
18213
|
const noDirectMutationState = defineRule({
|
|
@@ -18060,6 +18417,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18060
18417
|
} })
|
|
18061
18418
|
});
|
|
18062
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
|
|
18063
18440
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18064
18441
|
const noDynamicImportPath = defineRule({
|
|
18065
18442
|
id: "no-dynamic-import-path",
|
|
@@ -19438,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19815
|
"ReactDOM",
|
|
19439
19816
|
"ReactDom"
|
|
19440
19817
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19818
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19819
|
const noFindDomNode = defineRule({
|
|
19443
19820
|
id: "no-find-dom-node",
|
|
19444
19821
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19826,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19826
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19827
|
context.report({
|
|
19451
19828
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19829
|
+
message: MESSAGE$22
|
|
19453
19830
|
});
|
|
19454
19831
|
return;
|
|
19455
19832
|
}
|
|
@@ -19460,7 +19837,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19837
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19838
|
context.report({
|
|
19462
19839
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19840
|
+
message: MESSAGE$22
|
|
19464
19841
|
});
|
|
19465
19842
|
}
|
|
19466
19843
|
} })
|
|
@@ -19523,64 +19900,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
19900
|
} })
|
|
19524
19901
|
});
|
|
19525
19902
|
//#endregion
|
|
19526
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19527
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19528
|
-
"FunctionDeclaration",
|
|
19529
|
-
"FunctionExpression",
|
|
19530
|
-
"ArrowFunctionExpression",
|
|
19531
|
-
"ClassDeclaration",
|
|
19532
|
-
"ClassExpression"
|
|
19533
|
-
]);
|
|
19534
|
-
const isReactImport$1 = (symbol) => {
|
|
19535
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19536
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19537
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19538
|
-
return importDeclaration.source.value === "react";
|
|
19539
|
-
};
|
|
19540
|
-
const getImportedName = (symbol) => {
|
|
19541
|
-
if (symbol.kind !== "import") return null;
|
|
19542
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19543
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19544
|
-
};
|
|
19545
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19546
|
-
if (symbol.kind !== "import") return false;
|
|
19547
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19548
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19549
|
-
};
|
|
19550
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19551
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19552
|
-
const symbol = scopes.symbolFor(callee);
|
|
19553
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19554
|
-
};
|
|
19555
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19556
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19557
|
-
if (callee.computed) return false;
|
|
19558
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19559
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19560
|
-
if (callee.property.name !== "createElement") return false;
|
|
19561
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19562
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19563
|
-
};
|
|
19564
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19565
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19566
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19567
|
-
};
|
|
19568
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19569
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19570
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19571
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19572
|
-
const nodeRecord = node;
|
|
19573
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19574
|
-
if (key === "parent") continue;
|
|
19575
|
-
const child = nodeRecord[key];
|
|
19576
|
-
if (Array.isArray(child)) {
|
|
19577
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19578
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19579
|
-
}
|
|
19580
|
-
return false;
|
|
19581
|
-
};
|
|
19582
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19583
|
-
//#endregion
|
|
19584
19903
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
19904
|
const noGiantComponent = defineRule({
|
|
19586
19905
|
id: "no-giant-component",
|
|
@@ -19759,6 +20078,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
20078
|
} })
|
|
19760
20079
|
});
|
|
19761
20080
|
//#endregion
|
|
20081
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
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.";
|
|
20083
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20084
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20085
|
+
title: "Lazy image with high fetchPriority",
|
|
20086
|
+
severity: "warn",
|
|
20087
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
20088
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20089
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20090
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20091
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20092
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20093
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20094
|
+
context.report({
|
|
20095
|
+
node: node.name,
|
|
20096
|
+
message: MESSAGE$21
|
|
20097
|
+
});
|
|
20098
|
+
} })
|
|
20099
|
+
});
|
|
20100
|
+
//#endregion
|
|
19762
20101
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
20102
|
const noInitializeState = defineRule({
|
|
19764
20103
|
id: "no-initialize-state",
|
|
@@ -19988,8 +20327,31 @@ const noIsMounted = defineRule({
|
|
|
19988
20327
|
} })
|
|
19989
20328
|
});
|
|
19990
20329
|
//#endregion
|
|
20330
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
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)`.";
|
|
20332
|
+
const isJsonMethodCall = (node, method) => {
|
|
20333
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20334
|
+
const callee = node.callee;
|
|
20335
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20336
|
+
};
|
|
20337
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20338
|
+
id: "no-json-parse-stringify-clone",
|
|
20339
|
+
title: "JSON parse/stringify deep clone",
|
|
20340
|
+
severity: "warn",
|
|
20341
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20342
|
+
create: (context) => ({ CallExpression(node) {
|
|
20343
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20344
|
+
const firstArgument = node.arguments?.[0];
|
|
20345
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20346
|
+
context.report({
|
|
20347
|
+
node,
|
|
20348
|
+
message: MESSAGE$20
|
|
20349
|
+
});
|
|
20350
|
+
} })
|
|
20351
|
+
});
|
|
20352
|
+
//#endregion
|
|
19991
20353
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
|
-
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.";
|
|
19993
20355
|
const isJsxElementTypeReference = (node) => {
|
|
19994
20356
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
19995
20357
|
const typeName = node.typeName;
|
|
@@ -20006,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20006
20368
|
if (!typeAnnotation) return;
|
|
20007
20369
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20008
20370
|
node: typeAnnotation,
|
|
20009
|
-
message: MESSAGE$
|
|
20371
|
+
message: MESSAGE$19
|
|
20010
20372
|
});
|
|
20011
20373
|
};
|
|
20012
20374
|
const noJsxElementType = defineRule({
|
|
@@ -20310,9 +20672,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20310
20672
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20673
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20674
|
//#endregion
|
|
20313
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20314
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20315
|
-
//#endregion
|
|
20316
20675
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20676
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20677
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -20464,7 +20823,7 @@ const noMoment = defineRule({
|
|
|
20464
20823
|
});
|
|
20465
20824
|
//#endregion
|
|
20466
20825
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20467
|
-
const MESSAGE$
|
|
20826
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20468
20827
|
const resolveSettings$16 = (settings) => {
|
|
20469
20828
|
const reactDoctor = settings?.["react-doctor"];
|
|
20470
20829
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -20786,7 +21145,7 @@ const noMultiComp = defineRule({
|
|
|
20786
21145
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
20787
21146
|
for (const component of flagged.slice(1)) context.report({
|
|
20788
21147
|
node: component.reportNode,
|
|
20789
|
-
message: MESSAGE$
|
|
21148
|
+
message: MESSAGE$18
|
|
20790
21149
|
});
|
|
20791
21150
|
} };
|
|
20792
21151
|
}
|
|
@@ -20954,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
20954
21313
|
};
|
|
20955
21314
|
//#endregion
|
|
20956
21315
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
20957
|
-
const MESSAGE$
|
|
21316
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
20958
21317
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
20959
21318
|
"copyWithin",
|
|
20960
21319
|
"fill",
|
|
@@ -21164,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21164
21523
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21165
21524
|
context.report({
|
|
21166
21525
|
node: options.crossFileConsumerCallSite,
|
|
21167
|
-
message: `${MESSAGE$
|
|
21526
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21168
21527
|
});
|
|
21169
21528
|
return;
|
|
21170
21529
|
}
|
|
@@ -21173,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21173
21532
|
reportedNodes.add(mutation.node);
|
|
21174
21533
|
context.report({
|
|
21175
21534
|
node: mutation.node,
|
|
21176
|
-
message: MESSAGE$
|
|
21535
|
+
message: MESSAGE$17
|
|
21177
21536
|
});
|
|
21178
21537
|
}
|
|
21179
21538
|
};
|
|
@@ -21445,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21445
21804
|
});
|
|
21446
21805
|
//#endregion
|
|
21447
21806
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21448
|
-
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.";
|
|
21449
21808
|
const resolveSettings$14 = (settings) => {
|
|
21450
21809
|
const reactDoctor = settings?.["react-doctor"];
|
|
21451
21810
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21473,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21473
21832
|
if (numeric === null) {
|
|
21474
21833
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21475
21834
|
node: tabIndex,
|
|
21476
|
-
message: MESSAGE$
|
|
21835
|
+
message: MESSAGE$16
|
|
21477
21836
|
});
|
|
21478
21837
|
return;
|
|
21479
21838
|
}
|
|
@@ -21486,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21486
21845
|
if (!roleAttribute) {
|
|
21487
21846
|
context.report({
|
|
21488
21847
|
node: tabIndex,
|
|
21489
|
-
message: MESSAGE$
|
|
21848
|
+
message: MESSAGE$16
|
|
21490
21849
|
});
|
|
21491
21850
|
return;
|
|
21492
21851
|
}
|
|
@@ -21500,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21500
21859
|
}
|
|
21501
21860
|
context.report({
|
|
21502
21861
|
node: tabIndex,
|
|
21503
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
21504
21863
|
});
|
|
21505
21864
|
} };
|
|
21506
21865
|
}
|
|
@@ -22191,7 +22550,7 @@ const noRandomKey = defineRule({
|
|
|
22191
22550
|
});
|
|
22192
22551
|
//#endregion
|
|
22193
22552
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22194
|
-
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.";
|
|
22195
22554
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22196
22555
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22197
22556
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22217,13 +22576,13 @@ const noReactChildren = defineRule({
|
|
|
22217
22576
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22218
22577
|
context.report({
|
|
22219
22578
|
node: calleeOuter,
|
|
22220
|
-
message: MESSAGE$
|
|
22579
|
+
message: MESSAGE$15
|
|
22221
22580
|
});
|
|
22222
22581
|
return;
|
|
22223
22582
|
}
|
|
22224
22583
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22225
22584
|
node: calleeOuter,
|
|
22226
|
-
message: MESSAGE$
|
|
22585
|
+
message: MESSAGE$15
|
|
22227
22586
|
});
|
|
22228
22587
|
} })
|
|
22229
22588
|
});
|
|
@@ -22546,7 +22905,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22546
22905
|
});
|
|
22547
22906
|
//#endregion
|
|
22548
22907
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22549
|
-
const MESSAGE$
|
|
22908
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22550
22909
|
const isReactDomRenderCall = (node) => {
|
|
22551
22910
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22552
22911
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22570,7 +22929,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22570
22929
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22571
22930
|
context.report({
|
|
22572
22931
|
node: node.callee,
|
|
22573
|
-
message: MESSAGE$
|
|
22932
|
+
message: MESSAGE$14
|
|
22574
22933
|
});
|
|
22575
22934
|
} })
|
|
22576
22935
|
});
|
|
@@ -22730,11 +23089,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22730
23089
|
return "unknown";
|
|
22731
23090
|
};
|
|
22732
23091
|
//#endregion
|
|
22733
|
-
//#region src/plugin/utils/
|
|
22734
|
-
const
|
|
22735
|
-
|
|
23092
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23093
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23094
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23095
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23096
|
+
if (!words) return [];
|
|
23097
|
+
return words.map((word) => word.toLowerCase());
|
|
22736
23098
|
};
|
|
22737
23099
|
//#endregion
|
|
23100
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23101
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23102
|
+
//#endregion
|
|
22738
23103
|
//#region src/plugin/constants/tanstack.ts
|
|
22739
23104
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22740
23105
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23262,7 +23627,7 @@ const getParentComponent = (node) => {
|
|
|
23262
23627
|
};
|
|
23263
23628
|
//#endregion
|
|
23264
23629
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23265
|
-
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.";
|
|
23266
23631
|
const noSetState = defineRule({
|
|
23267
23632
|
id: "no-set-state",
|
|
23268
23633
|
title: "Local class state forbidden",
|
|
@@ -23277,7 +23642,7 @@ const noSetState = defineRule({
|
|
|
23277
23642
|
if (!getParentComponent(node)) return;
|
|
23278
23643
|
context.report({
|
|
23279
23644
|
node: node.callee,
|
|
23280
|
-
message: MESSAGE$
|
|
23645
|
+
message: MESSAGE$13
|
|
23281
23646
|
});
|
|
23282
23647
|
} })
|
|
23283
23648
|
});
|
|
@@ -23439,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23439
23804
|
};
|
|
23440
23805
|
//#endregion
|
|
23441
23806
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23442
|
-
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.";
|
|
23443
23808
|
const DEFAULT_HANDLERS = [
|
|
23444
23809
|
"onClick",
|
|
23445
23810
|
"onMouseDown",
|
|
@@ -23499,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23499
23864
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23500
23865
|
context.report({
|
|
23501
23866
|
node: node.name,
|
|
23502
|
-
message: MESSAGE$
|
|
23867
|
+
message: MESSAGE$12
|
|
23503
23868
|
});
|
|
23504
23869
|
return;
|
|
23505
23870
|
}
|
|
@@ -23509,19 +23874,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23509
23874
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23510
23875
|
context.report({
|
|
23511
23876
|
node: node.name,
|
|
23512
|
-
message: MESSAGE$
|
|
23877
|
+
message: MESSAGE$12
|
|
23513
23878
|
});
|
|
23514
23879
|
return;
|
|
23515
23880
|
}
|
|
23516
23881
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23517
23882
|
context.report({
|
|
23518
23883
|
node: node.name,
|
|
23519
|
-
message: MESSAGE$
|
|
23884
|
+
message: MESSAGE$12
|
|
23520
23885
|
});
|
|
23521
23886
|
} };
|
|
23522
23887
|
}
|
|
23523
23888
|
});
|
|
23524
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
|
|
23525
23937
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23526
23938
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23527
23939
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23572,6 +23984,27 @@ const noStringRefs = defineRule({
|
|
|
23572
23984
|
}
|
|
23573
23985
|
});
|
|
23574
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
|
|
23575
24008
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23576
24009
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23577
24010
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -34681,6 +35114,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34681
35114
|
}
|
|
34682
35115
|
});
|
|
34683
35116
|
//#endregion
|
|
35117
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
35118
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
35119
|
+
"signed",
|
|
35120
|
+
"logged",
|
|
35121
|
+
"sign"
|
|
35122
|
+
]);
|
|
35123
|
+
const mergeSignedInTokens = (tokens) => {
|
|
35124
|
+
const mergedTokens = [];
|
|
35125
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
35126
|
+
const currentToken = tokens[tokenIndex];
|
|
35127
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
35128
|
+
mergedTokens.push(`${currentToken}in`);
|
|
35129
|
+
tokenIndex += 1;
|
|
35130
|
+
continue;
|
|
35131
|
+
}
|
|
35132
|
+
mergedTokens.push(currentToken);
|
|
35133
|
+
}
|
|
35134
|
+
return mergedTokens;
|
|
35135
|
+
};
|
|
35136
|
+
const isAuthGuardName = (calleeName) => {
|
|
35137
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
35138
|
+
if (tokens.length === 0) return false;
|
|
35139
|
+
let hasAssertiveVerb = false;
|
|
35140
|
+
let hasGetterVerb = false;
|
|
35141
|
+
let hasQualifier = false;
|
|
35142
|
+
let hasStrongNoun = false;
|
|
35143
|
+
let hasWeakNoun = false;
|
|
35144
|
+
for (const token of tokens) {
|
|
35145
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35146
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35147
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35148
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35149
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35150
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35151
|
+
}
|
|
35152
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35153
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35154
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35155
|
+
return false;
|
|
35156
|
+
};
|
|
35157
|
+
//#endregion
|
|
34684
35158
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34685
35159
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34686
35160
|
if (!node) return false;
|
|
@@ -34723,9 +35197,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34723
35197
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34724
35198
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34725
35199
|
if (!calleeNode) return null;
|
|
34726
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35200
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35201
|
+
const calleeName = calleeNode.name;
|
|
35202
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35203
|
+
}
|
|
34727
35204
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34728
35205
|
const methodName = calleeNode.property.name;
|
|
35206
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34729
35207
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34730
35208
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34731
35209
|
return methodName;
|
|
@@ -35102,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35102
35580
|
const collectDeclaredNames = (declaration) => {
|
|
35103
35581
|
const names = /* @__PURE__ */ new Set();
|
|
35104
35582
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35105
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35106
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35107
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35108
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35109
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35110
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35111
|
-
}
|
|
35583
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35112
35584
|
return names;
|
|
35113
35585
|
};
|
|
35114
35586
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35118,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35118
35590
|
};
|
|
35119
35591
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35120
35592
|
if (names.size === 0) return false;
|
|
35593
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35121
35594
|
let didRead = false;
|
|
35122
|
-
|
|
35123
|
-
if (
|
|
35124
|
-
|
|
35125
|
-
|
|
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
|
+
}
|
|
35126
35602
|
return didRead;
|
|
35127
35603
|
};
|
|
35128
35604
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36382,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36382
36858
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36383
36859
|
scan: scanByPattern({
|
|
36384
36860
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36385
|
-
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,
|
|
36386
36862
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36387
36863
|
})
|
|
36388
36864
|
});
|
|
@@ -37144,6 +37620,17 @@ const reactDoctorRules = [
|
|
|
37144
37620
|
category: "Performance"
|
|
37145
37621
|
}
|
|
37146
37622
|
},
|
|
37623
|
+
{
|
|
37624
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37625
|
+
id: "auth-token-in-web-storage",
|
|
37626
|
+
source: "react-doctor",
|
|
37627
|
+
originallyExternal: false,
|
|
37628
|
+
rule: {
|
|
37629
|
+
...authTokenInWebStorage,
|
|
37630
|
+
framework: "global",
|
|
37631
|
+
category: "Security"
|
|
37632
|
+
}
|
|
37633
|
+
},
|
|
37147
37634
|
{
|
|
37148
37635
|
key: "react-doctor/autocomplete-valid",
|
|
37149
37636
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +37847,18 @@ const reactDoctorRules = [
|
|
|
37360
37847
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
37848
|
}
|
|
37362
37849
|
},
|
|
37850
|
+
{
|
|
37851
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37852
|
+
id: "dialog-has-accessible-name",
|
|
37853
|
+
source: "react-doctor",
|
|
37854
|
+
originallyExternal: false,
|
|
37855
|
+
rule: {
|
|
37856
|
+
...dialogHasAccessibleName,
|
|
37857
|
+
framework: "global",
|
|
37858
|
+
category: "Accessibility",
|
|
37859
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37860
|
+
}
|
|
37861
|
+
},
|
|
37363
37862
|
{
|
|
37364
37863
|
key: "react-doctor/display-name",
|
|
37365
37864
|
id: "display-name",
|
|
@@ -38519,6 +39018,18 @@ const reactDoctorRules = [
|
|
|
38519
39018
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
39019
|
}
|
|
38521
39020
|
},
|
|
39021
|
+
{
|
|
39022
|
+
key: "react-doctor/no-async-effect-callback",
|
|
39023
|
+
id: "no-async-effect-callback",
|
|
39024
|
+
source: "react-doctor",
|
|
39025
|
+
originallyExternal: false,
|
|
39026
|
+
rule: {
|
|
39027
|
+
...noAsyncEffectCallback,
|
|
39028
|
+
framework: "global",
|
|
39029
|
+
category: "Bugs",
|
|
39030
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
39031
|
+
}
|
|
39032
|
+
},
|
|
38522
39033
|
{
|
|
38523
39034
|
key: "react-doctor/no-autofocus",
|
|
38524
39035
|
id: "no-autofocus",
|
|
@@ -38542,6 +39053,18 @@ const reactDoctorRules = [
|
|
|
38542
39053
|
category: "Performance"
|
|
38543
39054
|
}
|
|
38544
39055
|
},
|
|
39056
|
+
{
|
|
39057
|
+
key: "react-doctor/no-call-component-as-function",
|
|
39058
|
+
id: "no-call-component-as-function",
|
|
39059
|
+
source: "react-doctor",
|
|
39060
|
+
originallyExternal: false,
|
|
39061
|
+
rule: {
|
|
39062
|
+
...noCallComponentAsFunction,
|
|
39063
|
+
framework: "global",
|
|
39064
|
+
category: "Bugs",
|
|
39065
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
39066
|
+
}
|
|
39067
|
+
},
|
|
38545
39068
|
{
|
|
38546
39069
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
39070
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +39125,18 @@ const reactDoctorRules = [
|
|
|
38602
39125
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
39126
|
}
|
|
38604
39127
|
},
|
|
39128
|
+
{
|
|
39129
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
39130
|
+
id: "no-create-ref-in-function-component",
|
|
39131
|
+
source: "react-doctor",
|
|
39132
|
+
originallyExternal: false,
|
|
39133
|
+
rule: {
|
|
39134
|
+
...noCreateRefInFunctionComponent,
|
|
39135
|
+
framework: "global",
|
|
39136
|
+
category: "Bugs",
|
|
39137
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
39138
|
+
}
|
|
39139
|
+
},
|
|
38605
39140
|
{
|
|
38606
39141
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
39142
|
id: "no-create-store-in-render",
|
|
@@ -38779,6 +39314,17 @@ const reactDoctorRules = [
|
|
|
38779
39314
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
38780
39315
|
}
|
|
38781
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
|
+
},
|
|
38782
39328
|
{
|
|
38783
39329
|
key: "react-doctor/no-dynamic-import-path",
|
|
38784
39330
|
id: "no-dynamic-import-path",
|
|
@@ -38976,6 +39522,18 @@ const reactDoctorRules = [
|
|
|
38976
39522
|
category: "Accessibility"
|
|
38977
39523
|
}
|
|
38978
39524
|
},
|
|
39525
|
+
{
|
|
39526
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39527
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39528
|
+
source: "react-doctor",
|
|
39529
|
+
originallyExternal: false,
|
|
39530
|
+
rule: {
|
|
39531
|
+
...noImgLazyWithHighFetchpriority,
|
|
39532
|
+
framework: "global",
|
|
39533
|
+
category: "Performance",
|
|
39534
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39535
|
+
}
|
|
39536
|
+
},
|
|
38979
39537
|
{
|
|
38980
39538
|
key: "react-doctor/no-initialize-state",
|
|
38981
39539
|
id: "no-initialize-state",
|
|
@@ -39046,6 +39604,17 @@ const reactDoctorRules = [
|
|
|
39046
39604
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
39605
|
}
|
|
39048
39606
|
},
|
|
39607
|
+
{
|
|
39608
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39609
|
+
id: "no-json-parse-stringify-clone",
|
|
39610
|
+
source: "react-doctor",
|
|
39611
|
+
originallyExternal: false,
|
|
39612
|
+
rule: {
|
|
39613
|
+
...noJsonParseStringifyClone,
|
|
39614
|
+
framework: "global",
|
|
39615
|
+
category: "Performance"
|
|
39616
|
+
}
|
|
39617
|
+
},
|
|
39049
39618
|
{
|
|
39050
39619
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
39620
|
id: "no-jsx-element-type",
|
|
@@ -39565,6 +40134,18 @@ const reactDoctorRules = [
|
|
|
39565
40134
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39566
40135
|
}
|
|
39567
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
|
+
},
|
|
39568
40149
|
{
|
|
39569
40150
|
key: "react-doctor/no-string-refs",
|
|
39570
40151
|
id: "no-string-refs",
|
|
@@ -39577,6 +40158,17 @@ const reactDoctorRules = [
|
|
|
39577
40158
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39578
40159
|
}
|
|
39579
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
|
+
},
|
|
39580
40172
|
{
|
|
39581
40173
|
key: "react-doctor/no-this-in-sfc",
|
|
39582
40174
|
id: "no-this-in-sfc",
|