oxlint-plugin-react-doctor 0.5.6 → 0.5.7-dev.439e8e1
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 -1
- package/dist/index.js +846 -291
- 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
|
});
|
|
@@ -2965,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
|
|
|
2965
3019
|
"widget",
|
|
2966
3020
|
"window"
|
|
2967
3021
|
]);
|
|
2968
|
-
const PRESENTATION_ROLES$
|
|
3022
|
+
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
2969
3023
|
//#endregion
|
|
2970
3024
|
//#region src/plugin/rules/a11y/aria-role.ts
|
|
2971
3025
|
const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
|
|
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3139
|
scan: scanByPattern({
|
|
3086
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3141
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3142
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3144
|
})
|
|
3091
3145
|
});
|
|
@@ -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([
|
|
@@ -4534,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
4534
4710
|
}
|
|
4535
4711
|
});
|
|
4536
4712
|
//#endregion
|
|
4713
|
+
//#region src/plugin/utils/is-presentation-role.ts
|
|
4714
|
+
const isPresentationRole = (openingElement) => {
|
|
4715
|
+
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
4716
|
+
if (!roleAttribute) return false;
|
|
4717
|
+
const value = getJsxPropStringValue(roleAttribute);
|
|
4718
|
+
return value !== null && PRESENTATION_ROLES$1.has(value);
|
|
4719
|
+
};
|
|
4720
|
+
//#endregion
|
|
4537
4721
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
4538
4722
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
4539
4723
|
"stopPropagation",
|
|
@@ -4571,8 +4755,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4571
4755
|
};
|
|
4572
4756
|
//#endregion
|
|
4573
4757
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
|
-
const
|
|
4575
|
-
const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4758
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4576
4759
|
const KEY_HANDLERS = [
|
|
4577
4760
|
"onKeyUp",
|
|
4578
4761
|
"onKeyDown",
|
|
@@ -4596,15 +4779,11 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4596
4779
|
if (!onClick) return;
|
|
4597
4780
|
if (isPureEventBlockerHandler(onClick)) return;
|
|
4598
4781
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
4599
|
-
|
|
4600
|
-
if (roleAttribute) {
|
|
4601
|
-
const roleValue = getJsxPropStringValue(roleAttribute);
|
|
4602
|
-
if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
|
|
4603
|
-
}
|
|
4782
|
+
if (isPresentationRole(node)) return;
|
|
4604
4783
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4784
|
context.report({
|
|
4606
4785
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4786
|
+
message: MESSAGE$56
|
|
4608
4787
|
});
|
|
4609
4788
|
} };
|
|
4610
4789
|
}
|
|
@@ -4719,7 +4898,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4898
|
};
|
|
4720
4899
|
//#endregion
|
|
4721
4900
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
const MESSAGE$
|
|
4901
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4723
4902
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4903
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4904
|
"alt",
|
|
@@ -4880,7 +5059,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
5059
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
5060
|
context.report({
|
|
4882
5061
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
5062
|
+
message: MESSAGE$55
|
|
4884
5063
|
});
|
|
4885
5064
|
} };
|
|
4886
5065
|
}
|
|
@@ -5306,6 +5485,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5485
|
} })
|
|
5307
5486
|
});
|
|
5308
5487
|
//#endregion
|
|
5488
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5489
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5490
|
+
//#endregion
|
|
5491
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5492
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5493
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5494
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5495
|
+
"aria-label",
|
|
5496
|
+
"aria-labelledby",
|
|
5497
|
+
"title"
|
|
5498
|
+
];
|
|
5499
|
+
const dialogHasAccessibleName = defineRule({
|
|
5500
|
+
id: "dialog-has-accessible-name",
|
|
5501
|
+
title: "Dialog without accessible name",
|
|
5502
|
+
severity: "warn",
|
|
5503
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5504
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5505
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5506
|
+
const tagName = node.name.name;
|
|
5507
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5508
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5509
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5510
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5511
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5512
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5513
|
+
context.report({
|
|
5514
|
+
node: node.name,
|
|
5515
|
+
message: MESSAGE$54
|
|
5516
|
+
});
|
|
5517
|
+
} })
|
|
5518
|
+
});
|
|
5519
|
+
//#endregion
|
|
5309
5520
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5521
|
const PRAGMA$2 = "React";
|
|
5311
5522
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5551,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5551
|
};
|
|
5341
5552
|
//#endregion
|
|
5342
5553
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5554
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5555
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5556
|
"observer",
|
|
5346
5557
|
"lazy",
|
|
@@ -5539,11 +5750,11 @@ const displayName = defineRule({
|
|
|
5539
5750
|
category: "Architecture",
|
|
5540
5751
|
create: (context) => {
|
|
5541
5752
|
const settings = resolveSettings$44(context.settings);
|
|
5542
|
-
const ignoreNamed = settings.ignoreTranspilerName
|
|
5753
|
+
const ignoreNamed = !settings.ignoreTranspilerName;
|
|
5543
5754
|
const reportAt = (node) => {
|
|
5544
5755
|
context.report({
|
|
5545
5756
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5757
|
+
message: MESSAGE$53
|
|
5547
5758
|
});
|
|
5548
5759
|
};
|
|
5549
5760
|
return {
|
|
@@ -7691,7 +7902,7 @@ const forbidElements = defineRule({
|
|
|
7691
7902
|
});
|
|
7692
7903
|
//#endregion
|
|
7693
7904
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7905
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7906
|
const forwardRefUsesRef = defineRule({
|
|
7696
7907
|
id: "forward-ref-uses-ref",
|
|
7697
7908
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7922,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7922
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7923
|
context.report({
|
|
7713
7924
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7925
|
+
message: MESSAGE$52
|
|
7715
7926
|
});
|
|
7716
7927
|
} })
|
|
7717
7928
|
});
|
|
@@ -7748,7 +7959,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7959
|
});
|
|
7749
7960
|
//#endregion
|
|
7750
7961
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
const MESSAGE$
|
|
7962
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7752
7963
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7964
|
"h1",
|
|
7754
7965
|
"h2",
|
|
@@ -7781,7 +7992,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7992
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7993
|
context.report({
|
|
7783
7994
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7995
|
+
message: MESSAGE$51
|
|
7785
7996
|
});
|
|
7786
7997
|
} };
|
|
7787
7998
|
}
|
|
@@ -7919,7 +8130,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8130
|
});
|
|
7920
8131
|
//#endregion
|
|
7921
8132
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
const MESSAGE$
|
|
8133
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7923
8134
|
const resolveSettings$38 = (settings) => {
|
|
7924
8135
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8136
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7962,26 +8173,17 @@ const htmlHasLang = defineRule({
|
|
|
7962
8173
|
return { JSXOpeningElement(node) {
|
|
7963
8174
|
const tag = getElementType(node, context.settings);
|
|
7964
8175
|
if (!tagSet.has(tag)) return;
|
|
7965
|
-
const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
7966
8176
|
const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
|
|
7967
8177
|
if (!lang) {
|
|
7968
8178
|
context.report({
|
|
7969
8179
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8180
|
+
message: MESSAGE$50
|
|
7971
8181
|
});
|
|
7972
8182
|
return;
|
|
7973
8183
|
}
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
node: lang,
|
|
7978
|
-
message: MESSAGE$44
|
|
7979
|
-
});
|
|
7980
|
-
return;
|
|
7981
|
-
}
|
|
7982
|
-
if (hasSpread && !lang) context.report({
|
|
7983
|
-
node: node.name,
|
|
7984
|
-
message: MESSAGE$44
|
|
8184
|
+
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8185
|
+
node: lang,
|
|
8186
|
+
message: MESSAGE$50
|
|
7985
8187
|
});
|
|
7986
8188
|
} };
|
|
7987
8189
|
}
|
|
@@ -8195,7 +8397,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8397
|
});
|
|
8196
8398
|
//#endregion
|
|
8197
8399
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
const MESSAGE$
|
|
8400
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8199
8401
|
const evaluateTitleValue = (value) => {
|
|
8200
8402
|
if (!value) return "missing";
|
|
8201
8403
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8437,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8437
|
if (!titleAttr) {
|
|
8236
8438
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8439
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8440
|
+
message: MESSAGE$49
|
|
8239
8441
|
});
|
|
8240
8442
|
return;
|
|
8241
8443
|
}
|
|
8242
8444
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8445
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8446
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8447
|
+
message: MESSAGE$49
|
|
8246
8448
|
});
|
|
8247
8449
|
} })
|
|
8248
8450
|
});
|
|
@@ -8346,7 +8548,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8548
|
});
|
|
8347
8549
|
//#endregion
|
|
8348
8550
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
const MESSAGE$
|
|
8551
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8350
8552
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8553
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8554
|
"image",
|
|
@@ -8411,7 +8613,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8613
|
if (!altAttribute) return;
|
|
8412
8614
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8615
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8616
|
+
message: MESSAGE$48
|
|
8415
8617
|
});
|
|
8416
8618
|
} };
|
|
8417
8619
|
}
|
|
@@ -8694,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
|
|
|
8694
8896
|
//#region src/plugin/utils/is-non-interactive-role.ts
|
|
8695
8897
|
const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
|
|
8696
8898
|
//#endregion
|
|
8697
|
-
//#region src/plugin/utils/is-presentation-role.ts
|
|
8698
|
-
const isPresentationRole = (openingElement) => {
|
|
8699
|
-
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
8700
|
-
if (!roleAttribute) return false;
|
|
8701
|
-
const value = getJsxPropStringValue(roleAttribute);
|
|
8702
|
-
return value !== null && PRESENTATION_ROLES$2.has(value);
|
|
8703
|
-
};
|
|
8704
|
-
//#endregion
|
|
8705
8899
|
//#region src/plugin/rules/a11y/interactive-supports-focus.ts
|
|
8706
8900
|
const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
|
|
8707
8901
|
const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
|
|
@@ -10476,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10476
10670
|
return false;
|
|
10477
10671
|
};
|
|
10478
10672
|
//#endregion
|
|
10673
|
+
//#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
|
|
10674
|
+
const ascendThroughJsxValueWrappers = (node) => {
|
|
10675
|
+
let current = node;
|
|
10676
|
+
while (current.parent) {
|
|
10677
|
+
const parent = current.parent;
|
|
10678
|
+
if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
|
|
10679
|
+
current = parent;
|
|
10680
|
+
}
|
|
10681
|
+
return current;
|
|
10682
|
+
};
|
|
10683
|
+
const isNonChildrenJsxAttributeValue = (node) => {
|
|
10684
|
+
const container = ascendThroughJsxValueWrappers(node).parent;
|
|
10685
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
|
|
10686
|
+
const attribute = container.parent;
|
|
10687
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
|
|
10688
|
+
return getJsxAttributeName(attribute.name) !== "children";
|
|
10689
|
+
};
|
|
10690
|
+
//#endregion
|
|
10479
10691
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10480
10692
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10481
10693
|
"map",
|
|
@@ -10514,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10514
10726
|
const arrayParent = parent.parent;
|
|
10515
10727
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10516
10728
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10729
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10517
10730
|
return { kind: "array" };
|
|
10518
10731
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10519
10732
|
const callee = parent.callee;
|
|
@@ -10526,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10526
10739
|
if (!targetArg) return null;
|
|
10527
10740
|
let walker = current;
|
|
10528
10741
|
while (walker && walker !== parent) {
|
|
10529
|
-
if (walker === targetArg)
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10742
|
+
if (walker === targetArg) {
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10744
|
+
return {
|
|
10745
|
+
kind: "iterator",
|
|
10746
|
+
callExpression: parent
|
|
10747
|
+
};
|
|
10748
|
+
}
|
|
10533
10749
|
walker = walker.parent ?? null;
|
|
10534
10750
|
}
|
|
10535
10751
|
return null;
|
|
@@ -10768,7 +10984,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10984
|
});
|
|
10769
10985
|
//#endregion
|
|
10770
10986
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10987
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10988
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10989
|
"code",
|
|
10774
10990
|
"pre",
|
|
@@ -10804,7 +11020,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
11020
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
11021
|
context.report({
|
|
10806
11022
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
11023
|
+
message: MESSAGE$47
|
|
10808
11024
|
});
|
|
10809
11025
|
} })
|
|
10810
11026
|
});
|
|
@@ -10835,7 +11051,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
11051
|
};
|
|
10836
11052
|
//#endregion
|
|
10837
11053
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
11054
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
11055
|
const CONTEXT_MODULES$1 = [
|
|
10840
11056
|
"react",
|
|
10841
11057
|
"use-context-selector",
|
|
@@ -10933,7 +11149,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11149
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11150
|
context.report({
|
|
10935
11151
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11152
|
+
message: MESSAGE$46
|
|
10937
11153
|
});
|
|
10938
11154
|
}
|
|
10939
11155
|
}
|
|
@@ -11019,7 +11235,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11235
|
};
|
|
11020
11236
|
//#endregion
|
|
11021
11237
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11238
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11239
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11240
|
"icon",
|
|
11025
11241
|
"Icon",
|
|
@@ -11288,7 +11504,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11504
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11505
|
context.report({
|
|
11290
11506
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11507
|
+
message: MESSAGE$45
|
|
11292
11508
|
});
|
|
11293
11509
|
}
|
|
11294
11510
|
};
|
|
@@ -11576,7 +11792,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11792
|
];
|
|
11577
11793
|
//#endregion
|
|
11578
11794
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11795
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11796
|
const isDataArrayPropName = (propName) => {
|
|
11581
11797
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11798
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11876,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11876
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11877
|
context.report({
|
|
11662
11878
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11879
|
+
message: MESSAGE$44
|
|
11664
11880
|
});
|
|
11665
11881
|
}
|
|
11666
11882
|
};
|
|
@@ -11918,7 +12134,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12134
|
]);
|
|
11919
12135
|
//#endregion
|
|
11920
12136
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12137
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12138
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12139
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12140
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12340,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12340
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12341
|
context.report({
|
|
12126
12342
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12343
|
+
message: MESSAGE$43
|
|
12128
12344
|
});
|
|
12129
12345
|
}
|
|
12130
12346
|
};
|
|
@@ -12344,7 +12560,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12560
|
];
|
|
12345
12561
|
//#endregion
|
|
12346
12562
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12563
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12564
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12565
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12566
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12648
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12649
|
context.report({
|
|
12434
12650
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12651
|
+
message: MESSAGE$42
|
|
12436
12652
|
});
|
|
12437
12653
|
}
|
|
12438
12654
|
};
|
|
@@ -12440,7 +12656,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12656
|
});
|
|
12441
12657
|
//#endregion
|
|
12442
12658
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12659
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
12660
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12445
12661
|
const resolveSettings$28 = (settings) => {
|
|
12446
12662
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12697,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12697
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12698
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12699
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12700
|
+
message: MESSAGE$41
|
|
12485
12701
|
});
|
|
12486
12702
|
}
|
|
12487
12703
|
} };
|
|
@@ -12796,7 +13012,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
13012
|
});
|
|
12797
13013
|
//#endregion
|
|
12798
13014
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
13015
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12800
13016
|
const resolveSettings$25 = (settings) => {
|
|
12801
13017
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
13018
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +13053,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
13053
|
}
|
|
12838
13054
|
context.report({
|
|
12839
13055
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
13056
|
+
message: MESSAGE$40
|
|
12841
13057
|
});
|
|
12842
13058
|
}
|
|
12843
13059
|
} };
|
|
@@ -13065,7 +13281,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13281
|
});
|
|
13066
13282
|
//#endregion
|
|
13067
13283
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
const MESSAGE$
|
|
13284
|
+
const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13069
13285
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13286
|
"aa",
|
|
13071
13287
|
"ab",
|
|
@@ -13277,7 +13493,7 @@ const lang = defineRule({
|
|
|
13277
13493
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13494
|
context.report({
|
|
13279
13495
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13496
|
+
message: MESSAGE$39
|
|
13281
13497
|
});
|
|
13282
13498
|
return;
|
|
13283
13499
|
}
|
|
@@ -13286,7 +13502,7 @@ const lang = defineRule({
|
|
|
13286
13502
|
if (value === null) return;
|
|
13287
13503
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13504
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13505
|
+
message: MESSAGE$39
|
|
13290
13506
|
});
|
|
13291
13507
|
} })
|
|
13292
13508
|
});
|
|
@@ -13312,6 +13528,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13312
13528
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13313
13529
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13314
13530
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13531
|
+
ignoreStringLiterals: true,
|
|
13315
13532
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13316
13533
|
})
|
|
13317
13534
|
});
|
|
@@ -13330,7 +13547,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13547
|
});
|
|
13331
13548
|
//#endregion
|
|
13332
13549
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
const MESSAGE$
|
|
13550
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13334
13551
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13552
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13553
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13588,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13588
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13589
|
context.report({
|
|
13373
13590
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13591
|
+
message: MESSAGE$38
|
|
13375
13592
|
});
|
|
13376
13593
|
return;
|
|
13377
13594
|
}
|
|
@@ -13388,7 +13605,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13605
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13606
|
})) context.report({
|
|
13390
13607
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13608
|
+
message: MESSAGE$38
|
|
13392
13609
|
});
|
|
13393
13610
|
} };
|
|
13394
13611
|
}
|
|
@@ -15189,7 +15406,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15406
|
});
|
|
15190
15407
|
//#endregion
|
|
15191
15408
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15409
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15410
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15411
|
const noAccessKey = defineRule({
|
|
15195
15412
|
id: "no-access-key",
|
|
@@ -15206,7 +15423,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15423
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15424
|
context.report({
|
|
15208
15425
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15426
|
+
message: MESSAGE$37
|
|
15210
15427
|
});
|
|
15211
15428
|
return;
|
|
15212
15429
|
}
|
|
@@ -15216,7 +15433,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15433
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15434
|
context.report({
|
|
15218
15435
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15436
|
+
message: MESSAGE$37
|
|
15220
15437
|
});
|
|
15221
15438
|
}
|
|
15222
15439
|
} })
|
|
@@ -15699,7 +15916,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15699
15916
|
});
|
|
15700
15917
|
//#endregion
|
|
15701
15918
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
const MESSAGE$
|
|
15919
|
+
const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15703
15920
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15921
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15922
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15943,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15943
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15944
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15945
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15946
|
+
message: MESSAGE$36
|
|
15730
15947
|
});
|
|
15731
15948
|
} })
|
|
15732
15949
|
});
|
|
@@ -16094,7 +16311,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16311
|
});
|
|
16095
16312
|
//#endregion
|
|
16096
16313
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16314
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16315
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16316
|
"every",
|
|
16100
16317
|
"filter",
|
|
@@ -16298,7 +16515,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16515
|
}
|
|
16299
16516
|
context.report({
|
|
16300
16517
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16518
|
+
message: MESSAGE$35
|
|
16302
16519
|
});
|
|
16303
16520
|
},
|
|
16304
16521
|
CallExpression(node) {
|
|
@@ -16318,15 +16535,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16535
|
if (propName !== "key") continue;
|
|
16319
16536
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16537
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16538
|
+
message: MESSAGE$35
|
|
16322
16539
|
});
|
|
16323
16540
|
}
|
|
16324
16541
|
}
|
|
16325
16542
|
})
|
|
16326
16543
|
});
|
|
16327
16544
|
//#endregion
|
|
16545
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16546
|
+
const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16547
|
+
const noAsyncEffectCallback = defineRule({
|
|
16548
|
+
id: "no-async-effect-callback",
|
|
16549
|
+
title: "Async effect callback",
|
|
16550
|
+
severity: "warn",
|
|
16551
|
+
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.",
|
|
16552
|
+
create: (context) => ({ CallExpression(node) {
|
|
16553
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16554
|
+
const callback = getEffectCallback(node);
|
|
16555
|
+
if (!callback) return;
|
|
16556
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16557
|
+
if (!callback.async) return;
|
|
16558
|
+
context.report({
|
|
16559
|
+
node: callback,
|
|
16560
|
+
message: MESSAGE$34
|
|
16561
|
+
});
|
|
16562
|
+
} })
|
|
16563
|
+
});
|
|
16564
|
+
//#endregion
|
|
16328
16565
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
const MESSAGE$
|
|
16566
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16330
16567
|
const resolveSettings$21 = (settings) => {
|
|
16331
16568
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16569
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,7 +16619,7 @@ const noAutofocus = defineRule({
|
|
|
16382
16619
|
}
|
|
16383
16620
|
context.report({
|
|
16384
16621
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16622
|
+
message: MESSAGE$33
|
|
16386
16623
|
});
|
|
16387
16624
|
} };
|
|
16388
16625
|
}
|
|
@@ -16632,6 +16869,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16869
|
}
|
|
16633
16870
|
});
|
|
16634
16871
|
//#endregion
|
|
16872
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16873
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16874
|
+
"FunctionDeclaration",
|
|
16875
|
+
"FunctionExpression",
|
|
16876
|
+
"ArrowFunctionExpression",
|
|
16877
|
+
"ClassDeclaration",
|
|
16878
|
+
"ClassExpression"
|
|
16879
|
+
]);
|
|
16880
|
+
const isReactImport$1 = (symbol) => {
|
|
16881
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16882
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16883
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16884
|
+
return importDeclaration.source.value === "react";
|
|
16885
|
+
};
|
|
16886
|
+
const getImportedName = (symbol) => {
|
|
16887
|
+
if (symbol.kind !== "import") return null;
|
|
16888
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16889
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16890
|
+
};
|
|
16891
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16892
|
+
if (symbol.kind !== "import") return false;
|
|
16893
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16894
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16895
|
+
};
|
|
16896
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16897
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16898
|
+
const symbol = scopes.symbolFor(callee);
|
|
16899
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16900
|
+
};
|
|
16901
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16902
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16903
|
+
if (callee.computed) return false;
|
|
16904
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16905
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16906
|
+
if (callee.property.name !== "createElement") return false;
|
|
16907
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16908
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16909
|
+
};
|
|
16910
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16911
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16912
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16913
|
+
};
|
|
16914
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16915
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16916
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16917
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16918
|
+
const nodeRecord = node;
|
|
16919
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16920
|
+
if (key === "parent") continue;
|
|
16921
|
+
const child = nodeRecord[key];
|
|
16922
|
+
if (Array.isArray(child)) {
|
|
16923
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16924
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16925
|
+
}
|
|
16926
|
+
return false;
|
|
16927
|
+
};
|
|
16928
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16929
|
+
//#endregion
|
|
16930
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16931
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16932
|
+
//#endregion
|
|
16933
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16934
|
+
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.`;
|
|
16935
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16936
|
+
const declaration = symbol.declarationNode;
|
|
16937
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16938
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16939
|
+
return false;
|
|
16940
|
+
};
|
|
16941
|
+
const noCallComponentAsFunction = defineRule({
|
|
16942
|
+
id: "no-call-component-as-function",
|
|
16943
|
+
title: "Component called as a function",
|
|
16944
|
+
severity: "warn",
|
|
16945
|
+
tags: ["test-noise"],
|
|
16946
|
+
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.",
|
|
16947
|
+
create: (context) => {
|
|
16948
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16949
|
+
const candidateCalls = [];
|
|
16950
|
+
return {
|
|
16951
|
+
JSXOpeningElement(node) {
|
|
16952
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16953
|
+
},
|
|
16954
|
+
CallExpression(node) {
|
|
16955
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16956
|
+
node,
|
|
16957
|
+
callee: node.callee,
|
|
16958
|
+
name: node.callee.name
|
|
16959
|
+
});
|
|
16960
|
+
},
|
|
16961
|
+
"Program:exit"() {
|
|
16962
|
+
for (const candidate of candidateCalls) {
|
|
16963
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16964
|
+
if (!symbol) continue;
|
|
16965
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16966
|
+
node: candidate.node,
|
|
16967
|
+
message: message(candidate.name)
|
|
16968
|
+
});
|
|
16969
|
+
}
|
|
16970
|
+
}
|
|
16971
|
+
};
|
|
16972
|
+
}
|
|
16973
|
+
});
|
|
16974
|
+
//#endregion
|
|
16635
16975
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
16976
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
16977
|
//#endregion
|
|
@@ -16783,7 +17123,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
17123
|
});
|
|
16784
17124
|
//#endregion
|
|
16785
17125
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
const MESSAGE$
|
|
17126
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16787
17127
|
const noChildrenProp = defineRule({
|
|
16788
17128
|
id: "no-children-prop",
|
|
16789
17129
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17135,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17135
|
if (node.name.name !== "children") return;
|
|
16796
17136
|
context.report({
|
|
16797
17137
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17138
|
+
message: MESSAGE$32
|
|
16799
17139
|
});
|
|
16800
17140
|
},
|
|
16801
17141
|
CallExpression(node) {
|
|
@@ -16808,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17148
|
const propertyKey = property.key;
|
|
16809
17149
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17150
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17151
|
+
message: MESSAGE$32
|
|
16812
17152
|
});
|
|
16813
17153
|
}
|
|
16814
17154
|
}
|
|
@@ -16816,7 +17156,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17156
|
});
|
|
16817
17157
|
//#endregion
|
|
16818
17158
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
const MESSAGE$
|
|
17159
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16820
17160
|
const noCloneElement = defineRule({
|
|
16821
17161
|
id: "no-clone-element",
|
|
16822
17162
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17169,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17169
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17170
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17171
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17172
|
+
message: MESSAGE$31
|
|
16833
17173
|
});
|
|
16834
17174
|
return;
|
|
16835
17175
|
}
|
|
@@ -16842,7 +17182,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17182
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17183
|
context.report({
|
|
16844
17184
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17185
|
+
message: MESSAGE$31
|
|
16846
17186
|
});
|
|
16847
17187
|
}
|
|
16848
17188
|
} })
|
|
@@ -16891,7 +17231,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17231
|
};
|
|
16892
17232
|
//#endregion
|
|
16893
17233
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17234
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17235
|
const CONTEXT_MODULES = [
|
|
16896
17236
|
"react",
|
|
16897
17237
|
"use-context-selector",
|
|
@@ -16927,7 +17267,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17267
|
if (!componentOrHookName) return;
|
|
16928
17268
|
context.report({
|
|
16929
17269
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17270
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17271
|
+
});
|
|
17272
|
+
} })
|
|
17273
|
+
});
|
|
17274
|
+
//#endregion
|
|
17275
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17276
|
+
const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17277
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17278
|
+
id: "no-create-ref-in-function-component",
|
|
17279
|
+
title: "createRef in function component",
|
|
17280
|
+
severity: "warn",
|
|
17281
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17282
|
+
create: (context) => ({ CallExpression(node) {
|
|
17283
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17284
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17285
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17286
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17287
|
+
}
|
|
17288
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17289
|
+
if (!enclosingFunction) return;
|
|
17290
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17291
|
+
if (!displayName) return;
|
|
17292
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17293
|
+
context.report({
|
|
17294
|
+
node,
|
|
17295
|
+
message: MESSAGE$29
|
|
16931
17296
|
});
|
|
16932
17297
|
} })
|
|
16933
17298
|
});
|
|
@@ -17067,12 +17432,13 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17432
|
});
|
|
17068
17433
|
//#endregion
|
|
17069
17434
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17435
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17436
|
const noDanger = defineRule({
|
|
17072
17437
|
id: "no-danger",
|
|
17073
17438
|
title: "Raw HTML injection can run unsafe markup",
|
|
17074
17439
|
severity: "warn",
|
|
17075
17440
|
category: "Security",
|
|
17441
|
+
defaultEnabled: false,
|
|
17076
17442
|
recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
|
|
17077
17443
|
create: (context) => ({
|
|
17078
17444
|
JSXOpeningElement(node) {
|
|
@@ -17080,7 +17446,7 @@ const noDanger = defineRule({
|
|
|
17080
17446
|
if (!propAttribute) return;
|
|
17081
17447
|
context.report({
|
|
17082
17448
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17449
|
+
message: MESSAGE$28
|
|
17084
17450
|
});
|
|
17085
17451
|
},
|
|
17086
17452
|
CallExpression(node) {
|
|
@@ -17092,7 +17458,7 @@ const noDanger = defineRule({
|
|
|
17092
17458
|
const propertyKey = property.key;
|
|
17093
17459
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17460
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17461
|
+
message: MESSAGE$28
|
|
17096
17462
|
});
|
|
17097
17463
|
}
|
|
17098
17464
|
}
|
|
@@ -17100,7 +17466,7 @@ const noDanger = defineRule({
|
|
|
17100
17466
|
});
|
|
17101
17467
|
//#endregion
|
|
17102
17468
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17469
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17470
|
const isLineBreak = (child) => {
|
|
17105
17471
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17472
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17536,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17536
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17537
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17538
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17539
|
+
message: MESSAGE$27
|
|
17174
17540
|
});
|
|
17175
17541
|
},
|
|
17176
17542
|
CallExpression(node) {
|
|
@@ -17182,7 +17548,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17548
|
if (!propsShape.hasDangerously) return;
|
|
17183
17549
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17550
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17551
|
+
message: MESSAGE$27
|
|
17186
17552
|
});
|
|
17187
17553
|
}
|
|
17188
17554
|
})
|
|
@@ -17759,7 +18125,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
18125
|
//#endregion
|
|
17760
18126
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
18127
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
18128
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
18129
|
const resolveSettings$20 = (settings) => {
|
|
17764
18130
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
18131
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18144,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18144
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18145
|
context.report({
|
|
17780
18146
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18147
|
+
message: MESSAGE$26
|
|
17782
18148
|
});
|
|
17783
18149
|
} };
|
|
17784
18150
|
}
|
|
@@ -17786,7 +18152,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18152
|
//#endregion
|
|
17787
18153
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18154
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18155
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18156
|
const resolveSettings$19 = (settings) => {
|
|
17791
18157
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18158
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18171,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18171
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18172
|
context.report({
|
|
17807
18173
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18174
|
+
message: MESSAGE$25
|
|
17809
18175
|
});
|
|
17810
18176
|
} };
|
|
17811
18177
|
}
|
|
@@ -17828,7 +18194,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18194
|
};
|
|
17829
18195
|
//#endregion
|
|
17830
18196
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18197
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18198
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18199
|
let isConstructor = false;
|
|
17834
18200
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18216,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18216
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18217
|
context.report({
|
|
17852
18218
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18219
|
+
message: MESSAGE$24
|
|
17854
18220
|
});
|
|
17855
18221
|
};
|
|
17856
18222
|
const noDirectMutationState = defineRule({
|
|
@@ -18060,6 +18426,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18060
18426
|
} })
|
|
18061
18427
|
});
|
|
18062
18428
|
//#endregion
|
|
18429
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18430
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18431
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18432
|
+
const noDocumentWrite = defineRule({
|
|
18433
|
+
id: "no-document-write",
|
|
18434
|
+
title: "document.write/writeln",
|
|
18435
|
+
severity: "warn",
|
|
18436
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18437
|
+
create: (context) => ({ CallExpression(node) {
|
|
18438
|
+
const callee = node.callee;
|
|
18439
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18440
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18441
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18442
|
+
context.report({
|
|
18443
|
+
node,
|
|
18444
|
+
message: MESSAGE$23
|
|
18445
|
+
});
|
|
18446
|
+
} })
|
|
18447
|
+
});
|
|
18448
|
+
//#endregion
|
|
18063
18449
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18064
18450
|
const noDynamicImportPath = defineRule({
|
|
18065
18451
|
id: "no-dynamic-import-path",
|
|
@@ -19438,7 +19824,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19824
|
"ReactDOM",
|
|
19439
19825
|
"ReactDom"
|
|
19440
19826
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19827
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19828
|
const noFindDomNode = defineRule({
|
|
19443
19829
|
id: "no-find-dom-node",
|
|
19444
19830
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19835,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19835
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19836
|
context.report({
|
|
19451
19837
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19838
|
+
message: MESSAGE$22
|
|
19453
19839
|
});
|
|
19454
19840
|
return;
|
|
19455
19841
|
}
|
|
@@ -19460,7 +19846,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19846
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19847
|
context.report({
|
|
19462
19848
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19849
|
+
message: MESSAGE$22
|
|
19464
19850
|
});
|
|
19465
19851
|
}
|
|
19466
19852
|
} })
|
|
@@ -19523,64 +19909,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
19909
|
} })
|
|
19524
19910
|
});
|
|
19525
19911
|
//#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
19912
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
19913
|
const noGiantComponent = defineRule({
|
|
19586
19914
|
id: "no-giant-component",
|
|
@@ -19759,6 +20087,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
20087
|
} })
|
|
19760
20088
|
});
|
|
19761
20089
|
//#endregion
|
|
20090
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20091
|
+
const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20092
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20093
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20094
|
+
title: "Lazy image with high fetchPriority",
|
|
20095
|
+
severity: "warn",
|
|
20096
|
+
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.",
|
|
20097
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20098
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20099
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20100
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20101
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20102
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20103
|
+
context.report({
|
|
20104
|
+
node: node.name,
|
|
20105
|
+
message: MESSAGE$21
|
|
20106
|
+
});
|
|
20107
|
+
} })
|
|
20108
|
+
});
|
|
20109
|
+
//#endregion
|
|
19762
20110
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
20111
|
const noInitializeState = defineRule({
|
|
19764
20112
|
id: "no-initialize-state",
|
|
@@ -19851,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
19851
20199
|
severity: "warn",
|
|
19852
20200
|
tags: ["test-noise", "react-jsx-only"],
|
|
19853
20201
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
19854
|
-
create: (context) =>
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
20202
|
+
create: (context) => {
|
|
20203
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20204
|
+
return { JSXAttribute(node) {
|
|
20205
|
+
const expression = getInlineStyleExpression(node);
|
|
20206
|
+
if (!expression) return;
|
|
20207
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20208
|
+
if (propertyCount < 8) return;
|
|
20209
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20210
|
+
context.report({
|
|
20211
|
+
node: expression,
|
|
20212
|
+
message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
|
|
20213
|
+
});
|
|
20214
|
+
} };
|
|
20215
|
+
}
|
|
19863
20216
|
});
|
|
19864
20217
|
//#endregion
|
|
19865
20218
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -19988,8 +20341,31 @@ const noIsMounted = defineRule({
|
|
|
19988
20341
|
} })
|
|
19989
20342
|
});
|
|
19990
20343
|
//#endregion
|
|
20344
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20345
|
+
const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20346
|
+
const isJsonMethodCall = (node, method) => {
|
|
20347
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20348
|
+
const callee = node.callee;
|
|
20349
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20350
|
+
};
|
|
20351
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20352
|
+
id: "no-json-parse-stringify-clone",
|
|
20353
|
+
title: "JSON parse/stringify deep clone",
|
|
20354
|
+
severity: "warn",
|
|
20355
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20356
|
+
create: (context) => ({ CallExpression(node) {
|
|
20357
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20358
|
+
const firstArgument = node.arguments?.[0];
|
|
20359
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20360
|
+
context.report({
|
|
20361
|
+
node,
|
|
20362
|
+
message: MESSAGE$20
|
|
20363
|
+
});
|
|
20364
|
+
} })
|
|
20365
|
+
});
|
|
20366
|
+
//#endregion
|
|
19991
20367
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
|
-
const MESSAGE$
|
|
20368
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19993
20369
|
const isJsxElementTypeReference = (node) => {
|
|
19994
20370
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
19995
20371
|
const typeName = node.typeName;
|
|
@@ -20006,7 +20382,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20006
20382
|
if (!typeAnnotation) return;
|
|
20007
20383
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20008
20384
|
node: typeAnnotation,
|
|
20009
|
-
message: MESSAGE$
|
|
20385
|
+
message: MESSAGE$19
|
|
20010
20386
|
});
|
|
20011
20387
|
};
|
|
20012
20388
|
const noJsxElementType = defineRule({
|
|
@@ -20310,9 +20686,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20310
20686
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20687
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20688
|
//#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
20689
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20690
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20691
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -20464,7 +20837,7 @@ const noMoment = defineRule({
|
|
|
20464
20837
|
});
|
|
20465
20838
|
//#endregion
|
|
20466
20839
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20467
|
-
const MESSAGE$
|
|
20840
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20468
20841
|
const resolveSettings$16 = (settings) => {
|
|
20469
20842
|
const reactDoctor = settings?.["react-doctor"];
|
|
20470
20843
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -20786,7 +21159,7 @@ const noMultiComp = defineRule({
|
|
|
20786
21159
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
20787
21160
|
for (const component of flagged.slice(1)) context.report({
|
|
20788
21161
|
node: component.reportNode,
|
|
20789
|
-
message: MESSAGE$
|
|
21162
|
+
message: MESSAGE$18
|
|
20790
21163
|
});
|
|
20791
21164
|
} };
|
|
20792
21165
|
}
|
|
@@ -20954,7 +21327,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
20954
21327
|
};
|
|
20955
21328
|
//#endregion
|
|
20956
21329
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
20957
|
-
const MESSAGE$
|
|
21330
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
20958
21331
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
20959
21332
|
"copyWithin",
|
|
20960
21333
|
"fill",
|
|
@@ -21164,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21164
21537
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21165
21538
|
context.report({
|
|
21166
21539
|
node: options.crossFileConsumerCallSite,
|
|
21167
|
-
message: `${MESSAGE$
|
|
21540
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21168
21541
|
});
|
|
21169
21542
|
return;
|
|
21170
21543
|
}
|
|
@@ -21173,7 +21546,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21173
21546
|
reportedNodes.add(mutation.node);
|
|
21174
21547
|
context.report({
|
|
21175
21548
|
node: mutation.node,
|
|
21176
|
-
message: MESSAGE$
|
|
21549
|
+
message: MESSAGE$17
|
|
21177
21550
|
});
|
|
21178
21551
|
}
|
|
21179
21552
|
};
|
|
@@ -21445,7 +21818,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21445
21818
|
});
|
|
21446
21819
|
//#endregion
|
|
21447
21820
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21448
|
-
const MESSAGE$
|
|
21821
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21449
21822
|
const resolveSettings$14 = (settings) => {
|
|
21450
21823
|
const reactDoctor = settings?.["react-doctor"];
|
|
21451
21824
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21473,7 +21846,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21473
21846
|
if (numeric === null) {
|
|
21474
21847
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21475
21848
|
node: tabIndex,
|
|
21476
|
-
message: MESSAGE$
|
|
21849
|
+
message: MESSAGE$16
|
|
21477
21850
|
});
|
|
21478
21851
|
return;
|
|
21479
21852
|
}
|
|
@@ -21486,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21486
21859
|
if (!roleAttribute) {
|
|
21487
21860
|
context.report({
|
|
21488
21861
|
node: tabIndex,
|
|
21489
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
21490
21863
|
});
|
|
21491
21864
|
return;
|
|
21492
21865
|
}
|
|
@@ -21500,7 +21873,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21500
21873
|
}
|
|
21501
21874
|
context.report({
|
|
21502
21875
|
node: tabIndex,
|
|
21503
|
-
message: MESSAGE$
|
|
21876
|
+
message: MESSAGE$16
|
|
21504
21877
|
});
|
|
21505
21878
|
} };
|
|
21506
21879
|
}
|
|
@@ -22191,7 +22564,7 @@ const noRandomKey = defineRule({
|
|
|
22191
22564
|
});
|
|
22192
22565
|
//#endregion
|
|
22193
22566
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22194
|
-
const MESSAGE$
|
|
22567
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22195
22568
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22196
22569
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22197
22570
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22217,13 +22590,13 @@ const noReactChildren = defineRule({
|
|
|
22217
22590
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22218
22591
|
context.report({
|
|
22219
22592
|
node: calleeOuter,
|
|
22220
|
-
message: MESSAGE$
|
|
22593
|
+
message: MESSAGE$15
|
|
22221
22594
|
});
|
|
22222
22595
|
return;
|
|
22223
22596
|
}
|
|
22224
22597
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22225
22598
|
node: calleeOuter,
|
|
22226
|
-
message: MESSAGE$
|
|
22599
|
+
message: MESSAGE$15
|
|
22227
22600
|
});
|
|
22228
22601
|
} })
|
|
22229
22602
|
});
|
|
@@ -22546,7 +22919,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22546
22919
|
});
|
|
22547
22920
|
//#endregion
|
|
22548
22921
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22549
|
-
const MESSAGE$
|
|
22922
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22550
22923
|
const isReactDomRenderCall = (node) => {
|
|
22551
22924
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22552
22925
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22570,7 +22943,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22570
22943
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22571
22944
|
context.report({
|
|
22572
22945
|
node: node.callee,
|
|
22573
|
-
message: MESSAGE$
|
|
22946
|
+
message: MESSAGE$14
|
|
22574
22947
|
});
|
|
22575
22948
|
} })
|
|
22576
22949
|
});
|
|
@@ -22730,11 +23103,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22730
23103
|
return "unknown";
|
|
22731
23104
|
};
|
|
22732
23105
|
//#endregion
|
|
22733
|
-
//#region src/plugin/utils/
|
|
22734
|
-
const
|
|
22735
|
-
|
|
23106
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23107
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23108
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23109
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23110
|
+
if (!words) return [];
|
|
23111
|
+
return words.map((word) => word.toLowerCase());
|
|
22736
23112
|
};
|
|
22737
23113
|
//#endregion
|
|
23114
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23115
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23116
|
+
//#endregion
|
|
22738
23117
|
//#region src/plugin/constants/tanstack.ts
|
|
22739
23118
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22740
23119
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23262,7 +23641,7 @@ const getParentComponent = (node) => {
|
|
|
23262
23641
|
};
|
|
23263
23642
|
//#endregion
|
|
23264
23643
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23265
|
-
const MESSAGE$
|
|
23644
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23266
23645
|
const noSetState = defineRule({
|
|
23267
23646
|
id: "no-set-state",
|
|
23268
23647
|
title: "Local class state forbidden",
|
|
@@ -23277,7 +23656,7 @@ const noSetState = defineRule({
|
|
|
23277
23656
|
if (!getParentComponent(node)) return;
|
|
23278
23657
|
context.report({
|
|
23279
23658
|
node: node.callee,
|
|
23280
|
-
message: MESSAGE$
|
|
23659
|
+
message: MESSAGE$13
|
|
23281
23660
|
});
|
|
23282
23661
|
} })
|
|
23283
23662
|
});
|
|
@@ -23439,7 +23818,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23439
23818
|
};
|
|
23440
23819
|
//#endregion
|
|
23441
23820
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23442
|
-
const MESSAGE$
|
|
23821
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
23443
23822
|
const DEFAULT_HANDLERS = [
|
|
23444
23823
|
"onClick",
|
|
23445
23824
|
"onMouseDown",
|
|
@@ -23499,7 +23878,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23499
23878
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23500
23879
|
context.report({
|
|
23501
23880
|
node: node.name,
|
|
23502
|
-
message: MESSAGE$
|
|
23881
|
+
message: MESSAGE$12
|
|
23503
23882
|
});
|
|
23504
23883
|
return;
|
|
23505
23884
|
}
|
|
@@ -23509,19 +23888,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23509
23888
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23510
23889
|
context.report({
|
|
23511
23890
|
node: node.name,
|
|
23512
|
-
message: MESSAGE$
|
|
23891
|
+
message: MESSAGE$12
|
|
23513
23892
|
});
|
|
23514
23893
|
return;
|
|
23515
23894
|
}
|
|
23516
23895
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23517
23896
|
context.report({
|
|
23518
23897
|
node: node.name,
|
|
23519
|
-
message: MESSAGE$
|
|
23898
|
+
message: MESSAGE$12
|
|
23520
23899
|
});
|
|
23521
23900
|
} };
|
|
23522
23901
|
}
|
|
23523
23902
|
});
|
|
23524
23903
|
//#endregion
|
|
23904
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
23905
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
23906
|
+
"disabled",
|
|
23907
|
+
"checked",
|
|
23908
|
+
"readonly",
|
|
23909
|
+
"required",
|
|
23910
|
+
"selected",
|
|
23911
|
+
"multiple",
|
|
23912
|
+
"autofocus",
|
|
23913
|
+
"autoplay",
|
|
23914
|
+
"controls",
|
|
23915
|
+
"loop",
|
|
23916
|
+
"muted",
|
|
23917
|
+
"open",
|
|
23918
|
+
"reversed",
|
|
23919
|
+
"default",
|
|
23920
|
+
"novalidate",
|
|
23921
|
+
"formnovalidate",
|
|
23922
|
+
"playsinline",
|
|
23923
|
+
"itemscope",
|
|
23924
|
+
"allowfullscreen"
|
|
23925
|
+
]);
|
|
23926
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
23927
|
+
id: "no-string-false-on-boolean-attribute",
|
|
23928
|
+
title: "String true/false on a boolean attribute",
|
|
23929
|
+
severity: "warn",
|
|
23930
|
+
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.",
|
|
23931
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23932
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23933
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
23934
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
23935
|
+
for (const attribute of node.attributes) {
|
|
23936
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23937
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23938
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
23939
|
+
const value = getJsxPropStringValue(attribute);
|
|
23940
|
+
if (value !== "false" && value !== "true") continue;
|
|
23941
|
+
const attributeName = attribute.name.name;
|
|
23942
|
+
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}\``;
|
|
23943
|
+
context.report({
|
|
23944
|
+
node: attribute,
|
|
23945
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
23946
|
+
});
|
|
23947
|
+
}
|
|
23948
|
+
} })
|
|
23949
|
+
});
|
|
23950
|
+
//#endregion
|
|
23525
23951
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23526
23952
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23527
23953
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23572,6 +23998,27 @@ const noStringRefs = defineRule({
|
|
|
23572
23998
|
}
|
|
23573
23999
|
});
|
|
23574
24000
|
//#endregion
|
|
24001
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24002
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
24003
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24004
|
+
const noSyncXhr = defineRule({
|
|
24005
|
+
id: "no-sync-xhr",
|
|
24006
|
+
title: "Synchronous XMLHttpRequest",
|
|
24007
|
+
severity: "warn",
|
|
24008
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24009
|
+
create: (context) => ({ CallExpression(node) {
|
|
24010
|
+
const callee = node.callee;
|
|
24011
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24012
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24013
|
+
const asyncArgument = node.arguments?.[2];
|
|
24014
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24015
|
+
context.report({
|
|
24016
|
+
node,
|
|
24017
|
+
message: MESSAGE$11
|
|
24018
|
+
});
|
|
24019
|
+
} })
|
|
24020
|
+
});
|
|
24021
|
+
//#endregion
|
|
23575
24022
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23576
24023
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23577
24024
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -24981,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
24981
25428
|
visit(root);
|
|
24982
25429
|
return found;
|
|
24983
25430
|
};
|
|
24984
|
-
const classExtendsReactComponent$1 = (classNode) => {
|
|
24985
|
-
const superClass = classNode.superClass;
|
|
24986
|
-
if (!superClass) return false;
|
|
24987
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
24988
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
24989
|
-
return false;
|
|
24990
|
-
};
|
|
24991
25431
|
const isReactClassComponent = (classNode) => {
|
|
24992
|
-
if (
|
|
25432
|
+
if (isEs6Component(classNode)) return true;
|
|
24993
25433
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
24994
25434
|
};
|
|
24995
25435
|
const findEnclosingComponent = (node) => {
|
|
@@ -25149,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25149
25589
|
create: (context) => {
|
|
25150
25590
|
const settings = resolveSettings$8(context.settings);
|
|
25151
25591
|
const renderPropRegex = compileGlob(settings.propNamePattern);
|
|
25152
|
-
const reportCandidate = (candidateNode, reportNode
|
|
25592
|
+
const reportCandidate = (candidateNode, reportNode) => {
|
|
25153
25593
|
if (isFirstArgumentOfHocCall(candidateNode)) return;
|
|
25154
25594
|
if (isReturnOfMapCallback(candidateNode)) return;
|
|
25155
25595
|
const propInfo = isComponentDeclaredInProp(candidateNode);
|
|
@@ -25170,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25170
25610
|
const inferredName = inferFunctionLikeName(node);
|
|
25171
25611
|
const propInfo = isComponentDeclaredInProp(node);
|
|
25172
25612
|
if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
|
|
25173
|
-
reportCandidate(node, node
|
|
25613
|
+
reportCandidate(node, node);
|
|
25174
25614
|
};
|
|
25175
25615
|
return {
|
|
25176
25616
|
FunctionDeclaration: checkFunctionLike,
|
|
@@ -25180,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
|
|
|
25180
25620
|
if (!node.id) return;
|
|
25181
25621
|
if (!isReactComponentName(node.id.name)) return;
|
|
25182
25622
|
if (!isReactClassComponent(node)) return;
|
|
25183
|
-
reportCandidate(node, node
|
|
25623
|
+
reportCandidate(node, node);
|
|
25184
25624
|
},
|
|
25185
25625
|
ClassExpression(node) {
|
|
25186
25626
|
const inferredName = node.id?.name ?? inferFunctionLikeName(node);
|
|
25187
25627
|
if (!inferredName || !isReactComponentName(inferredName)) return;
|
|
25188
25628
|
if (!isReactClassComponent(node)) return;
|
|
25189
|
-
reportCandidate(node, node
|
|
25629
|
+
reportCandidate(node, node);
|
|
25190
25630
|
},
|
|
25191
25631
|
CallExpression(node) {
|
|
25192
25632
|
if (!isHocCallee$1(node)) return;
|
|
25193
25633
|
if (!hocCallContainsComponent(node)) return;
|
|
25194
|
-
reportCandidate(node, node
|
|
25634
|
+
reportCandidate(node, node);
|
|
25195
25635
|
}
|
|
25196
25636
|
};
|
|
25197
25637
|
}
|
|
@@ -25631,13 +26071,6 @@ const skipTsExpression = (expression) => {
|
|
|
25631
26071
|
if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
|
|
25632
26072
|
return expression;
|
|
25633
26073
|
};
|
|
25634
|
-
const classExtendsReactComponent = (classNode) => {
|
|
25635
|
-
const superClass = classNode.superClass;
|
|
25636
|
-
if (!superClass) return false;
|
|
25637
|
-
if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
|
|
25638
|
-
if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
|
|
25639
|
-
return false;
|
|
25640
|
-
};
|
|
25641
26074
|
const isReactCreateContext = (initializer) => {
|
|
25642
26075
|
if (!initializer) return false;
|
|
25643
26076
|
const expression = skipTsExpression(initializer);
|
|
@@ -25828,7 +26261,7 @@ const onlyExportComponents = defineRule({
|
|
|
25828
26261
|
if (stripped.id) {
|
|
25829
26262
|
const idNode = stripped.id;
|
|
25830
26263
|
isExportedNodeIds.add(stripped);
|
|
25831
|
-
if (isReactComponentName(idNode.name) &&
|
|
26264
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
25832
26265
|
else exports.push({
|
|
25833
26266
|
kind: "non-component",
|
|
25834
26267
|
reportNode: idNode
|
|
@@ -25888,7 +26321,7 @@ const onlyExportComponents = defineRule({
|
|
|
25888
26321
|
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
25889
26322
|
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
25890
26323
|
isExportedNodeIds.add(declaration);
|
|
25891
|
-
if (isReactComponentName(declaration.id.name) &&
|
|
26324
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
25892
26325
|
else exports.push({
|
|
25893
26326
|
kind: "non-component",
|
|
25894
26327
|
reportNode: declaration.id
|
|
@@ -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;
|
|
@@ -34947,6 +35425,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
34947
35425
|
CallExpression(node) {
|
|
34948
35426
|
if (!isServerSideFile) return;
|
|
34949
35427
|
if (!isFetchCall(node)) return;
|
|
35428
|
+
if (isMutatingFetchCall(node)) return;
|
|
34950
35429
|
const optionsArg = node.arguments?.[1];
|
|
34951
35430
|
if (optionsArg && objectExpressionHasNextRevalidate(optionsArg)) return;
|
|
34952
35431
|
const urlArg = node.arguments?.[0];
|
|
@@ -35102,13 +35581,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35102
35581
|
const collectDeclaredNames = (declaration) => {
|
|
35103
35582
|
const names = /* @__PURE__ */ new Set();
|
|
35104
35583
|
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
|
-
}
|
|
35584
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35112
35585
|
return names;
|
|
35113
35586
|
};
|
|
35114
35587
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35118,11 +35591,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35118
35591
|
};
|
|
35119
35592
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35120
35593
|
if (names.size === 0) return false;
|
|
35594
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35121
35595
|
let didRead = false;
|
|
35122
|
-
|
|
35123
|
-
if (
|
|
35124
|
-
|
|
35125
|
-
|
|
35596
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35597
|
+
if (!declarator.init) continue;
|
|
35598
|
+
walkAst(declarator.init, (child) => {
|
|
35599
|
+
if (didRead) return;
|
|
35600
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35601
|
+
});
|
|
35602
|
+
}
|
|
35126
35603
|
return didRead;
|
|
35127
35604
|
};
|
|
35128
35605
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36382,7 +36859,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36382
36859
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36383
36860
|
scan: scanByPattern({
|
|
36384
36861
|
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,
|
|
36862
|
+
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
36863
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36387
36864
|
})
|
|
36388
36865
|
});
|
|
@@ -37144,6 +37621,17 @@ const reactDoctorRules = [
|
|
|
37144
37621
|
category: "Performance"
|
|
37145
37622
|
}
|
|
37146
37623
|
},
|
|
37624
|
+
{
|
|
37625
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37626
|
+
id: "auth-token-in-web-storage",
|
|
37627
|
+
source: "react-doctor",
|
|
37628
|
+
originallyExternal: false,
|
|
37629
|
+
rule: {
|
|
37630
|
+
...authTokenInWebStorage,
|
|
37631
|
+
framework: "global",
|
|
37632
|
+
category: "Security"
|
|
37633
|
+
}
|
|
37634
|
+
},
|
|
37147
37635
|
{
|
|
37148
37636
|
key: "react-doctor/autocomplete-valid",
|
|
37149
37637
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +37848,18 @@ const reactDoctorRules = [
|
|
|
37360
37848
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
37849
|
}
|
|
37362
37850
|
},
|
|
37851
|
+
{
|
|
37852
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37853
|
+
id: "dialog-has-accessible-name",
|
|
37854
|
+
source: "react-doctor",
|
|
37855
|
+
originallyExternal: false,
|
|
37856
|
+
rule: {
|
|
37857
|
+
...dialogHasAccessibleName,
|
|
37858
|
+
framework: "global",
|
|
37859
|
+
category: "Accessibility",
|
|
37860
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37861
|
+
}
|
|
37862
|
+
},
|
|
37363
37863
|
{
|
|
37364
37864
|
key: "react-doctor/display-name",
|
|
37365
37865
|
id: "display-name",
|
|
@@ -38519,6 +39019,18 @@ const reactDoctorRules = [
|
|
|
38519
39019
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
39020
|
}
|
|
38521
39021
|
},
|
|
39022
|
+
{
|
|
39023
|
+
key: "react-doctor/no-async-effect-callback",
|
|
39024
|
+
id: "no-async-effect-callback",
|
|
39025
|
+
source: "react-doctor",
|
|
39026
|
+
originallyExternal: false,
|
|
39027
|
+
rule: {
|
|
39028
|
+
...noAsyncEffectCallback,
|
|
39029
|
+
framework: "global",
|
|
39030
|
+
category: "Bugs",
|
|
39031
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
39032
|
+
}
|
|
39033
|
+
},
|
|
38522
39034
|
{
|
|
38523
39035
|
key: "react-doctor/no-autofocus",
|
|
38524
39036
|
id: "no-autofocus",
|
|
@@ -38542,6 +39054,18 @@ const reactDoctorRules = [
|
|
|
38542
39054
|
category: "Performance"
|
|
38543
39055
|
}
|
|
38544
39056
|
},
|
|
39057
|
+
{
|
|
39058
|
+
key: "react-doctor/no-call-component-as-function",
|
|
39059
|
+
id: "no-call-component-as-function",
|
|
39060
|
+
source: "react-doctor",
|
|
39061
|
+
originallyExternal: false,
|
|
39062
|
+
rule: {
|
|
39063
|
+
...noCallComponentAsFunction,
|
|
39064
|
+
framework: "global",
|
|
39065
|
+
category: "Bugs",
|
|
39066
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
39067
|
+
}
|
|
39068
|
+
},
|
|
38545
39069
|
{
|
|
38546
39070
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
39071
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +39126,18 @@ const reactDoctorRules = [
|
|
|
38602
39126
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
39127
|
}
|
|
38604
39128
|
},
|
|
39129
|
+
{
|
|
39130
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
39131
|
+
id: "no-create-ref-in-function-component",
|
|
39132
|
+
source: "react-doctor",
|
|
39133
|
+
originallyExternal: false,
|
|
39134
|
+
rule: {
|
|
39135
|
+
...noCreateRefInFunctionComponent,
|
|
39136
|
+
framework: "global",
|
|
39137
|
+
category: "Bugs",
|
|
39138
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
39139
|
+
}
|
|
39140
|
+
},
|
|
38605
39141
|
{
|
|
38606
39142
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
39143
|
id: "no-create-store-in-render",
|
|
@@ -38779,6 +39315,17 @@ const reactDoctorRules = [
|
|
|
38779
39315
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
38780
39316
|
}
|
|
38781
39317
|
},
|
|
39318
|
+
{
|
|
39319
|
+
key: "react-doctor/no-document-write",
|
|
39320
|
+
id: "no-document-write",
|
|
39321
|
+
source: "react-doctor",
|
|
39322
|
+
originallyExternal: false,
|
|
39323
|
+
rule: {
|
|
39324
|
+
...noDocumentWrite,
|
|
39325
|
+
framework: "global",
|
|
39326
|
+
category: "Performance"
|
|
39327
|
+
}
|
|
39328
|
+
},
|
|
38782
39329
|
{
|
|
38783
39330
|
key: "react-doctor/no-dynamic-import-path",
|
|
38784
39331
|
id: "no-dynamic-import-path",
|
|
@@ -38976,6 +39523,18 @@ const reactDoctorRules = [
|
|
|
38976
39523
|
category: "Accessibility"
|
|
38977
39524
|
}
|
|
38978
39525
|
},
|
|
39526
|
+
{
|
|
39527
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39528
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39529
|
+
source: "react-doctor",
|
|
39530
|
+
originallyExternal: false,
|
|
39531
|
+
rule: {
|
|
39532
|
+
...noImgLazyWithHighFetchpriority,
|
|
39533
|
+
framework: "global",
|
|
39534
|
+
category: "Performance",
|
|
39535
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39536
|
+
}
|
|
39537
|
+
},
|
|
38979
39538
|
{
|
|
38980
39539
|
key: "react-doctor/no-initialize-state",
|
|
38981
39540
|
id: "no-initialize-state",
|
|
@@ -39046,6 +39605,17 @@ const reactDoctorRules = [
|
|
|
39046
39605
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
39606
|
}
|
|
39048
39607
|
},
|
|
39608
|
+
{
|
|
39609
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39610
|
+
id: "no-json-parse-stringify-clone",
|
|
39611
|
+
source: "react-doctor",
|
|
39612
|
+
originallyExternal: false,
|
|
39613
|
+
rule: {
|
|
39614
|
+
...noJsonParseStringifyClone,
|
|
39615
|
+
framework: "global",
|
|
39616
|
+
category: "Performance"
|
|
39617
|
+
}
|
|
39618
|
+
},
|
|
39049
39619
|
{
|
|
39050
39620
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
39621
|
id: "no-jsx-element-type",
|
|
@@ -39565,6 +40135,18 @@ const reactDoctorRules = [
|
|
|
39565
40135
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39566
40136
|
}
|
|
39567
40137
|
},
|
|
40138
|
+
{
|
|
40139
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40140
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40141
|
+
source: "react-doctor",
|
|
40142
|
+
originallyExternal: false,
|
|
40143
|
+
rule: {
|
|
40144
|
+
...noStringFalseOnBooleanAttribute,
|
|
40145
|
+
framework: "global",
|
|
40146
|
+
category: "Bugs",
|
|
40147
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40148
|
+
}
|
|
40149
|
+
},
|
|
39568
40150
|
{
|
|
39569
40151
|
key: "react-doctor/no-string-refs",
|
|
39570
40152
|
id: "no-string-refs",
|
|
@@ -39577,6 +40159,17 @@ const reactDoctorRules = [
|
|
|
39577
40159
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39578
40160
|
}
|
|
39579
40161
|
},
|
|
40162
|
+
{
|
|
40163
|
+
key: "react-doctor/no-sync-xhr",
|
|
40164
|
+
id: "no-sync-xhr",
|
|
40165
|
+
source: "react-doctor",
|
|
40166
|
+
originallyExternal: false,
|
|
40167
|
+
rule: {
|
|
40168
|
+
...noSyncXhr,
|
|
40169
|
+
framework: "global",
|
|
40170
|
+
category: "Performance"
|
|
40171
|
+
}
|
|
40172
|
+
},
|
|
39580
40173
|
{
|
|
39581
40174
|
key: "react-doctor/no-this-in-sfc",
|
|
39582
40175
|
id: "no-this-in-sfc",
|
|
@@ -41685,32 +42278,6 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
41685
42278
|
}
|
|
41686
42279
|
return unconditional;
|
|
41687
42280
|
};
|
|
41688
|
-
const computeDominatesExit = (cfg) => {
|
|
41689
|
-
const reachableToExit = /* @__PURE__ */ new Set();
|
|
41690
|
-
const queue = [cfg.exit];
|
|
41691
|
-
while (queue.length > 0) {
|
|
41692
|
-
const block = queue.shift();
|
|
41693
|
-
if (reachableToExit.has(block)) continue;
|
|
41694
|
-
reachableToExit.add(block);
|
|
41695
|
-
for (const edge of block.predecessors) queue.push(edge.from);
|
|
41696
|
-
}
|
|
41697
|
-
const dominatesExit = /* @__PURE__ */ new Set();
|
|
41698
|
-
const visit = (block) => {
|
|
41699
|
-
if (block === cfg.exit) return true;
|
|
41700
|
-
if (dominatesExit.has(block)) return true;
|
|
41701
|
-
if (block.successors.length === 0) return false;
|
|
41702
|
-
dominatesExit.add(block);
|
|
41703
|
-
let allReach = true;
|
|
41704
|
-
for (const edge of block.successors) if (!visit(edge.to)) {
|
|
41705
|
-
allReach = false;
|
|
41706
|
-
break;
|
|
41707
|
-
}
|
|
41708
|
-
if (!allReach) dominatesExit.delete(block);
|
|
41709
|
-
return allReach;
|
|
41710
|
-
};
|
|
41711
|
-
for (const block of cfg.blocks) visit(block);
|
|
41712
|
-
return dominatesExit;
|
|
41713
|
-
};
|
|
41714
42281
|
const analyzeControlFlow = (program) => {
|
|
41715
42282
|
nextBlockId = 0;
|
|
41716
42283
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
@@ -41718,8 +42285,7 @@ const analyzeControlFlow = (program) => {
|
|
|
41718
42285
|
const cfg = buildFunctionCfg(functionNode, body);
|
|
41719
42286
|
functionCfgs.set(functionNode, {
|
|
41720
42287
|
cfg,
|
|
41721
|
-
unconditionalSet: computeUnconditionalSet(cfg)
|
|
41722
|
-
dominatesExitSet: computeDominatesExit(cfg)
|
|
42288
|
+
unconditionalSet: computeUnconditionalSet(cfg)
|
|
41723
42289
|
});
|
|
41724
42290
|
};
|
|
41725
42291
|
if (isNodeOfType(program, "Program")) buildFor(program, {
|
|
@@ -41762,20 +42328,10 @@ const analyzeControlFlow = (program) => {
|
|
|
41762
42328
|
if (!block) return true;
|
|
41763
42329
|
return entry.unconditionalSet.has(block);
|
|
41764
42330
|
};
|
|
41765
|
-
const dominatesExit = (node) => {
|
|
41766
|
-
const owner = enclosingFunction(node);
|
|
41767
|
-
if (!owner) return true;
|
|
41768
|
-
const entry = functionCfgs.get(owner);
|
|
41769
|
-
if (!entry) return true;
|
|
41770
|
-
const block = entry.cfg.blockOf(node);
|
|
41771
|
-
if (!block) return true;
|
|
41772
|
-
return entry.dominatesExitSet.has(block);
|
|
41773
|
-
};
|
|
41774
42331
|
return {
|
|
41775
42332
|
cfgFor,
|
|
41776
42333
|
enclosingFunction,
|
|
41777
|
-
isUnconditionalFromEntry
|
|
41778
|
-
dominatesExit
|
|
42334
|
+
isUnconditionalFromEntry
|
|
41779
42335
|
};
|
|
41780
42336
|
};
|
|
41781
42337
|
//#endregion
|
|
@@ -41800,8 +42356,7 @@ const buildFallbackScopes = () => ({
|
|
|
41800
42356
|
const FALLBACK_CFG = {
|
|
41801
42357
|
cfgFor: () => null,
|
|
41802
42358
|
enclosingFunction: () => null,
|
|
41803
|
-
isUnconditionalFromEntry: () => false
|
|
41804
|
-
dominatesExit: () => false
|
|
42359
|
+
isUnconditionalFromEntry: () => false
|
|
41805
42360
|
};
|
|
41806
42361
|
const wrapWithSemanticContext = (rule) => ({
|
|
41807
42362
|
...rule,
|