oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.424d8f9
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 +945 -21
- package/dist/index.js +1503 -229
- 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$64 = "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$64
|
|
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$63 = "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$63
|
|
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$63
|
|
2300
2354
|
});
|
|
2301
2355
|
} })
|
|
2302
2356
|
});
|
|
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3139
|
scan: scanByPattern({
|
|
3086
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3141
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3142
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3144
|
})
|
|
3091
3145
|
});
|
|
@@ -3104,6 +3158,76 @@ const AUTH_FUNCTION_NAMES = new Set([
|
|
|
3104
3158
|
"getAuth",
|
|
3105
3159
|
"validateSession"
|
|
3106
3160
|
]);
|
|
3161
|
+
const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
|
|
3162
|
+
const AUTH_STANDALONE_NOUN_TOKENS = new Set([
|
|
3163
|
+
"signedin",
|
|
3164
|
+
"loggedin",
|
|
3165
|
+
"signin"
|
|
3166
|
+
]);
|
|
3167
|
+
const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
|
|
3168
|
+
"require",
|
|
3169
|
+
"ensure",
|
|
3170
|
+
"assert",
|
|
3171
|
+
"verify",
|
|
3172
|
+
"validate",
|
|
3173
|
+
"check",
|
|
3174
|
+
"protect",
|
|
3175
|
+
"enforce",
|
|
3176
|
+
"guard",
|
|
3177
|
+
"gate",
|
|
3178
|
+
"restrict",
|
|
3179
|
+
"is",
|
|
3180
|
+
"has",
|
|
3181
|
+
"can",
|
|
3182
|
+
"must"
|
|
3183
|
+
]);
|
|
3184
|
+
const AUTH_GETTER_VERB_TOKENS = new Set([
|
|
3185
|
+
"get",
|
|
3186
|
+
"fetch",
|
|
3187
|
+
"load",
|
|
3188
|
+
"read",
|
|
3189
|
+
"resolve",
|
|
3190
|
+
"retrieve",
|
|
3191
|
+
"use"
|
|
3192
|
+
]);
|
|
3193
|
+
const AUTH_QUALIFIER_TOKENS = new Set([
|
|
3194
|
+
"current",
|
|
3195
|
+
"my",
|
|
3196
|
+
"own"
|
|
3197
|
+
]);
|
|
3198
|
+
const AUTH_STRONG_NOUN_TOKENS = new Set([
|
|
3199
|
+
"session",
|
|
3200
|
+
"sessions",
|
|
3201
|
+
"login",
|
|
3202
|
+
"admin",
|
|
3203
|
+
"admins",
|
|
3204
|
+
"superadmin",
|
|
3205
|
+
"superuser",
|
|
3206
|
+
"role",
|
|
3207
|
+
"roles",
|
|
3208
|
+
"permission",
|
|
3209
|
+
"permissions",
|
|
3210
|
+
"jwt",
|
|
3211
|
+
"identity",
|
|
3212
|
+
"principal",
|
|
3213
|
+
"credential",
|
|
3214
|
+
"credentials"
|
|
3215
|
+
]);
|
|
3216
|
+
const AUTH_WEAK_NOUN_TOKENS = new Set([
|
|
3217
|
+
"user",
|
|
3218
|
+
"users",
|
|
3219
|
+
"account",
|
|
3220
|
+
"accounts",
|
|
3221
|
+
"token",
|
|
3222
|
+
"tokens",
|
|
3223
|
+
"access",
|
|
3224
|
+
"me",
|
|
3225
|
+
"viewer",
|
|
3226
|
+
"caller",
|
|
3227
|
+
"subject",
|
|
3228
|
+
"scope",
|
|
3229
|
+
"scopes"
|
|
3230
|
+
]);
|
|
3107
3231
|
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3108
3232
|
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3109
3233
|
const SECRET_PATTERNS = [
|
|
@@ -4201,6 +4325,58 @@ const asyncParallel = defineRule({
|
|
|
4201
4325
|
}
|
|
4202
4326
|
});
|
|
4203
4327
|
//#endregion
|
|
4328
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4329
|
+
const MESSAGE$62 = "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$62
|
|
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$62
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
})
|
|
4378
|
+
});
|
|
4379
|
+
//#endregion
|
|
4204
4380
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4205
4381
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4206
4382
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4572,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4572
4748
|
//#endregion
|
|
4573
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4575
|
-
const MESSAGE$
|
|
4751
|
+
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4576
4752
|
const KEY_HANDLERS = [
|
|
4577
4753
|
"onKeyUp",
|
|
4578
4754
|
"onKeyDown",
|
|
@@ -4604,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4604
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4781
|
context.report({
|
|
4606
4782
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$61
|
|
4608
4784
|
});
|
|
4609
4785
|
} };
|
|
4610
4786
|
}
|
|
@@ -4719,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4895
|
};
|
|
4720
4896
|
//#endregion
|
|
4721
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
const MESSAGE$
|
|
4898
|
+
const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4723
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4901
|
"alt",
|
|
@@ -4880,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
5057
|
context.report({
|
|
4882
5058
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$60
|
|
4884
5060
|
});
|
|
4885
5061
|
} };
|
|
4886
5062
|
}
|
|
@@ -5009,6 +5185,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
5009
5185
|
return findings;
|
|
5010
5186
|
}
|
|
5011
5187
|
});
|
|
5188
|
+
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5012
5189
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5013
5190
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5014
5191
|
"continue",
|
|
@@ -5306,6 +5483,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5483
|
} })
|
|
5307
5484
|
});
|
|
5308
5485
|
//#endregion
|
|
5486
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5487
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5488
|
+
//#endregion
|
|
5489
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5490
|
+
const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5491
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5492
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5493
|
+
"aria-label",
|
|
5494
|
+
"aria-labelledby",
|
|
5495
|
+
"title"
|
|
5496
|
+
];
|
|
5497
|
+
const dialogHasAccessibleName = defineRule({
|
|
5498
|
+
id: "dialog-has-accessible-name",
|
|
5499
|
+
title: "Dialog without accessible name",
|
|
5500
|
+
severity: "warn",
|
|
5501
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5502
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5503
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5504
|
+
const tagName = node.name.name;
|
|
5505
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5506
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5507
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5508
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5509
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5510
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5511
|
+
context.report({
|
|
5512
|
+
node: node.name,
|
|
5513
|
+
message: MESSAGE$59
|
|
5514
|
+
});
|
|
5515
|
+
} })
|
|
5516
|
+
});
|
|
5517
|
+
//#endregion
|
|
5309
5518
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5519
|
const PRAGMA$2 = "React";
|
|
5311
5520
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5549,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5549
|
};
|
|
5341
5550
|
//#endregion
|
|
5342
5551
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5552
|
+
const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5553
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5554
|
"observer",
|
|
5346
5555
|
"lazy",
|
|
@@ -5543,7 +5752,7 @@ const displayName = defineRule({
|
|
|
5543
5752
|
const reportAt = (node) => {
|
|
5544
5753
|
context.report({
|
|
5545
5754
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5755
|
+
message: MESSAGE$58
|
|
5547
5756
|
});
|
|
5548
5757
|
};
|
|
5549
5758
|
return {
|
|
@@ -7691,7 +7900,7 @@ const forbidElements = defineRule({
|
|
|
7691
7900
|
});
|
|
7692
7901
|
//#endregion
|
|
7693
7902
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7903
|
+
const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7904
|
const forwardRefUsesRef = defineRule({
|
|
7696
7905
|
id: "forward-ref-uses-ref",
|
|
7697
7906
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7920,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7920
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7921
|
context.report({
|
|
7713
7922
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7923
|
+
message: MESSAGE$57
|
|
7715
7924
|
});
|
|
7716
7925
|
} })
|
|
7717
7926
|
});
|
|
@@ -7748,7 +7957,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7957
|
});
|
|
7749
7958
|
//#endregion
|
|
7750
7959
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
const MESSAGE$
|
|
7960
|
+
const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7752
7961
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7962
|
"h1",
|
|
7754
7963
|
"h2",
|
|
@@ -7781,7 +7990,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7990
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7991
|
context.report({
|
|
7783
7992
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7993
|
+
message: MESSAGE$56
|
|
7785
7994
|
});
|
|
7786
7995
|
} };
|
|
7787
7996
|
}
|
|
@@ -7919,7 +8128,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8128
|
});
|
|
7920
8129
|
//#endregion
|
|
7921
8130
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
const MESSAGE$
|
|
8131
|
+
const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7923
8132
|
const resolveSettings$38 = (settings) => {
|
|
7924
8133
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8134
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7967,7 +8176,7 @@ const htmlHasLang = defineRule({
|
|
|
7967
8176
|
if (!lang) {
|
|
7968
8177
|
context.report({
|
|
7969
8178
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8179
|
+
message: MESSAGE$55
|
|
7971
8180
|
});
|
|
7972
8181
|
return;
|
|
7973
8182
|
}
|
|
@@ -7975,13 +8184,13 @@ const htmlHasLang = defineRule({
|
|
|
7975
8184
|
if (verdict === "missing" || verdict === "empty") {
|
|
7976
8185
|
context.report({
|
|
7977
8186
|
node: lang,
|
|
7978
|
-
message: MESSAGE$
|
|
8187
|
+
message: MESSAGE$55
|
|
7979
8188
|
});
|
|
7980
8189
|
return;
|
|
7981
8190
|
}
|
|
7982
8191
|
if (hasSpread && !lang) context.report({
|
|
7983
8192
|
node: node.name,
|
|
7984
|
-
message: MESSAGE$
|
|
8193
|
+
message: MESSAGE$55
|
|
7985
8194
|
});
|
|
7986
8195
|
} };
|
|
7987
8196
|
}
|
|
@@ -8195,7 +8404,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8404
|
});
|
|
8196
8405
|
//#endregion
|
|
8197
8406
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
const MESSAGE$
|
|
8407
|
+
const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8199
8408
|
const evaluateTitleValue = (value) => {
|
|
8200
8409
|
if (!value) return "missing";
|
|
8201
8410
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8444,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8444
|
if (!titleAttr) {
|
|
8236
8445
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8446
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8447
|
+
message: MESSAGE$54
|
|
8239
8448
|
});
|
|
8240
8449
|
return;
|
|
8241
8450
|
}
|
|
8242
8451
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8452
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8453
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8454
|
+
message: MESSAGE$54
|
|
8246
8455
|
});
|
|
8247
8456
|
} })
|
|
8248
8457
|
});
|
|
@@ -8346,7 +8555,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8555
|
});
|
|
8347
8556
|
//#endregion
|
|
8348
8557
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
const MESSAGE$
|
|
8558
|
+
const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8350
8559
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8560
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8561
|
"image",
|
|
@@ -8411,7 +8620,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8620
|
if (!altAttribute) return;
|
|
8412
8621
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8622
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8623
|
+
message: MESSAGE$53
|
|
8415
8624
|
});
|
|
8416
8625
|
} };
|
|
8417
8626
|
}
|
|
@@ -10768,7 +10977,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10977
|
});
|
|
10769
10978
|
//#endregion
|
|
10770
10979
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10980
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10981
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10982
|
"code",
|
|
10774
10983
|
"pre",
|
|
@@ -10804,7 +11013,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
11013
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
11014
|
context.report({
|
|
10806
11015
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
11016
|
+
message: MESSAGE$52
|
|
10808
11017
|
});
|
|
10809
11018
|
} })
|
|
10810
11019
|
});
|
|
@@ -10835,7 +11044,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
11044
|
};
|
|
10836
11045
|
//#endregion
|
|
10837
11046
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
11047
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
11048
|
const CONTEXT_MODULES$1 = [
|
|
10840
11049
|
"react",
|
|
10841
11050
|
"use-context-selector",
|
|
@@ -10933,7 +11142,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11142
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11143
|
context.report({
|
|
10935
11144
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11145
|
+
message: MESSAGE$51
|
|
10937
11146
|
});
|
|
10938
11147
|
}
|
|
10939
11148
|
}
|
|
@@ -11019,7 +11228,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11228
|
};
|
|
11020
11229
|
//#endregion
|
|
11021
11230
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11231
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11232
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11233
|
"icon",
|
|
11025
11234
|
"Icon",
|
|
@@ -11288,7 +11497,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11497
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11498
|
context.report({
|
|
11290
11499
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11500
|
+
message: MESSAGE$50
|
|
11292
11501
|
});
|
|
11293
11502
|
}
|
|
11294
11503
|
};
|
|
@@ -11576,7 +11785,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11785
|
];
|
|
11577
11786
|
//#endregion
|
|
11578
11787
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11788
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11789
|
const isDataArrayPropName = (propName) => {
|
|
11581
11790
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11791
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11869,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11869
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11870
|
context.report({
|
|
11662
11871
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11872
|
+
message: MESSAGE$49
|
|
11664
11873
|
});
|
|
11665
11874
|
}
|
|
11666
11875
|
};
|
|
@@ -11918,7 +12127,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12127
|
]);
|
|
11919
12128
|
//#endregion
|
|
11920
12129
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12130
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12131
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12132
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12133
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12333,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12333
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12334
|
context.report({
|
|
12126
12335
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12336
|
+
message: MESSAGE$48
|
|
12128
12337
|
});
|
|
12129
12338
|
}
|
|
12130
12339
|
};
|
|
@@ -12344,7 +12553,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12553
|
];
|
|
12345
12554
|
//#endregion
|
|
12346
12555
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12556
|
+
const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12557
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12558
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12559
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12641,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12641
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12642
|
context.report({
|
|
12434
12643
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12644
|
+
message: MESSAGE$47
|
|
12436
12645
|
});
|
|
12437
12646
|
}
|
|
12438
12647
|
};
|
|
@@ -12440,7 +12649,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12649
|
});
|
|
12441
12650
|
//#endregion
|
|
12442
12651
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12652
|
+
const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
12653
|
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
12654
|
const resolveSettings$28 = (settings) => {
|
|
12446
12655
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12690,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12690
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12691
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12692
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12693
|
+
message: MESSAGE$46
|
|
12485
12694
|
});
|
|
12486
12695
|
}
|
|
12487
12696
|
} };
|
|
@@ -12796,7 +13005,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
13005
|
});
|
|
12797
13006
|
//#endregion
|
|
12798
13007
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
13008
|
+
const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
|
|
12800
13009
|
const resolveSettings$25 = (settings) => {
|
|
12801
13010
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
13011
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +13046,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
13046
|
}
|
|
12838
13047
|
context.report({
|
|
12839
13048
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
13049
|
+
message: MESSAGE$45
|
|
12841
13050
|
});
|
|
12842
13051
|
}
|
|
12843
13052
|
} };
|
|
@@ -13065,7 +13274,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13274
|
});
|
|
13066
13275
|
//#endregion
|
|
13067
13276
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
const MESSAGE$
|
|
13277
|
+
const MESSAGE$44 = "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
13278
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13279
|
"aa",
|
|
13071
13280
|
"ab",
|
|
@@ -13277,7 +13486,7 @@ const lang = defineRule({
|
|
|
13277
13486
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13487
|
context.report({
|
|
13279
13488
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13489
|
+
message: MESSAGE$44
|
|
13281
13490
|
});
|
|
13282
13491
|
return;
|
|
13283
13492
|
}
|
|
@@ -13286,7 +13495,7 @@ const lang = defineRule({
|
|
|
13286
13495
|
if (value === null) return;
|
|
13287
13496
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13497
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13498
|
+
message: MESSAGE$44
|
|
13290
13499
|
});
|
|
13291
13500
|
} })
|
|
13292
13501
|
});
|
|
@@ -13312,6 +13521,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13312
13521
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13313
13522
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13314
13523
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13524
|
+
ignoreStringLiterals: true,
|
|
13315
13525
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13316
13526
|
})
|
|
13317
13527
|
});
|
|
@@ -13330,7 +13540,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13540
|
});
|
|
13331
13541
|
//#endregion
|
|
13332
13542
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
const MESSAGE$
|
|
13543
|
+
const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13334
13544
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13545
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13546
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13581,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13581
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13582
|
context.report({
|
|
13373
13583
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13584
|
+
message: MESSAGE$43
|
|
13375
13585
|
});
|
|
13376
13586
|
return;
|
|
13377
13587
|
}
|
|
@@ -13388,7 +13598,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13598
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13599
|
})) context.report({
|
|
13390
13600
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13601
|
+
message: MESSAGE$43
|
|
13392
13602
|
});
|
|
13393
13603
|
} };
|
|
13394
13604
|
}
|
|
@@ -15189,7 +15399,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15399
|
});
|
|
15190
15400
|
//#endregion
|
|
15191
15401
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15402
|
+
const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15403
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15404
|
const noAccessKey = defineRule({
|
|
15195
15405
|
id: "no-access-key",
|
|
@@ -15206,7 +15416,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15416
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15417
|
context.report({
|
|
15208
15418
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15419
|
+
message: MESSAGE$42
|
|
15210
15420
|
});
|
|
15211
15421
|
return;
|
|
15212
15422
|
}
|
|
@@ -15216,7 +15426,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15426
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15427
|
context.report({
|
|
15218
15428
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15429
|
+
message: MESSAGE$42
|
|
15220
15430
|
});
|
|
15221
15431
|
}
|
|
15222
15432
|
} })
|
|
@@ -15698,8 +15908,41 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15698
15908
|
} })
|
|
15699
15909
|
});
|
|
15700
15910
|
//#endregion
|
|
15911
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15912
|
+
const getStringFromClassNameAttr = (node) => {
|
|
15913
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15914
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15915
|
+
if (!classAttr?.value) return null;
|
|
15916
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15917
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15918
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15919
|
+
return null;
|
|
15920
|
+
};
|
|
15921
|
+
//#endregion
|
|
15922
|
+
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15923
|
+
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15924
|
+
const noArbitraryPxFontSize = defineRule({
|
|
15925
|
+
id: "no-arbitrary-px-font-size",
|
|
15926
|
+
title: "Pixel arbitrary font size",
|
|
15927
|
+
tags: ["design", "test-noise"],
|
|
15928
|
+
severity: "warn",
|
|
15929
|
+
category: "Accessibility",
|
|
15930
|
+
recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
|
|
15931
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
15932
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
15933
|
+
if (!classNameValue) return;
|
|
15934
|
+
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15935
|
+
const rem = parseFloat(match[1]) / 16;
|
|
15936
|
+
context.report({
|
|
15937
|
+
node,
|
|
15938
|
+
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15939
|
+
});
|
|
15940
|
+
}
|
|
15941
|
+
} })
|
|
15942
|
+
});
|
|
15943
|
+
//#endregion
|
|
15701
15944
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
const MESSAGE$
|
|
15945
|
+
const MESSAGE$41 = "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
15946
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15947
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15948
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15969,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15969
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15970
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15971
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15972
|
+
message: MESSAGE$41
|
|
15730
15973
|
});
|
|
15731
15974
|
} })
|
|
15732
15975
|
});
|
|
@@ -16094,7 +16337,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16337
|
});
|
|
16095
16338
|
//#endregion
|
|
16096
16339
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16340
|
+
const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16341
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16342
|
"every",
|
|
16100
16343
|
"filter",
|
|
@@ -16298,7 +16541,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16541
|
}
|
|
16299
16542
|
context.report({
|
|
16300
16543
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16544
|
+
message: MESSAGE$40
|
|
16302
16545
|
});
|
|
16303
16546
|
},
|
|
16304
16547
|
CallExpression(node) {
|
|
@@ -16318,15 +16561,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16561
|
if (propName !== "key") continue;
|
|
16319
16562
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16563
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16564
|
+
message: MESSAGE$40
|
|
16322
16565
|
});
|
|
16323
16566
|
}
|
|
16324
16567
|
}
|
|
16325
16568
|
})
|
|
16326
16569
|
});
|
|
16327
16570
|
//#endregion
|
|
16571
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16572
|
+
const MESSAGE$39 = "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.";
|
|
16573
|
+
const noAsyncEffectCallback = defineRule({
|
|
16574
|
+
id: "no-async-effect-callback",
|
|
16575
|
+
title: "Async effect callback",
|
|
16576
|
+
severity: "warn",
|
|
16577
|
+
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.",
|
|
16578
|
+
create: (context) => ({ CallExpression(node) {
|
|
16579
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16580
|
+
const callback = getEffectCallback(node);
|
|
16581
|
+
if (!callback) return;
|
|
16582
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16583
|
+
if (!callback.async) return;
|
|
16584
|
+
context.report({
|
|
16585
|
+
node: callback,
|
|
16586
|
+
message: MESSAGE$39
|
|
16587
|
+
});
|
|
16588
|
+
} })
|
|
16589
|
+
});
|
|
16590
|
+
//#endregion
|
|
16328
16591
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
const MESSAGE$
|
|
16592
|
+
const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16330
16593
|
const resolveSettings$21 = (settings) => {
|
|
16331
16594
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16595
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,12 +16645,45 @@ const noAutofocus = defineRule({
|
|
|
16382
16645
|
}
|
|
16383
16646
|
context.report({
|
|
16384
16647
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16648
|
+
message: MESSAGE$38
|
|
16386
16649
|
});
|
|
16387
16650
|
} };
|
|
16388
16651
|
}
|
|
16389
16652
|
});
|
|
16390
16653
|
//#endregion
|
|
16654
|
+
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16655
|
+
const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
|
|
16656
|
+
const resolveStaticBoolean = (attribute) => {
|
|
16657
|
+
const value = attribute.value;
|
|
16658
|
+
if (!value) return true;
|
|
16659
|
+
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16660
|
+
if (isNodeOfType(literal, "Literal")) {
|
|
16661
|
+
if (literal.value === true || literal.value === "true") return true;
|
|
16662
|
+
if (literal.value === false || literal.value === "false") return false;
|
|
16663
|
+
}
|
|
16664
|
+
return null;
|
|
16665
|
+
};
|
|
16666
|
+
const noAutoplayWithoutMuted = defineRule({
|
|
16667
|
+
id: "no-autoplay-without-muted",
|
|
16668
|
+
title: "Autoplaying media without muted",
|
|
16669
|
+
severity: "warn",
|
|
16670
|
+
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16671
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
16672
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16673
|
+
const tagName = node.name.name;
|
|
16674
|
+
if (tagName !== "video" && tagName !== "audio") return;
|
|
16675
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16676
|
+
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16677
|
+
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16678
|
+
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16679
|
+
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16680
|
+
context.report({
|
|
16681
|
+
node: node.name,
|
|
16682
|
+
message: MESSAGE$37
|
|
16683
|
+
});
|
|
16684
|
+
} })
|
|
16685
|
+
});
|
|
16686
|
+
//#endregion
|
|
16391
16687
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16392
16688
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16393
16689
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -16632,6 +16928,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16928
|
}
|
|
16633
16929
|
});
|
|
16634
16930
|
//#endregion
|
|
16931
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16932
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16933
|
+
"FunctionDeclaration",
|
|
16934
|
+
"FunctionExpression",
|
|
16935
|
+
"ArrowFunctionExpression",
|
|
16936
|
+
"ClassDeclaration",
|
|
16937
|
+
"ClassExpression"
|
|
16938
|
+
]);
|
|
16939
|
+
const isReactImport$1 = (symbol) => {
|
|
16940
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16941
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16942
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16943
|
+
return importDeclaration.source.value === "react";
|
|
16944
|
+
};
|
|
16945
|
+
const getImportedName = (symbol) => {
|
|
16946
|
+
if (symbol.kind !== "import") return null;
|
|
16947
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16948
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16949
|
+
};
|
|
16950
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16951
|
+
if (symbol.kind !== "import") return false;
|
|
16952
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16953
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16954
|
+
};
|
|
16955
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16956
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16957
|
+
const symbol = scopes.symbolFor(callee);
|
|
16958
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16959
|
+
};
|
|
16960
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16961
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16962
|
+
if (callee.computed) return false;
|
|
16963
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16964
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16965
|
+
if (callee.property.name !== "createElement") return false;
|
|
16966
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16967
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16968
|
+
};
|
|
16969
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16970
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16971
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16972
|
+
};
|
|
16973
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16974
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16975
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16976
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16977
|
+
const nodeRecord = node;
|
|
16978
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16979
|
+
if (key === "parent") continue;
|
|
16980
|
+
const child = nodeRecord[key];
|
|
16981
|
+
if (Array.isArray(child)) {
|
|
16982
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16983
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16984
|
+
}
|
|
16985
|
+
return false;
|
|
16986
|
+
};
|
|
16987
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16988
|
+
//#endregion
|
|
16989
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16990
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16991
|
+
//#endregion
|
|
16992
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16993
|
+
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.`;
|
|
16994
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16995
|
+
const declaration = symbol.declarationNode;
|
|
16996
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16997
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16998
|
+
return false;
|
|
16999
|
+
};
|
|
17000
|
+
const noCallComponentAsFunction = defineRule({
|
|
17001
|
+
id: "no-call-component-as-function",
|
|
17002
|
+
title: "Component called as a function",
|
|
17003
|
+
severity: "warn",
|
|
17004
|
+
tags: ["test-noise"],
|
|
17005
|
+
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.",
|
|
17006
|
+
create: (context) => {
|
|
17007
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
17008
|
+
const candidateCalls = [];
|
|
17009
|
+
return {
|
|
17010
|
+
JSXOpeningElement(node) {
|
|
17011
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
17012
|
+
},
|
|
17013
|
+
CallExpression(node) {
|
|
17014
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
17015
|
+
node,
|
|
17016
|
+
callee: node.callee,
|
|
17017
|
+
name: node.callee.name
|
|
17018
|
+
});
|
|
17019
|
+
},
|
|
17020
|
+
"Program:exit"() {
|
|
17021
|
+
for (const candidate of candidateCalls) {
|
|
17022
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
17023
|
+
if (!symbol) continue;
|
|
17024
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
17025
|
+
node: candidate.node,
|
|
17026
|
+
message: message(candidate.name)
|
|
17027
|
+
});
|
|
17028
|
+
}
|
|
17029
|
+
}
|
|
17030
|
+
};
|
|
17031
|
+
}
|
|
17032
|
+
});
|
|
17033
|
+
//#endregion
|
|
16635
17034
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
17035
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
17036
|
//#endregion
|
|
@@ -16783,7 +17182,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
17182
|
});
|
|
16784
17183
|
//#endregion
|
|
16785
17184
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
const MESSAGE$
|
|
17185
|
+
const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16787
17186
|
const noChildrenProp = defineRule({
|
|
16788
17187
|
id: "no-children-prop",
|
|
16789
17188
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17194,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17194
|
if (node.name.name !== "children") return;
|
|
16796
17195
|
context.report({
|
|
16797
17196
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17197
|
+
message: MESSAGE$36
|
|
16799
17198
|
});
|
|
16800
17199
|
},
|
|
16801
17200
|
CallExpression(node) {
|
|
@@ -16808,7 +17207,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17207
|
const propertyKey = property.key;
|
|
16809
17208
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17209
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17210
|
+
message: MESSAGE$36
|
|
16812
17211
|
});
|
|
16813
17212
|
}
|
|
16814
17213
|
}
|
|
@@ -16816,7 +17215,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17215
|
});
|
|
16817
17216
|
//#endregion
|
|
16818
17217
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
const MESSAGE$
|
|
17218
|
+
const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16820
17219
|
const noCloneElement = defineRule({
|
|
16821
17220
|
id: "no-clone-element",
|
|
16822
17221
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17228,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17228
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17229
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17230
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17231
|
+
message: MESSAGE$35
|
|
16833
17232
|
});
|
|
16834
17233
|
return;
|
|
16835
17234
|
}
|
|
@@ -16842,7 +17241,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17241
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17242
|
context.report({
|
|
16844
17243
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17244
|
+
message: MESSAGE$35
|
|
16846
17245
|
});
|
|
16847
17246
|
}
|
|
16848
17247
|
} })
|
|
@@ -16891,7 +17290,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17290
|
};
|
|
16892
17291
|
//#endregion
|
|
16893
17292
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17293
|
+
const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17294
|
const CONTEXT_MODULES = [
|
|
16896
17295
|
"react",
|
|
16897
17296
|
"use-context-selector",
|
|
@@ -16927,7 +17326,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17326
|
if (!componentOrHookName) return;
|
|
16928
17327
|
context.report({
|
|
16929
17328
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17329
|
+
message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
|
|
17330
|
+
});
|
|
17331
|
+
} })
|
|
17332
|
+
});
|
|
17333
|
+
//#endregion
|
|
17334
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17335
|
+
const MESSAGE$33 = "`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.";
|
|
17336
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17337
|
+
id: "no-create-ref-in-function-component",
|
|
17338
|
+
title: "createRef in function component",
|
|
17339
|
+
severity: "warn",
|
|
17340
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17341
|
+
create: (context) => ({ CallExpression(node) {
|
|
17342
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17343
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17344
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17345
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17346
|
+
}
|
|
17347
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17348
|
+
if (!enclosingFunction) return;
|
|
17349
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17350
|
+
if (!displayName) return;
|
|
17351
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17352
|
+
context.report({
|
|
17353
|
+
node,
|
|
17354
|
+
message: MESSAGE$33
|
|
16931
17355
|
});
|
|
16932
17356
|
} })
|
|
16933
17357
|
});
|
|
@@ -17067,7 +17491,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17491
|
});
|
|
17068
17492
|
//#endregion
|
|
17069
17493
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17494
|
+
const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17495
|
const noDanger = defineRule({
|
|
17072
17496
|
id: "no-danger",
|
|
17073
17497
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17080,7 +17504,7 @@ const noDanger = defineRule({
|
|
|
17080
17504
|
if (!propAttribute) return;
|
|
17081
17505
|
context.report({
|
|
17082
17506
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17507
|
+
message: MESSAGE$32
|
|
17084
17508
|
});
|
|
17085
17509
|
},
|
|
17086
17510
|
CallExpression(node) {
|
|
@@ -17092,7 +17516,7 @@ const noDanger = defineRule({
|
|
|
17092
17516
|
const propertyKey = property.key;
|
|
17093
17517
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17518
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17519
|
+
message: MESSAGE$32
|
|
17096
17520
|
});
|
|
17097
17521
|
}
|
|
17098
17522
|
}
|
|
@@ -17100,7 +17524,7 @@ const noDanger = defineRule({
|
|
|
17100
17524
|
});
|
|
17101
17525
|
//#endregion
|
|
17102
17526
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17527
|
+
const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17528
|
const isLineBreak = (child) => {
|
|
17105
17529
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17530
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17594,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17594
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17595
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17596
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17597
|
+
message: MESSAGE$31
|
|
17174
17598
|
});
|
|
17175
17599
|
},
|
|
17176
17600
|
CallExpression(node) {
|
|
@@ -17182,7 +17606,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17606
|
if (!propsShape.hasDangerously) return;
|
|
17183
17607
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17608
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17609
|
+
message: MESSAGE$31
|
|
17186
17610
|
});
|
|
17187
17611
|
}
|
|
17188
17612
|
})
|
|
@@ -17347,6 +17771,37 @@ const noDefaultProps = defineRule({
|
|
|
17347
17771
|
} })
|
|
17348
17772
|
});
|
|
17349
17773
|
//#endregion
|
|
17774
|
+
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17775
|
+
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17776
|
+
//#endregion
|
|
17777
|
+
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17778
|
+
const renameDeprecatedToken = (token) => {
|
|
17779
|
+
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17780
|
+
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17781
|
+
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17782
|
+
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17783
|
+
return null;
|
|
17784
|
+
};
|
|
17785
|
+
const noDeprecatedTailwindClass = defineRule({
|
|
17786
|
+
id: "no-deprecated-tailwind-class",
|
|
17787
|
+
title: "Deprecated Tailwind v4 utility",
|
|
17788
|
+
tags: ["design", "test-noise"],
|
|
17789
|
+
severity: "warn",
|
|
17790
|
+
requires: ["tailwind:4"],
|
|
17791
|
+
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17792
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
17793
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
17794
|
+
if (!classNameValue) return;
|
|
17795
|
+
for (const token of getClassNameTokens(classNameValue)) {
|
|
17796
|
+
const replacement = renameDeprecatedToken(token);
|
|
17797
|
+
if (replacement) context.report({
|
|
17798
|
+
node,
|
|
17799
|
+
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17800
|
+
});
|
|
17801
|
+
}
|
|
17802
|
+
} })
|
|
17803
|
+
});
|
|
17804
|
+
//#endregion
|
|
17350
17805
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17351
17806
|
const isInitialOnlyPropName = (propName) => {
|
|
17352
17807
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -17759,7 +18214,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
18214
|
//#endregion
|
|
17760
18215
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
18216
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
18217
|
+
const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
18218
|
const resolveSettings$20 = (settings) => {
|
|
17764
18219
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
18220
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18233,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18233
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18234
|
context.report({
|
|
17780
18235
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18236
|
+
message: MESSAGE$30
|
|
17782
18237
|
});
|
|
17783
18238
|
} };
|
|
17784
18239
|
}
|
|
@@ -17786,7 +18241,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18241
|
//#endregion
|
|
17787
18242
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18243
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18244
|
+
const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18245
|
const resolveSettings$19 = (settings) => {
|
|
17791
18246
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18247
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18260,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18260
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18261
|
context.report({
|
|
17807
18262
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18263
|
+
message: MESSAGE$29
|
|
17809
18264
|
});
|
|
17810
18265
|
} };
|
|
17811
18266
|
}
|
|
@@ -17828,7 +18283,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18283
|
};
|
|
17829
18284
|
//#endregion
|
|
17830
18285
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18286
|
+
const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18287
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18288
|
let isConstructor = false;
|
|
17834
18289
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18305,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18305
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18306
|
context.report({
|
|
17852
18307
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18308
|
+
message: MESSAGE$28
|
|
17854
18309
|
});
|
|
17855
18310
|
};
|
|
17856
18311
|
const noDirectMutationState = defineRule({
|
|
@@ -18060,6 +18515,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18060
18515
|
} })
|
|
18061
18516
|
});
|
|
18062
18517
|
//#endregion
|
|
18518
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18519
|
+
const MESSAGE$27 = "`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.";
|
|
18520
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18521
|
+
const noDocumentWrite = defineRule({
|
|
18522
|
+
id: "no-document-write",
|
|
18523
|
+
title: "document.write/writeln",
|
|
18524
|
+
severity: "warn",
|
|
18525
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18526
|
+
create: (context) => ({ CallExpression(node) {
|
|
18527
|
+
const callee = node.callee;
|
|
18528
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18529
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18530
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18531
|
+
context.report({
|
|
18532
|
+
node,
|
|
18533
|
+
message: MESSAGE$27
|
|
18534
|
+
});
|
|
18535
|
+
} })
|
|
18536
|
+
});
|
|
18537
|
+
//#endregion
|
|
18063
18538
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18064
18539
|
const noDynamicImportPath = defineRule({
|
|
18065
18540
|
id: "no-dynamic-import-path",
|
|
@@ -19438,7 +19913,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19913
|
"ReactDOM",
|
|
19439
19914
|
"ReactDom"
|
|
19440
19915
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19916
|
+
const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19917
|
const noFindDomNode = defineRule({
|
|
19443
19918
|
id: "no-find-dom-node",
|
|
19444
19919
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19924,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19924
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19925
|
context.report({
|
|
19451
19926
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19927
|
+
message: MESSAGE$26
|
|
19453
19928
|
});
|
|
19454
19929
|
return;
|
|
19455
19930
|
}
|
|
@@ -19460,7 +19935,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19935
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19936
|
context.report({
|
|
19462
19937
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19938
|
+
message: MESSAGE$26
|
|
19464
19939
|
});
|
|
19465
19940
|
}
|
|
19466
19941
|
} })
|
|
@@ -19501,6 +19976,41 @@ const noFullLodashImport = defineRule({
|
|
|
19501
19976
|
} })
|
|
19502
19977
|
});
|
|
19503
19978
|
//#endregion
|
|
19979
|
+
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19980
|
+
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19981
|
+
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19982
|
+
const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
|
|
19983
|
+
const noFullViewportWidth = defineRule({
|
|
19984
|
+
id: "no-full-viewport-width",
|
|
19985
|
+
title: "Full viewport width causes overflow",
|
|
19986
|
+
tags: ["design", "test-noise"],
|
|
19987
|
+
severity: "warn",
|
|
19988
|
+
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19989
|
+
create: (context) => ({
|
|
19990
|
+
JSXAttribute(node) {
|
|
19991
|
+
const expression = getInlineStyleExpression(node);
|
|
19992
|
+
if (!expression) return;
|
|
19993
|
+
for (const property of expression.properties ?? []) {
|
|
19994
|
+
const key = getStylePropertyKey(property);
|
|
19995
|
+
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19996
|
+
const value = getStylePropertyStringValue(property);
|
|
19997
|
+
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19998
|
+
node: property,
|
|
19999
|
+
message: MESSAGE$25
|
|
20000
|
+
});
|
|
20001
|
+
}
|
|
20002
|
+
},
|
|
20003
|
+
JSXOpeningElement(node) {
|
|
20004
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
20005
|
+
if (!classNameValue) return;
|
|
20006
|
+
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
20007
|
+
node,
|
|
20008
|
+
message: MESSAGE$25
|
|
20009
|
+
});
|
|
20010
|
+
}
|
|
20011
|
+
})
|
|
20012
|
+
});
|
|
20013
|
+
//#endregion
|
|
19504
20014
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19505
20015
|
const noGenericHandlerNames = defineRule({
|
|
19506
20016
|
id: "no-generic-handler-names",
|
|
@@ -19523,64 +20033,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
20033
|
} })
|
|
19524
20034
|
});
|
|
19525
20035
|
//#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
20036
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
20037
|
const noGiantComponent = defineRule({
|
|
19586
20038
|
id: "no-giant-component",
|
|
@@ -19621,7 +20073,7 @@ const noGiantComponent = defineRule({
|
|
|
19621
20073
|
});
|
|
19622
20074
|
//#endregion
|
|
19623
20075
|
//#region src/plugin/constants/style.ts
|
|
19624
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
20076
|
+
const LAYOUT_PROPERTIES$1 = new Set([
|
|
19625
20077
|
"width",
|
|
19626
20078
|
"height",
|
|
19627
20079
|
"top",
|
|
@@ -19691,17 +20143,6 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
19691
20143
|
} })
|
|
19692
20144
|
});
|
|
19693
20145
|
//#endregion
|
|
19694
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
19695
|
-
const getStringFromClassNameAttr = (node) => {
|
|
19696
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
19697
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
19698
|
-
if (!classAttr?.value) return null;
|
|
19699
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
19700
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
19701
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
19702
|
-
return null;
|
|
19703
|
-
};
|
|
19704
|
-
//#endregion
|
|
19705
20146
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
19706
20147
|
const noGradientText = defineRule({
|
|
19707
20148
|
id: "no-gradient-text",
|
|
@@ -19759,6 +20200,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
20200
|
} })
|
|
19760
20201
|
});
|
|
19761
20202
|
//#endregion
|
|
20203
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20204
|
+
const MESSAGE$24 = "`<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.";
|
|
20205
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20206
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20207
|
+
title: "Lazy image with high fetchPriority",
|
|
20208
|
+
severity: "warn",
|
|
20209
|
+
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.",
|
|
20210
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20211
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20212
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20213
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20214
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20215
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20216
|
+
context.report({
|
|
20217
|
+
node: node.name,
|
|
20218
|
+
message: MESSAGE$24
|
|
20219
|
+
});
|
|
20220
|
+
} })
|
|
20221
|
+
});
|
|
20222
|
+
//#endregion
|
|
19762
20223
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
20224
|
const noInitializeState = defineRule({
|
|
19764
20225
|
id: "no-initialize-state",
|
|
@@ -19988,8 +20449,31 @@ const noIsMounted = defineRule({
|
|
|
19988
20449
|
} })
|
|
19989
20450
|
});
|
|
19990
20451
|
//#endregion
|
|
20452
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20453
|
+
const MESSAGE$23 = "`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)`.";
|
|
20454
|
+
const isJsonMethodCall = (node, method) => {
|
|
20455
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20456
|
+
const callee = node.callee;
|
|
20457
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20458
|
+
};
|
|
20459
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20460
|
+
id: "no-json-parse-stringify-clone",
|
|
20461
|
+
title: "JSON parse/stringify deep clone",
|
|
20462
|
+
severity: "warn",
|
|
20463
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20464
|
+
create: (context) => ({ CallExpression(node) {
|
|
20465
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20466
|
+
const firstArgument = node.arguments?.[0];
|
|
20467
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20468
|
+
context.report({
|
|
20469
|
+
node,
|
|
20470
|
+
message: MESSAGE$23
|
|
20471
|
+
});
|
|
20472
|
+
} })
|
|
20473
|
+
});
|
|
20474
|
+
//#endregion
|
|
19991
20475
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
|
-
const MESSAGE$
|
|
20476
|
+
const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19993
20477
|
const isJsxElementTypeReference = (node) => {
|
|
19994
20478
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
19995
20479
|
const typeName = node.typeName;
|
|
@@ -20006,7 +20490,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20006
20490
|
if (!typeAnnotation) return;
|
|
20007
20491
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20008
20492
|
node: typeAnnotation,
|
|
20009
|
-
message: MESSAGE$
|
|
20493
|
+
message: MESSAGE$22
|
|
20010
20494
|
});
|
|
20011
20495
|
};
|
|
20012
20496
|
const noJsxElementType = defineRule({
|
|
@@ -20116,7 +20600,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20116
20600
|
let propertyName = null;
|
|
20117
20601
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20118
20602
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20119
|
-
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20603
|
+
if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
|
|
20120
20604
|
node: property,
|
|
20121
20605
|
message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
|
|
20122
20606
|
});
|
|
@@ -20306,13 +20790,138 @@ const noLongTransitionDuration = defineRule({
|
|
|
20306
20790
|
} })
|
|
20307
20791
|
});
|
|
20308
20792
|
//#endregion
|
|
20793
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20794
|
+
const getStylePropertyNumberValue = (property) => {
|
|
20795
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
20796
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20797
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20798
|
+
return null;
|
|
20799
|
+
};
|
|
20800
|
+
//#endregion
|
|
20801
|
+
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20802
|
+
const linearizeChannel = (channel) => {
|
|
20803
|
+
const normalized = channel / 255;
|
|
20804
|
+
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20805
|
+
};
|
|
20806
|
+
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20807
|
+
const getWcagContrastRatio = (foreground, background) => {
|
|
20808
|
+
const foregroundLuminance = relativeLuminance(foreground);
|
|
20809
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
20810
|
+
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20811
|
+
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20812
|
+
return (lighter + .05) / (darker + .05);
|
|
20813
|
+
};
|
|
20814
|
+
//#endregion
|
|
20815
|
+
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20816
|
+
const UNRESOLVABLE = new Set([
|
|
20817
|
+
"transparent",
|
|
20818
|
+
"currentcolor",
|
|
20819
|
+
"inherit",
|
|
20820
|
+
"initial",
|
|
20821
|
+
"unset",
|
|
20822
|
+
"revert",
|
|
20823
|
+
"none"
|
|
20824
|
+
]);
|
|
20825
|
+
const resolveOpaqueColor = (raw) => {
|
|
20826
|
+
const value = raw.trim().toLowerCase();
|
|
20827
|
+
if (UNRESOLVABLE.has(value)) return null;
|
|
20828
|
+
if (value === "white") return {
|
|
20829
|
+
red: 255,
|
|
20830
|
+
green: 255,
|
|
20831
|
+
blue: 255
|
|
20832
|
+
};
|
|
20833
|
+
if (value === "black") return {
|
|
20834
|
+
red: 0,
|
|
20835
|
+
green: 0,
|
|
20836
|
+
blue: 0
|
|
20837
|
+
};
|
|
20838
|
+
if (value.startsWith("var(")) return null;
|
|
20839
|
+
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20840
|
+
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20841
|
+
if (value.startsWith("rgb")) {
|
|
20842
|
+
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20843
|
+
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20844
|
+
}
|
|
20845
|
+
return parseColorToRgb(value);
|
|
20846
|
+
};
|
|
20847
|
+
const toPx = (property) => {
|
|
20848
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20849
|
+
if (numberValue !== null) return numberValue;
|
|
20850
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20851
|
+
if (stringValue === null) return null;
|
|
20852
|
+
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20853
|
+
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20854
|
+
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20855
|
+
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20856
|
+
return null;
|
|
20857
|
+
};
|
|
20858
|
+
const isBoldWeight = (property) => {
|
|
20859
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20860
|
+
if (numberValue !== null) return numberValue >= 700;
|
|
20861
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20862
|
+
if (stringValue === null) return false;
|
|
20863
|
+
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20864
|
+
const numericWeight = Number(stringValue);
|
|
20865
|
+
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20866
|
+
};
|
|
20867
|
+
const noLowContrastInlineStyle = defineRule({
|
|
20868
|
+
id: "no-low-contrast-inline-style",
|
|
20869
|
+
title: "Low-contrast text in inline style",
|
|
20870
|
+
tags: ["test-noise"],
|
|
20871
|
+
severity: "warn",
|
|
20872
|
+
category: "Accessibility",
|
|
20873
|
+
recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
|
|
20874
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
20875
|
+
const expression = getInlineStyleExpression(node);
|
|
20876
|
+
if (!expression) return;
|
|
20877
|
+
const properties = expression.properties ?? [];
|
|
20878
|
+
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20879
|
+
let foreground = null;
|
|
20880
|
+
let backgroundColorRaw = null;
|
|
20881
|
+
let backgroundShorthandRaw = null;
|
|
20882
|
+
let backgroundIsUnknown = false;
|
|
20883
|
+
let fontSizePx = null;
|
|
20884
|
+
let isBold = false;
|
|
20885
|
+
for (const property of properties) {
|
|
20886
|
+
const key = getStylePropertyKey(property);
|
|
20887
|
+
if (!key) continue;
|
|
20888
|
+
if (key === "backgroundImage") {
|
|
20889
|
+
backgroundIsUnknown = true;
|
|
20890
|
+
continue;
|
|
20891
|
+
}
|
|
20892
|
+
if (key === "fontSize" && property.type === "Property") {
|
|
20893
|
+
fontSizePx = toPx(property);
|
|
20894
|
+
continue;
|
|
20895
|
+
}
|
|
20896
|
+
if (key === "fontWeight" && property.type === "Property") {
|
|
20897
|
+
isBold = isBoldWeight(property);
|
|
20898
|
+
continue;
|
|
20899
|
+
}
|
|
20900
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20901
|
+
if (key === "color") {
|
|
20902
|
+
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20903
|
+
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20904
|
+
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20905
|
+
else backgroundShorthandRaw = stringValue;
|
|
20906
|
+
}
|
|
20907
|
+
if (backgroundIsUnknown) return;
|
|
20908
|
+
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20909
|
+
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20910
|
+
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20911
|
+
if (!foreground || !background) return;
|
|
20912
|
+
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20913
|
+
const ratio = getWcagContrastRatio(foreground, background);
|
|
20914
|
+
if (ratio < threshold) context.report({
|
|
20915
|
+
node,
|
|
20916
|
+
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
20917
|
+
});
|
|
20918
|
+
} })
|
|
20919
|
+
});
|
|
20920
|
+
//#endregion
|
|
20309
20921
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20310
20922
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20923
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20924
|
//#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
20925
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20926
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20927
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -20464,7 +21073,7 @@ const noMoment = defineRule({
|
|
|
20464
21073
|
});
|
|
20465
21074
|
//#endregion
|
|
20466
21075
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20467
|
-
const MESSAGE$
|
|
21076
|
+
const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20468
21077
|
const resolveSettings$16 = (settings) => {
|
|
20469
21078
|
const reactDoctor = settings?.["react-doctor"];
|
|
20470
21079
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -20786,7 +21395,7 @@ const noMultiComp = defineRule({
|
|
|
20786
21395
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
20787
21396
|
for (const component of flagged.slice(1)) context.report({
|
|
20788
21397
|
node: component.reportNode,
|
|
20789
|
-
message: MESSAGE$
|
|
21398
|
+
message: MESSAGE$21
|
|
20790
21399
|
});
|
|
20791
21400
|
} };
|
|
20792
21401
|
}
|
|
@@ -20954,7 +21563,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
20954
21563
|
};
|
|
20955
21564
|
//#endregion
|
|
20956
21565
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
20957
|
-
const MESSAGE$
|
|
21566
|
+
const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
|
|
20958
21567
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
20959
21568
|
"copyWithin",
|
|
20960
21569
|
"fill",
|
|
@@ -21164,7 +21773,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21164
21773
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21165
21774
|
context.report({
|
|
21166
21775
|
node: options.crossFileConsumerCallSite,
|
|
21167
|
-
message: `${MESSAGE$
|
|
21776
|
+
message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21168
21777
|
});
|
|
21169
21778
|
return;
|
|
21170
21779
|
}
|
|
@@ -21173,7 +21782,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21173
21782
|
reportedNodes.add(mutation.node);
|
|
21174
21783
|
context.report({
|
|
21175
21784
|
node: mutation.node,
|
|
21176
|
-
message: MESSAGE$
|
|
21785
|
+
message: MESSAGE$20
|
|
21177
21786
|
});
|
|
21178
21787
|
}
|
|
21179
21788
|
};
|
|
@@ -21445,7 +22054,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21445
22054
|
});
|
|
21446
22055
|
//#endregion
|
|
21447
22056
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21448
|
-
const MESSAGE$
|
|
22057
|
+
const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21449
22058
|
const resolveSettings$14 = (settings) => {
|
|
21450
22059
|
const reactDoctor = settings?.["react-doctor"];
|
|
21451
22060
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21473,7 +22082,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21473
22082
|
if (numeric === null) {
|
|
21474
22083
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21475
22084
|
node: tabIndex,
|
|
21476
|
-
message: MESSAGE$
|
|
22085
|
+
message: MESSAGE$19
|
|
21477
22086
|
});
|
|
21478
22087
|
return;
|
|
21479
22088
|
}
|
|
@@ -21486,7 +22095,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21486
22095
|
if (!roleAttribute) {
|
|
21487
22096
|
context.report({
|
|
21488
22097
|
node: tabIndex,
|
|
21489
|
-
message: MESSAGE$
|
|
22098
|
+
message: MESSAGE$19
|
|
21490
22099
|
});
|
|
21491
22100
|
return;
|
|
21492
22101
|
}
|
|
@@ -21500,20 +22109,12 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21500
22109
|
}
|
|
21501
22110
|
context.report({
|
|
21502
22111
|
node: tabIndex,
|
|
21503
|
-
message: MESSAGE$
|
|
22112
|
+
message: MESSAGE$19
|
|
21504
22113
|
});
|
|
21505
22114
|
} };
|
|
21506
22115
|
}
|
|
21507
22116
|
});
|
|
21508
22117
|
//#endregion
|
|
21509
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21510
|
-
const getStylePropertyNumberValue = (property) => {
|
|
21511
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
21512
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21513
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21514
|
-
return null;
|
|
21515
|
-
};
|
|
21516
|
-
//#endregion
|
|
21517
22118
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
21518
22119
|
const noOutlineNone = defineRule({
|
|
21519
22120
|
id: "no-outline-none",
|
|
@@ -22191,7 +22792,7 @@ const noRandomKey = defineRule({
|
|
|
22191
22792
|
});
|
|
22192
22793
|
//#endregion
|
|
22193
22794
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22194
|
-
const MESSAGE$
|
|
22795
|
+
const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22195
22796
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22196
22797
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22197
22798
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22217,13 +22818,13 @@ const noReactChildren = defineRule({
|
|
|
22217
22818
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22218
22819
|
context.report({
|
|
22219
22820
|
node: calleeOuter,
|
|
22220
|
-
message: MESSAGE$
|
|
22821
|
+
message: MESSAGE$18
|
|
22221
22822
|
});
|
|
22222
22823
|
return;
|
|
22223
22824
|
}
|
|
22224
22825
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22225
22826
|
node: calleeOuter,
|
|
22226
|
-
message: MESSAGE$
|
|
22827
|
+
message: MESSAGE$18
|
|
22227
22828
|
});
|
|
22228
22829
|
} })
|
|
22229
22830
|
});
|
|
@@ -22334,6 +22935,86 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22334
22935
|
})
|
|
22335
22936
|
});
|
|
22336
22937
|
//#endregion
|
|
22938
|
+
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22939
|
+
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22940
|
+
"div",
|
|
22941
|
+
"p",
|
|
22942
|
+
"section",
|
|
22943
|
+
"article",
|
|
22944
|
+
"main",
|
|
22945
|
+
"header",
|
|
22946
|
+
"footer",
|
|
22947
|
+
"nav",
|
|
22948
|
+
"aside",
|
|
22949
|
+
"figure",
|
|
22950
|
+
"figcaption",
|
|
22951
|
+
"blockquote",
|
|
22952
|
+
"form",
|
|
22953
|
+
"fieldset",
|
|
22954
|
+
"address",
|
|
22955
|
+
"pre",
|
|
22956
|
+
"ul",
|
|
22957
|
+
"ol",
|
|
22958
|
+
"dl",
|
|
22959
|
+
"dt",
|
|
22960
|
+
"dd",
|
|
22961
|
+
"h1",
|
|
22962
|
+
"h2",
|
|
22963
|
+
"h3",
|
|
22964
|
+
"h4",
|
|
22965
|
+
"h5",
|
|
22966
|
+
"h6"
|
|
22967
|
+
]);
|
|
22968
|
+
const INLINE_DEFAULT_TAGS = new Set([
|
|
22969
|
+
"span",
|
|
22970
|
+
"a",
|
|
22971
|
+
"b",
|
|
22972
|
+
"i",
|
|
22973
|
+
"em",
|
|
22974
|
+
"strong",
|
|
22975
|
+
"small",
|
|
22976
|
+
"code",
|
|
22977
|
+
"abbr",
|
|
22978
|
+
"cite",
|
|
22979
|
+
"label",
|
|
22980
|
+
"mark",
|
|
22981
|
+
"q",
|
|
22982
|
+
"s",
|
|
22983
|
+
"u",
|
|
22984
|
+
"sub",
|
|
22985
|
+
"sup",
|
|
22986
|
+
"kbd",
|
|
22987
|
+
"samp",
|
|
22988
|
+
"var",
|
|
22989
|
+
"time"
|
|
22990
|
+
]);
|
|
22991
|
+
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22992
|
+
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22993
|
+
const noRedundantDisplayClass = defineRule({
|
|
22994
|
+
id: "no-redundant-display-class",
|
|
22995
|
+
title: "Redundant display utility",
|
|
22996
|
+
tags: ["design", "test-noise"],
|
|
22997
|
+
severity: "warn",
|
|
22998
|
+
recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
|
|
22999
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23000
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23001
|
+
const tagName = node.name.name;
|
|
23002
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
23003
|
+
if (!classNameValue) return;
|
|
23004
|
+
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
23005
|
+
context.report({
|
|
23006
|
+
node,
|
|
23007
|
+
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23008
|
+
});
|
|
23009
|
+
return;
|
|
23010
|
+
}
|
|
23011
|
+
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
23012
|
+
node,
|
|
23013
|
+
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
23014
|
+
});
|
|
23015
|
+
} })
|
|
23016
|
+
});
|
|
23017
|
+
//#endregion
|
|
22337
23018
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22338
23019
|
const ELEMENT_ROLE_PAIRS = [
|
|
22339
23020
|
["a", "link"],
|
|
@@ -22546,7 +23227,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22546
23227
|
});
|
|
22547
23228
|
//#endregion
|
|
22548
23229
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22549
|
-
const MESSAGE$
|
|
23230
|
+
const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22550
23231
|
const isReactDomRenderCall = (node) => {
|
|
22551
23232
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22552
23233
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22570,7 +23251,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22570
23251
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22571
23252
|
context.report({
|
|
22572
23253
|
node: node.callee,
|
|
22573
|
-
message: MESSAGE$
|
|
23254
|
+
message: MESSAGE$17
|
|
22574
23255
|
});
|
|
22575
23256
|
} })
|
|
22576
23257
|
});
|
|
@@ -22730,11 +23411,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22730
23411
|
return "unknown";
|
|
22731
23412
|
};
|
|
22732
23413
|
//#endregion
|
|
22733
|
-
//#region src/plugin/utils/
|
|
22734
|
-
const
|
|
22735
|
-
|
|
23414
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23415
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23416
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23417
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23418
|
+
if (!words) return [];
|
|
23419
|
+
return words.map((word) => word.toLowerCase());
|
|
22736
23420
|
};
|
|
22737
23421
|
//#endregion
|
|
23422
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23423
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23424
|
+
//#endregion
|
|
22738
23425
|
//#region src/plugin/constants/tanstack.ts
|
|
22739
23426
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22740
23427
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23262,7 +23949,7 @@ const getParentComponent = (node) => {
|
|
|
23262
23949
|
};
|
|
23263
23950
|
//#endregion
|
|
23264
23951
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23265
|
-
const MESSAGE$
|
|
23952
|
+
const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23266
23953
|
const noSetState = defineRule({
|
|
23267
23954
|
id: "no-set-state",
|
|
23268
23955
|
title: "Local class state forbidden",
|
|
@@ -23277,7 +23964,7 @@ const noSetState = defineRule({
|
|
|
23277
23964
|
if (!getParentComponent(node)) return;
|
|
23278
23965
|
context.report({
|
|
23279
23966
|
node: node.callee,
|
|
23280
|
-
message: MESSAGE$
|
|
23967
|
+
message: MESSAGE$16
|
|
23281
23968
|
});
|
|
23282
23969
|
} })
|
|
23283
23970
|
});
|
|
@@ -23439,7 +24126,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23439
24126
|
};
|
|
23440
24127
|
//#endregion
|
|
23441
24128
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23442
|
-
const MESSAGE$
|
|
24129
|
+
const MESSAGE$15 = "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
24130
|
const DEFAULT_HANDLERS = [
|
|
23444
24131
|
"onClick",
|
|
23445
24132
|
"onMouseDown",
|
|
@@ -23499,7 +24186,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23499
24186
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23500
24187
|
context.report({
|
|
23501
24188
|
node: node.name,
|
|
23502
|
-
message: MESSAGE$
|
|
24189
|
+
message: MESSAGE$15
|
|
23503
24190
|
});
|
|
23504
24191
|
return;
|
|
23505
24192
|
}
|
|
@@ -23509,19 +24196,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23509
24196
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23510
24197
|
context.report({
|
|
23511
24198
|
node: node.name,
|
|
23512
|
-
message: MESSAGE$
|
|
24199
|
+
message: MESSAGE$15
|
|
23513
24200
|
});
|
|
23514
24201
|
return;
|
|
23515
24202
|
}
|
|
23516
24203
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23517
24204
|
context.report({
|
|
23518
24205
|
node: node.name,
|
|
23519
|
-
message: MESSAGE$
|
|
24206
|
+
message: MESSAGE$15
|
|
23520
24207
|
});
|
|
23521
24208
|
} };
|
|
23522
24209
|
}
|
|
23523
24210
|
});
|
|
23524
24211
|
//#endregion
|
|
24212
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
24213
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
24214
|
+
"disabled",
|
|
24215
|
+
"checked",
|
|
24216
|
+
"readonly",
|
|
24217
|
+
"required",
|
|
24218
|
+
"selected",
|
|
24219
|
+
"multiple",
|
|
24220
|
+
"autofocus",
|
|
24221
|
+
"autoplay",
|
|
24222
|
+
"controls",
|
|
24223
|
+
"loop",
|
|
24224
|
+
"muted",
|
|
24225
|
+
"open",
|
|
24226
|
+
"reversed",
|
|
24227
|
+
"default",
|
|
24228
|
+
"novalidate",
|
|
24229
|
+
"formnovalidate",
|
|
24230
|
+
"playsinline",
|
|
24231
|
+
"itemscope",
|
|
24232
|
+
"allowfullscreen"
|
|
24233
|
+
]);
|
|
24234
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
24235
|
+
id: "no-string-false-on-boolean-attribute",
|
|
24236
|
+
title: "String true/false on a boolean attribute",
|
|
24237
|
+
severity: "warn",
|
|
24238
|
+
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.",
|
|
24239
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24240
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24241
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
24242
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
24243
|
+
for (const attribute of node.attributes) {
|
|
24244
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24245
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
24246
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
24247
|
+
const value = getJsxPropStringValue(attribute);
|
|
24248
|
+
if (value !== "false" && value !== "true") continue;
|
|
24249
|
+
const attributeName = attribute.name.name;
|
|
24250
|
+
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}\``;
|
|
24251
|
+
context.report({
|
|
24252
|
+
node: attribute,
|
|
24253
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
24254
|
+
});
|
|
24255
|
+
}
|
|
24256
|
+
} })
|
|
24257
|
+
});
|
|
24258
|
+
//#endregion
|
|
23525
24259
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23526
24260
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23527
24261
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23572,8 +24306,154 @@ const noStringRefs = defineRule({
|
|
|
23572
24306
|
}
|
|
23573
24307
|
});
|
|
23574
24308
|
//#endregion
|
|
24309
|
+
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24310
|
+
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24311
|
+
if (token.includes(":")) return false;
|
|
24312
|
+
if (!token.startsWith(prefix)) return false;
|
|
24313
|
+
const value = token.slice(prefix.length);
|
|
24314
|
+
if (value === "" || value === "current") return false;
|
|
24315
|
+
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24316
|
+
return true;
|
|
24317
|
+
});
|
|
24318
|
+
const isCurrentColor = (attribute) => {
|
|
24319
|
+
const value = getJsxPropStringValue(attribute);
|
|
24320
|
+
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24321
|
+
};
|
|
24322
|
+
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24323
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
24324
|
+
title: "currentColor fights a fill/stroke class",
|
|
24325
|
+
tags: ["design", "test-noise"],
|
|
24326
|
+
severity: "warn",
|
|
24327
|
+
recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
|
|
24328
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24329
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24330
|
+
if (!classNameValue) return;
|
|
24331
|
+
for (const paint of ["fill", "stroke"]) {
|
|
24332
|
+
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24333
|
+
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24334
|
+
context.report({
|
|
24335
|
+
node: attribute,
|
|
24336
|
+
message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
|
|
24337
|
+
});
|
|
24338
|
+
return;
|
|
24339
|
+
}
|
|
24340
|
+
}
|
|
24341
|
+
} })
|
|
24342
|
+
});
|
|
24343
|
+
//#endregion
|
|
24344
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24345
|
+
const MESSAGE$14 = "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)`).";
|
|
24346
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24347
|
+
const noSyncXhr = defineRule({
|
|
24348
|
+
id: "no-sync-xhr",
|
|
24349
|
+
title: "Synchronous XMLHttpRequest",
|
|
24350
|
+
severity: "warn",
|
|
24351
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24352
|
+
create: (context) => ({ CallExpression(node) {
|
|
24353
|
+
const callee = node.callee;
|
|
24354
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24355
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24356
|
+
const asyncArgument = node.arguments?.[2];
|
|
24357
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24358
|
+
context.report({
|
|
24359
|
+
node,
|
|
24360
|
+
message: MESSAGE$14
|
|
24361
|
+
});
|
|
24362
|
+
} })
|
|
24363
|
+
});
|
|
24364
|
+
//#endregion
|
|
24365
|
+
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24366
|
+
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24367
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
24368
|
+
"width",
|
|
24369
|
+
"height",
|
|
24370
|
+
"min-width",
|
|
24371
|
+
"max-width",
|
|
24372
|
+
"min-height",
|
|
24373
|
+
"max-height",
|
|
24374
|
+
"top",
|
|
24375
|
+
"left",
|
|
24376
|
+
"right",
|
|
24377
|
+
"bottom",
|
|
24378
|
+
"inset",
|
|
24379
|
+
"inset-block",
|
|
24380
|
+
"inset-inline",
|
|
24381
|
+
"margin",
|
|
24382
|
+
"margin-top",
|
|
24383
|
+
"margin-right",
|
|
24384
|
+
"margin-bottom",
|
|
24385
|
+
"margin-left",
|
|
24386
|
+
"margin-block",
|
|
24387
|
+
"margin-inline",
|
|
24388
|
+
"padding",
|
|
24389
|
+
"padding-top",
|
|
24390
|
+
"padding-right",
|
|
24391
|
+
"padding-bottom",
|
|
24392
|
+
"padding-left",
|
|
24393
|
+
"padding-block",
|
|
24394
|
+
"padding-inline"
|
|
24395
|
+
]);
|
|
24396
|
+
const noTailwindLayoutTransition = defineRule({
|
|
24397
|
+
id: "no-tailwind-layout-transition",
|
|
24398
|
+
title: "Animating a layout property",
|
|
24399
|
+
tags: ["design", "test-noise"],
|
|
24400
|
+
severity: "warn",
|
|
24401
|
+
category: "Performance",
|
|
24402
|
+
recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
|
|
24403
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24404
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24405
|
+
if (!classNameValue) return;
|
|
24406
|
+
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24407
|
+
const animatedProperties = transitionMatch[1];
|
|
24408
|
+
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24409
|
+
if (layoutProperty) context.report({
|
|
24410
|
+
node,
|
|
24411
|
+
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24412
|
+
});
|
|
24413
|
+
}
|
|
24414
|
+
} })
|
|
24415
|
+
});
|
|
24416
|
+
//#endregion
|
|
24417
|
+
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24418
|
+
const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
|
|
24419
|
+
const targetIsBlank = (attribute) => {
|
|
24420
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
24421
|
+
if (stringValue !== null) return stringValue === "_blank";
|
|
24422
|
+
const value = attribute.value;
|
|
24423
|
+
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24424
|
+
const expression = value.expression;
|
|
24425
|
+
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24426
|
+
}
|
|
24427
|
+
return false;
|
|
24428
|
+
};
|
|
24429
|
+
const noTargetBlankWithoutRel = defineRule({
|
|
24430
|
+
id: "no-target-blank-without-rel",
|
|
24431
|
+
title: "target=_blank without rel=noopener",
|
|
24432
|
+
severity: "warn",
|
|
24433
|
+
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24434
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24435
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24436
|
+
const tagName = node.name.name;
|
|
24437
|
+
if (tagName !== "a" && tagName !== "area") return;
|
|
24438
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24439
|
+
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24440
|
+
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24441
|
+
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24442
|
+
if (relAttribute) {
|
|
24443
|
+
const relValue = getJsxPropStringValue(relAttribute);
|
|
24444
|
+
if (relValue === null) return;
|
|
24445
|
+
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24446
|
+
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24447
|
+
}
|
|
24448
|
+
context.report({
|
|
24449
|
+
node: node.name,
|
|
24450
|
+
message: MESSAGE$13
|
|
24451
|
+
});
|
|
24452
|
+
} })
|
|
24453
|
+
});
|
|
24454
|
+
//#endregion
|
|
23575
24455
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23576
|
-
const MESSAGE$
|
|
24456
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
23577
24457
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
23578
24458
|
let ancestor = node.parent;
|
|
23579
24459
|
while (ancestor) {
|
|
@@ -23642,7 +24522,7 @@ const noThisInSfc = defineRule({
|
|
|
23642
24522
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
23643
24523
|
context.report({
|
|
23644
24524
|
node,
|
|
23645
|
-
message: MESSAGE$
|
|
24525
|
+
message: MESSAGE$12
|
|
23646
24526
|
});
|
|
23647
24527
|
} };
|
|
23648
24528
|
}
|
|
@@ -23680,26 +24560,39 @@ const noTinyText = defineRule({
|
|
|
23680
24560
|
});
|
|
23681
24561
|
//#endregion
|
|
23682
24562
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24563
|
+
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24564
|
+
const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
|
|
23683
24565
|
const noTransitionAll = defineRule({
|
|
23684
24566
|
id: "no-transition-all",
|
|
23685
24567
|
title: "transition: all animates everything",
|
|
23686
24568
|
tags: ["test-noise"],
|
|
23687
24569
|
severity: "warn",
|
|
23688
24570
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
23689
|
-
create: (context) => ({
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
|
|
23695
|
-
|
|
23696
|
-
|
|
23697
|
-
|
|
23698
|
-
|
|
23699
|
-
|
|
24571
|
+
create: (context) => ({
|
|
24572
|
+
JSXAttribute(node) {
|
|
24573
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24574
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24575
|
+
const expression = node.value.expression;
|
|
24576
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24577
|
+
for (const property of expression.properties ?? []) {
|
|
24578
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24579
|
+
const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
24580
|
+
if (key !== "transition" && key !== "transitionProperty") continue;
|
|
24581
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
|
|
24582
|
+
node: property,
|
|
24583
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24584
|
+
});
|
|
24585
|
+
}
|
|
24586
|
+
},
|
|
24587
|
+
JSXOpeningElement(node) {
|
|
24588
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24589
|
+
if (!classNameValue) return;
|
|
24590
|
+
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24591
|
+
node,
|
|
24592
|
+
message: TAILWIND_MESSAGE
|
|
23700
24593
|
});
|
|
23701
24594
|
}
|
|
23702
|
-
}
|
|
24595
|
+
})
|
|
23703
24596
|
});
|
|
23704
24597
|
//#endregion
|
|
23705
24598
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -23743,7 +24636,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
23743
24636
|
}
|
|
23744
24637
|
return stateNames;
|
|
23745
24638
|
};
|
|
23746
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
23747
24639
|
const noUncontrolledInput = defineRule({
|
|
23748
24640
|
id: "no-uncontrolled-input",
|
|
23749
24641
|
title: "Uncontrolled input value",
|
|
@@ -23847,6 +24739,38 @@ const noUnescapedEntities = defineRule({
|
|
|
23847
24739
|
} })
|
|
23848
24740
|
});
|
|
23849
24741
|
//#endregion
|
|
24742
|
+
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24743
|
+
const UNINFORMATIVE_LABELS = new Set([
|
|
24744
|
+
"icon",
|
|
24745
|
+
"button",
|
|
24746
|
+
"image",
|
|
24747
|
+
"img",
|
|
24748
|
+
"link",
|
|
24749
|
+
"graphic",
|
|
24750
|
+
"svg",
|
|
24751
|
+
"picture",
|
|
24752
|
+
"element",
|
|
24753
|
+
"field",
|
|
24754
|
+
"input"
|
|
24755
|
+
]);
|
|
24756
|
+
const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
|
|
24757
|
+
const noUninformativeAriaLabel = defineRule({
|
|
24758
|
+
id: "no-uninformative-aria-label",
|
|
24759
|
+
title: "Uninformative aria-label",
|
|
24760
|
+
severity: "warn",
|
|
24761
|
+
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24762
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24763
|
+
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24764
|
+
if (!ariaLabel) return;
|
|
24765
|
+
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24766
|
+
if (labelValue === null) return;
|
|
24767
|
+
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24768
|
+
node: ariaLabel,
|
|
24769
|
+
message: MESSAGE$11
|
|
24770
|
+
});
|
|
24771
|
+
} })
|
|
24772
|
+
});
|
|
24773
|
+
//#endregion
|
|
23850
24774
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
23851
24775
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
23852
24776
|
"activedescendant",
|
|
@@ -25318,7 +26242,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
25318
26242
|
//#endregion
|
|
25319
26243
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
25320
26244
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
25321
|
-
const MESSAGE$
|
|
26245
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
25322
26246
|
const resolveSettings$7 = (settings) => {
|
|
25323
26247
|
const reactDoctor = settings?.["react-doctor"];
|
|
25324
26248
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -25352,7 +26276,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
25352
26276
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25353
26277
|
context.report({
|
|
25354
26278
|
node: node.callee,
|
|
25355
|
-
message: MESSAGE$
|
|
26279
|
+
message: MESSAGE$10
|
|
25356
26280
|
});
|
|
25357
26281
|
} };
|
|
25358
26282
|
}
|
|
@@ -26230,7 +27154,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
26230
27154
|
});
|
|
26231
27155
|
//#endregion
|
|
26232
27156
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
26233
|
-
const MESSAGE$
|
|
27157
|
+
const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
26234
27158
|
const preactPreferOndblclick = defineRule({
|
|
26235
27159
|
id: "preact-prefer-ondblclick",
|
|
26236
27160
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -26245,7 +27169,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
26245
27169
|
if (!onDoubleClickAttribute) return;
|
|
26246
27170
|
context.report({
|
|
26247
27171
|
node: onDoubleClickAttribute,
|
|
26248
|
-
message: MESSAGE$
|
|
27172
|
+
message: MESSAGE$9
|
|
26249
27173
|
});
|
|
26250
27174
|
} })
|
|
26251
27175
|
});
|
|
@@ -26285,6 +27209,42 @@ const preactPreferOninput = defineRule({
|
|
|
26285
27209
|
} })
|
|
26286
27210
|
});
|
|
26287
27211
|
//#endregion
|
|
27212
|
+
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27213
|
+
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27214
|
+
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27215
|
+
const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
|
|
27216
|
+
const preferDvhOverVh = defineRule({
|
|
27217
|
+
id: "prefer-dvh-over-vh",
|
|
27218
|
+
title: "Use dvh instead of vh for full height",
|
|
27219
|
+
tags: ["design", "test-noise"],
|
|
27220
|
+
severity: "warn",
|
|
27221
|
+
requires: ["tailwind:3.4"],
|
|
27222
|
+
recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
|
|
27223
|
+
create: (context) => ({
|
|
27224
|
+
JSXAttribute(node) {
|
|
27225
|
+
const expression = getInlineStyleExpression(node);
|
|
27226
|
+
if (!expression) return;
|
|
27227
|
+
for (const property of expression.properties ?? []) {
|
|
27228
|
+
const key = getStylePropertyKey(property);
|
|
27229
|
+
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27230
|
+
const value = getStylePropertyStringValue(property);
|
|
27231
|
+
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27232
|
+
node: property,
|
|
27233
|
+
message: MESSAGE$8
|
|
27234
|
+
});
|
|
27235
|
+
}
|
|
27236
|
+
},
|
|
27237
|
+
JSXOpeningElement(node) {
|
|
27238
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27239
|
+
if (!classNameValue) return;
|
|
27240
|
+
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27241
|
+
node,
|
|
27242
|
+
message: MESSAGE$8
|
|
27243
|
+
});
|
|
27244
|
+
}
|
|
27245
|
+
})
|
|
27246
|
+
});
|
|
27247
|
+
//#endregion
|
|
26288
27248
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
26289
27249
|
const preferDynamicImport = defineRule({
|
|
26290
27250
|
id: "prefer-dynamic-import",
|
|
@@ -26876,6 +27836,26 @@ const preferTagOverRole = defineRule({
|
|
|
26876
27836
|
} })
|
|
26877
27837
|
});
|
|
26878
27838
|
//#endregion
|
|
27839
|
+
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27840
|
+
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27841
|
+
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27842
|
+
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27843
|
+
const preferTruncateShorthand = defineRule({
|
|
27844
|
+
id: "prefer-truncate-shorthand",
|
|
27845
|
+
title: "Use truncate shorthand",
|
|
27846
|
+
tags: ["design", "test-noise"],
|
|
27847
|
+
severity: "warn",
|
|
27848
|
+
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27849
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
27850
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27851
|
+
if (!classNameValue) return;
|
|
27852
|
+
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27853
|
+
node,
|
|
27854
|
+
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27855
|
+
});
|
|
27856
|
+
} })
|
|
27857
|
+
});
|
|
27858
|
+
//#endregion
|
|
26879
27859
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
26880
27860
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
26881
27861
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -34681,6 +35661,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34681
35661
|
}
|
|
34682
35662
|
});
|
|
34683
35663
|
//#endregion
|
|
35664
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
35665
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
35666
|
+
"signed",
|
|
35667
|
+
"logged",
|
|
35668
|
+
"sign"
|
|
35669
|
+
]);
|
|
35670
|
+
const mergeSignedInTokens = (tokens) => {
|
|
35671
|
+
const mergedTokens = [];
|
|
35672
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
35673
|
+
const currentToken = tokens[tokenIndex];
|
|
35674
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
35675
|
+
mergedTokens.push(`${currentToken}in`);
|
|
35676
|
+
tokenIndex += 1;
|
|
35677
|
+
continue;
|
|
35678
|
+
}
|
|
35679
|
+
mergedTokens.push(currentToken);
|
|
35680
|
+
}
|
|
35681
|
+
return mergedTokens;
|
|
35682
|
+
};
|
|
35683
|
+
const isAuthGuardName = (calleeName) => {
|
|
35684
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
35685
|
+
if (tokens.length === 0) return false;
|
|
35686
|
+
let hasAssertiveVerb = false;
|
|
35687
|
+
let hasGetterVerb = false;
|
|
35688
|
+
let hasQualifier = false;
|
|
35689
|
+
let hasStrongNoun = false;
|
|
35690
|
+
let hasWeakNoun = false;
|
|
35691
|
+
for (const token of tokens) {
|
|
35692
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35693
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35694
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35695
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35696
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35697
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35698
|
+
}
|
|
35699
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35700
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35701
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35702
|
+
return false;
|
|
35703
|
+
};
|
|
35704
|
+
//#endregion
|
|
34684
35705
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34685
35706
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34686
35707
|
if (!node) return false;
|
|
@@ -34723,9 +35744,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34723
35744
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34724
35745
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34725
35746
|
if (!calleeNode) return null;
|
|
34726
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35747
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35748
|
+
const calleeName = calleeNode.name;
|
|
35749
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35750
|
+
}
|
|
34727
35751
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34728
35752
|
const methodName = calleeNode.property.name;
|
|
35753
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34729
35754
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34730
35755
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34731
35756
|
return methodName;
|
|
@@ -35102,13 +36127,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35102
36127
|
const collectDeclaredNames = (declaration) => {
|
|
35103
36128
|
const names = /* @__PURE__ */ new Set();
|
|
35104
36129
|
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
|
-
}
|
|
36130
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35112
36131
|
return names;
|
|
35113
36132
|
};
|
|
35114
36133
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35118,11 +36137,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35118
36137
|
};
|
|
35119
36138
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35120
36139
|
if (names.size === 0) return false;
|
|
36140
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35121
36141
|
let didRead = false;
|
|
35122
|
-
|
|
35123
|
-
if (
|
|
35124
|
-
|
|
35125
|
-
|
|
36142
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
36143
|
+
if (!declarator.init) continue;
|
|
36144
|
+
walkAst(declarator.init, (child) => {
|
|
36145
|
+
if (didRead) return;
|
|
36146
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
36147
|
+
});
|
|
36148
|
+
}
|
|
35126
36149
|
return didRead;
|
|
35127
36150
|
};
|
|
35128
36151
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36382,7 +37405,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36382
37405
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36383
37406
|
scan: scanByPattern({
|
|
36384
37407
|
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,
|
|
37408
|
+
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
37409
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36387
37410
|
})
|
|
36388
37411
|
});
|
|
@@ -37144,6 +38167,17 @@ const reactDoctorRules = [
|
|
|
37144
38167
|
category: "Performance"
|
|
37145
38168
|
}
|
|
37146
38169
|
},
|
|
38170
|
+
{
|
|
38171
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
38172
|
+
id: "auth-token-in-web-storage",
|
|
38173
|
+
source: "react-doctor",
|
|
38174
|
+
originallyExternal: false,
|
|
38175
|
+
rule: {
|
|
38176
|
+
...authTokenInWebStorage,
|
|
38177
|
+
framework: "global",
|
|
38178
|
+
category: "Security"
|
|
38179
|
+
}
|
|
38180
|
+
},
|
|
37147
38181
|
{
|
|
37148
38182
|
key: "react-doctor/autocomplete-valid",
|
|
37149
38183
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +38394,18 @@ const reactDoctorRules = [
|
|
|
37360
38394
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
38395
|
}
|
|
37362
38396
|
},
|
|
38397
|
+
{
|
|
38398
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
38399
|
+
id: "dialog-has-accessible-name",
|
|
38400
|
+
source: "react-doctor",
|
|
38401
|
+
originallyExternal: false,
|
|
38402
|
+
rule: {
|
|
38403
|
+
...dialogHasAccessibleName,
|
|
38404
|
+
framework: "global",
|
|
38405
|
+
category: "Accessibility",
|
|
38406
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
38407
|
+
}
|
|
38408
|
+
},
|
|
37363
38409
|
{
|
|
37364
38410
|
key: "react-doctor/display-name",
|
|
37365
38411
|
id: "display-name",
|
|
@@ -38484,6 +39530,17 @@ const reactDoctorRules = [
|
|
|
38484
39530
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
38485
39531
|
}
|
|
38486
39532
|
},
|
|
39533
|
+
{
|
|
39534
|
+
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39535
|
+
id: "no-arbitrary-px-font-size",
|
|
39536
|
+
source: "react-doctor",
|
|
39537
|
+
originallyExternal: false,
|
|
39538
|
+
rule: {
|
|
39539
|
+
...noArbitraryPxFontSize,
|
|
39540
|
+
framework: "global",
|
|
39541
|
+
category: "Accessibility"
|
|
39542
|
+
}
|
|
39543
|
+
},
|
|
38487
39544
|
{
|
|
38488
39545
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
38489
39546
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -38519,6 +39576,18 @@ const reactDoctorRules = [
|
|
|
38519
39576
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
39577
|
}
|
|
38521
39578
|
},
|
|
39579
|
+
{
|
|
39580
|
+
key: "react-doctor/no-async-effect-callback",
|
|
39581
|
+
id: "no-async-effect-callback",
|
|
39582
|
+
source: "react-doctor",
|
|
39583
|
+
originallyExternal: false,
|
|
39584
|
+
rule: {
|
|
39585
|
+
...noAsyncEffectCallback,
|
|
39586
|
+
framework: "global",
|
|
39587
|
+
category: "Bugs",
|
|
39588
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
39589
|
+
}
|
|
39590
|
+
},
|
|
38522
39591
|
{
|
|
38523
39592
|
key: "react-doctor/no-autofocus",
|
|
38524
39593
|
id: "no-autofocus",
|
|
@@ -38531,6 +39600,18 @@ const reactDoctorRules = [
|
|
|
38531
39600
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
38532
39601
|
}
|
|
38533
39602
|
},
|
|
39603
|
+
{
|
|
39604
|
+
key: "react-doctor/no-autoplay-without-muted",
|
|
39605
|
+
id: "no-autoplay-without-muted",
|
|
39606
|
+
source: "react-doctor",
|
|
39607
|
+
originallyExternal: false,
|
|
39608
|
+
rule: {
|
|
39609
|
+
...noAutoplayWithoutMuted,
|
|
39610
|
+
framework: "global",
|
|
39611
|
+
category: "Accessibility",
|
|
39612
|
+
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39613
|
+
}
|
|
39614
|
+
},
|
|
38534
39615
|
{
|
|
38535
39616
|
key: "react-doctor/no-barrel-import",
|
|
38536
39617
|
id: "no-barrel-import",
|
|
@@ -38542,6 +39623,18 @@ const reactDoctorRules = [
|
|
|
38542
39623
|
category: "Performance"
|
|
38543
39624
|
}
|
|
38544
39625
|
},
|
|
39626
|
+
{
|
|
39627
|
+
key: "react-doctor/no-call-component-as-function",
|
|
39628
|
+
id: "no-call-component-as-function",
|
|
39629
|
+
source: "react-doctor",
|
|
39630
|
+
originallyExternal: false,
|
|
39631
|
+
rule: {
|
|
39632
|
+
...noCallComponentAsFunction,
|
|
39633
|
+
framework: "global",
|
|
39634
|
+
category: "Bugs",
|
|
39635
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
39636
|
+
}
|
|
39637
|
+
},
|
|
38545
39638
|
{
|
|
38546
39639
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
39640
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +39695,18 @@ const reactDoctorRules = [
|
|
|
38602
39695
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
39696
|
}
|
|
38604
39697
|
},
|
|
39698
|
+
{
|
|
39699
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
39700
|
+
id: "no-create-ref-in-function-component",
|
|
39701
|
+
source: "react-doctor",
|
|
39702
|
+
originallyExternal: false,
|
|
39703
|
+
rule: {
|
|
39704
|
+
...noCreateRefInFunctionComponent,
|
|
39705
|
+
framework: "global",
|
|
39706
|
+
category: "Bugs",
|
|
39707
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
39708
|
+
}
|
|
39709
|
+
},
|
|
38605
39710
|
{
|
|
38606
39711
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
39712
|
id: "no-create-store-in-render",
|
|
@@ -38660,6 +39765,17 @@ const reactDoctorRules = [
|
|
|
38660
39765
|
category: "Maintainability"
|
|
38661
39766
|
}
|
|
38662
39767
|
},
|
|
39768
|
+
{
|
|
39769
|
+
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39770
|
+
id: "no-deprecated-tailwind-class",
|
|
39771
|
+
source: "react-doctor",
|
|
39772
|
+
originallyExternal: false,
|
|
39773
|
+
rule: {
|
|
39774
|
+
...noDeprecatedTailwindClass,
|
|
39775
|
+
framework: "global",
|
|
39776
|
+
category: "Maintainability"
|
|
39777
|
+
}
|
|
39778
|
+
},
|
|
38663
39779
|
{
|
|
38664
39780
|
key: "react-doctor/no-derived-state",
|
|
38665
39781
|
id: "no-derived-state",
|
|
@@ -38779,6 +39895,17 @@ const reactDoctorRules = [
|
|
|
38779
39895
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
38780
39896
|
}
|
|
38781
39897
|
},
|
|
39898
|
+
{
|
|
39899
|
+
key: "react-doctor/no-document-write",
|
|
39900
|
+
id: "no-document-write",
|
|
39901
|
+
source: "react-doctor",
|
|
39902
|
+
originallyExternal: false,
|
|
39903
|
+
rule: {
|
|
39904
|
+
...noDocumentWrite,
|
|
39905
|
+
framework: "global",
|
|
39906
|
+
category: "Performance"
|
|
39907
|
+
}
|
|
39908
|
+
},
|
|
38782
39909
|
{
|
|
38783
39910
|
key: "react-doctor/no-dynamic-import-path",
|
|
38784
39911
|
id: "no-dynamic-import-path",
|
|
@@ -38920,6 +40047,17 @@ const reactDoctorRules = [
|
|
|
38920
40047
|
category: "Performance"
|
|
38921
40048
|
}
|
|
38922
40049
|
},
|
|
40050
|
+
{
|
|
40051
|
+
key: "react-doctor/no-full-viewport-width",
|
|
40052
|
+
id: "no-full-viewport-width",
|
|
40053
|
+
source: "react-doctor",
|
|
40054
|
+
originallyExternal: false,
|
|
40055
|
+
rule: {
|
|
40056
|
+
...noFullViewportWidth,
|
|
40057
|
+
framework: "global",
|
|
40058
|
+
category: "Maintainability"
|
|
40059
|
+
}
|
|
40060
|
+
},
|
|
38923
40061
|
{
|
|
38924
40062
|
key: "react-doctor/no-generic-handler-names",
|
|
38925
40063
|
id: "no-generic-handler-names",
|
|
@@ -38976,6 +40114,18 @@ const reactDoctorRules = [
|
|
|
38976
40114
|
category: "Accessibility"
|
|
38977
40115
|
}
|
|
38978
40116
|
},
|
|
40117
|
+
{
|
|
40118
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
40119
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
40120
|
+
source: "react-doctor",
|
|
40121
|
+
originallyExternal: false,
|
|
40122
|
+
rule: {
|
|
40123
|
+
...noImgLazyWithHighFetchpriority,
|
|
40124
|
+
framework: "global",
|
|
40125
|
+
category: "Performance",
|
|
40126
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
40127
|
+
}
|
|
40128
|
+
},
|
|
38979
40129
|
{
|
|
38980
40130
|
key: "react-doctor/no-initialize-state",
|
|
38981
40131
|
id: "no-initialize-state",
|
|
@@ -39046,6 +40196,17 @@ const reactDoctorRules = [
|
|
|
39046
40196
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
40197
|
}
|
|
39048
40198
|
},
|
|
40199
|
+
{
|
|
40200
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
40201
|
+
id: "no-json-parse-stringify-clone",
|
|
40202
|
+
source: "react-doctor",
|
|
40203
|
+
originallyExternal: false,
|
|
40204
|
+
rule: {
|
|
40205
|
+
...noJsonParseStringifyClone,
|
|
40206
|
+
framework: "global",
|
|
40207
|
+
category: "Performance"
|
|
40208
|
+
}
|
|
40209
|
+
},
|
|
39049
40210
|
{
|
|
39050
40211
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
40212
|
id: "no-jsx-element-type",
|
|
@@ -39136,6 +40297,17 @@ const reactDoctorRules = [
|
|
|
39136
40297
|
category: "Performance"
|
|
39137
40298
|
}
|
|
39138
40299
|
},
|
|
40300
|
+
{
|
|
40301
|
+
key: "react-doctor/no-low-contrast-inline-style",
|
|
40302
|
+
id: "no-low-contrast-inline-style",
|
|
40303
|
+
source: "react-doctor",
|
|
40304
|
+
originallyExternal: false,
|
|
40305
|
+
rule: {
|
|
40306
|
+
...noLowContrastInlineStyle,
|
|
40307
|
+
framework: "global",
|
|
40308
|
+
category: "Accessibility"
|
|
40309
|
+
}
|
|
40310
|
+
},
|
|
39139
40311
|
{
|
|
39140
40312
|
key: "react-doctor/no-many-boolean-props",
|
|
39141
40313
|
id: "no-many-boolean-props",
|
|
@@ -39413,6 +40585,17 @@ const reactDoctorRules = [
|
|
|
39413
40585
|
category: "Maintainability"
|
|
39414
40586
|
}
|
|
39415
40587
|
},
|
|
40588
|
+
{
|
|
40589
|
+
key: "react-doctor/no-redundant-display-class",
|
|
40590
|
+
id: "no-redundant-display-class",
|
|
40591
|
+
source: "react-doctor",
|
|
40592
|
+
originallyExternal: false,
|
|
40593
|
+
rule: {
|
|
40594
|
+
...noRedundantDisplayClass,
|
|
40595
|
+
framework: "global",
|
|
40596
|
+
category: "Maintainability"
|
|
40597
|
+
}
|
|
40598
|
+
},
|
|
39416
40599
|
{
|
|
39417
40600
|
key: "react-doctor/no-redundant-roles",
|
|
39418
40601
|
id: "no-redundant-roles",
|
|
@@ -39565,6 +40748,18 @@ const reactDoctorRules = [
|
|
|
39565
40748
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39566
40749
|
}
|
|
39567
40750
|
},
|
|
40751
|
+
{
|
|
40752
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40753
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40754
|
+
source: "react-doctor",
|
|
40755
|
+
originallyExternal: false,
|
|
40756
|
+
rule: {
|
|
40757
|
+
...noStringFalseOnBooleanAttribute,
|
|
40758
|
+
framework: "global",
|
|
40759
|
+
category: "Bugs",
|
|
40760
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40761
|
+
}
|
|
40762
|
+
},
|
|
39568
40763
|
{
|
|
39569
40764
|
key: "react-doctor/no-string-refs",
|
|
39570
40765
|
id: "no-string-refs",
|
|
@@ -39577,6 +40772,51 @@ const reactDoctorRules = [
|
|
|
39577
40772
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39578
40773
|
}
|
|
39579
40774
|
},
|
|
40775
|
+
{
|
|
40776
|
+
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40777
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
40778
|
+
source: "react-doctor",
|
|
40779
|
+
originallyExternal: false,
|
|
40780
|
+
rule: {
|
|
40781
|
+
...noSvgCurrentcolorWithFillClass,
|
|
40782
|
+
framework: "global",
|
|
40783
|
+
category: "Maintainability"
|
|
40784
|
+
}
|
|
40785
|
+
},
|
|
40786
|
+
{
|
|
40787
|
+
key: "react-doctor/no-sync-xhr",
|
|
40788
|
+
id: "no-sync-xhr",
|
|
40789
|
+
source: "react-doctor",
|
|
40790
|
+
originallyExternal: false,
|
|
40791
|
+
rule: {
|
|
40792
|
+
...noSyncXhr,
|
|
40793
|
+
framework: "global",
|
|
40794
|
+
category: "Performance"
|
|
40795
|
+
}
|
|
40796
|
+
},
|
|
40797
|
+
{
|
|
40798
|
+
key: "react-doctor/no-tailwind-layout-transition",
|
|
40799
|
+
id: "no-tailwind-layout-transition",
|
|
40800
|
+
source: "react-doctor",
|
|
40801
|
+
originallyExternal: false,
|
|
40802
|
+
rule: {
|
|
40803
|
+
...noTailwindLayoutTransition,
|
|
40804
|
+
framework: "global",
|
|
40805
|
+
category: "Performance"
|
|
40806
|
+
}
|
|
40807
|
+
},
|
|
40808
|
+
{
|
|
40809
|
+
key: "react-doctor/no-target-blank-without-rel",
|
|
40810
|
+
id: "no-target-blank-without-rel",
|
|
40811
|
+
source: "react-doctor",
|
|
40812
|
+
originallyExternal: false,
|
|
40813
|
+
rule: {
|
|
40814
|
+
...noTargetBlankWithoutRel,
|
|
40815
|
+
framework: "global",
|
|
40816
|
+
category: "Accessibility",
|
|
40817
|
+
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40818
|
+
}
|
|
40819
|
+
},
|
|
39580
40820
|
{
|
|
39581
40821
|
key: "react-doctor/no-this-in-sfc",
|
|
39582
40822
|
id: "no-this-in-sfc",
|
|
@@ -39646,6 +40886,18 @@ const reactDoctorRules = [
|
|
|
39646
40886
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
39647
40887
|
}
|
|
39648
40888
|
},
|
|
40889
|
+
{
|
|
40890
|
+
key: "react-doctor/no-uninformative-aria-label",
|
|
40891
|
+
id: "no-uninformative-aria-label",
|
|
40892
|
+
source: "react-doctor",
|
|
40893
|
+
originallyExternal: false,
|
|
40894
|
+
rule: {
|
|
40895
|
+
...noUninformativeAriaLabel,
|
|
40896
|
+
framework: "global",
|
|
40897
|
+
category: "Accessibility",
|
|
40898
|
+
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40899
|
+
}
|
|
40900
|
+
},
|
|
39649
40901
|
{
|
|
39650
40902
|
key: "react-doctor/no-unknown-property",
|
|
39651
40903
|
id: "no-unknown-property",
|
|
@@ -39855,6 +41107,17 @@ const reactDoctorRules = [
|
|
|
39855
41107
|
category: "Bugs"
|
|
39856
41108
|
}
|
|
39857
41109
|
},
|
|
41110
|
+
{
|
|
41111
|
+
key: "react-doctor/prefer-dvh-over-vh",
|
|
41112
|
+
id: "prefer-dvh-over-vh",
|
|
41113
|
+
source: "react-doctor",
|
|
41114
|
+
originallyExternal: false,
|
|
41115
|
+
rule: {
|
|
41116
|
+
...preferDvhOverVh,
|
|
41117
|
+
framework: "global",
|
|
41118
|
+
category: "Maintainability"
|
|
41119
|
+
}
|
|
41120
|
+
},
|
|
39858
41121
|
{
|
|
39859
41122
|
key: "react-doctor/prefer-dynamic-import",
|
|
39860
41123
|
id: "prefer-dynamic-import",
|
|
@@ -39959,6 +41222,17 @@ const reactDoctorRules = [
|
|
|
39959
41222
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
39960
41223
|
}
|
|
39961
41224
|
},
|
|
41225
|
+
{
|
|
41226
|
+
key: "react-doctor/prefer-truncate-shorthand",
|
|
41227
|
+
id: "prefer-truncate-shorthand",
|
|
41228
|
+
source: "react-doctor",
|
|
41229
|
+
originallyExternal: false,
|
|
41230
|
+
rule: {
|
|
41231
|
+
...preferTruncateShorthand,
|
|
41232
|
+
framework: "global",
|
|
41233
|
+
category: "Maintainability"
|
|
41234
|
+
}
|
|
41235
|
+
},
|
|
39962
41236
|
{
|
|
39963
41237
|
key: "react-doctor/prefer-use-effect-event",
|
|
39964
41238
|
id: "prefer-use-effect-event",
|