oxlint-plugin-react-doctor 0.5.5 → 0.5.6-dev.0053a02
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 +1423 -4
- package/dist/index.js +1338 -193
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -323,9 +323,18 @@ const BROWSER_ARTIFACT_PATH_PATTERNS = [
|
|
|
323
323
|
const AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN = /\b(?:exec|execSync|spawn|child_process|eval|new Function|vm\.run|readFile|writeFile|fs\.read|fs\.write|fetch|axios|http\.request|sandbox|runCode|executeCode)\b/;
|
|
324
324
|
//#endregion
|
|
325
325
|
//#region src/plugin/rules/security-scan/utils/is-browser-artifact-path.ts
|
|
326
|
-
const
|
|
326
|
+
const SERVER_BUILD_ROOT_SEGMENTS = new Set([".next", ".output"]);
|
|
327
|
+
const isNonShippedBuildArtifactPath = (relativePath) => {
|
|
328
|
+
const segments = relativePath.split("/");
|
|
329
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
330
|
+
if (!SERVER_BUILD_ROOT_SEGMENTS.has(segments[index])) continue;
|
|
331
|
+
if (segments[index] === ".next" && segments[index + 1] === "dev") return true;
|
|
332
|
+
if (segments[index + 1] === "server" && index + 2 < segments.length) return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
};
|
|
327
336
|
const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
328
|
-
if (
|
|
337
|
+
if (isNonShippedBuildArtifactPath(relativePath)) return false;
|
|
329
338
|
if (isGeneratedBundle) return true;
|
|
330
339
|
if (relativePath.endsWith(".map")) return true;
|
|
331
340
|
return BROWSER_ARTIFACT_PATH_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
@@ -881,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
881
890
|
});
|
|
882
891
|
//#endregion
|
|
883
892
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
884
|
-
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) => {
|
|
885
907
|
const characters = content.split("");
|
|
886
908
|
let stringDelimiter = null;
|
|
909
|
+
let isBlankingString = false;
|
|
910
|
+
const templateExpressionDepths = [];
|
|
887
911
|
let index = 0;
|
|
912
|
+
const blankUnlessNewline = (offset) => {
|
|
913
|
+
if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
|
|
914
|
+
};
|
|
888
915
|
while (index < content.length) {
|
|
889
916
|
const character = content[index];
|
|
890
917
|
const nextCharacter = content[index + 1];
|
|
891
918
|
if (stringDelimiter !== null) {
|
|
892
919
|
if (character === "\\") {
|
|
920
|
+
if (isBlankingString) {
|
|
921
|
+
blankUnlessNewline(index);
|
|
922
|
+
blankUnlessNewline(index + 1);
|
|
923
|
+
}
|
|
893
924
|
index += 2;
|
|
894
925
|
continue;
|
|
895
926
|
}
|
|
896
|
-
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);
|
|
897
939
|
index += 1;
|
|
898
940
|
continue;
|
|
899
941
|
}
|
|
900
|
-
if (character === "\"" || character === "'"
|
|
942
|
+
if (character === "\"" || character === "'") {
|
|
901
943
|
stringDelimiter = character;
|
|
944
|
+
isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
|
|
945
|
+
index += 1;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (character === "`") {
|
|
949
|
+
stringDelimiter = "`";
|
|
950
|
+
isBlankingString = blankStringContents;
|
|
902
951
|
index += 1;
|
|
903
952
|
continue;
|
|
904
953
|
}
|
|
@@ -917,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
|
|
|
917
966
|
index += 2;
|
|
918
967
|
break;
|
|
919
968
|
}
|
|
920
|
-
|
|
969
|
+
blankUnlessNewline(index);
|
|
921
970
|
index += 1;
|
|
922
971
|
}
|
|
923
972
|
continue;
|
|
924
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
|
+
}
|
|
925
983
|
index += 1;
|
|
926
984
|
}
|
|
927
985
|
return characters.join("");
|
|
928
986
|
};
|
|
987
|
+
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
988
|
+
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
929
989
|
//#endregion
|
|
930
990
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
931
991
|
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
932
|
-
const
|
|
992
|
+
const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
993
|
+
const getScannableContent = (file, ignoreStringLiterals = false) => {
|
|
933
994
|
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
934
|
-
const
|
|
995
|
+
const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
|
|
996
|
+
const cachedContent = cache.get(file);
|
|
935
997
|
if (cachedContent !== void 0) return cachedContent;
|
|
936
|
-
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
937
|
-
|
|
998
|
+
const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
|
|
999
|
+
cache.set(file, strippedContent);
|
|
938
1000
|
return strippedContent;
|
|
939
1001
|
};
|
|
940
|
-
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
1002
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
|
|
941
1003
|
if (!shouldScan(file)) return [];
|
|
942
|
-
const content = getScannableContent(file);
|
|
1004
|
+
const content = getScannableContent(file, ignoreStringLiterals);
|
|
943
1005
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
944
1006
|
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
945
1007
|
if (matchedPattern === void 0) return [];
|
|
@@ -964,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
964
1026
|
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
965
1027
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
966
1028
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1029
|
+
ignoreStringLiterals: true,
|
|
967
1030
|
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
968
1031
|
})
|
|
969
1032
|
});
|
|
@@ -1108,6 +1171,11 @@ const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSourc
|
|
|
1108
1171
|
if (info.source !== moduleSource) return null;
|
|
1109
1172
|
return info.imported;
|
|
1110
1173
|
};
|
|
1174
|
+
const getImportSourceForName = (contextNode, localIdentifierName) => {
|
|
1175
|
+
const lookup = getImportLookup(contextNode);
|
|
1176
|
+
if (!lookup) return null;
|
|
1177
|
+
return lookup.get(localIdentifierName)?.source ?? null;
|
|
1178
|
+
};
|
|
1111
1179
|
//#endregion
|
|
1112
1180
|
//#region src/plugin/utils/find-variable-initializer.ts
|
|
1113
1181
|
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
@@ -1847,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1847
1915
|
});
|
|
1848
1916
|
//#endregion
|
|
1849
1917
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1850
|
-
const MESSAGE$
|
|
1918
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1851
1919
|
const anchorHasContent = defineRule({
|
|
1852
1920
|
id: "anchor-has-content",
|
|
1853
1921
|
title: "Anchor has no content",
|
|
@@ -1863,7 +1931,7 @@ const anchorHasContent = defineRule({
|
|
|
1863
1931
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1864
1932
|
context.report({
|
|
1865
1933
|
node: opening.name,
|
|
1866
|
-
message: MESSAGE$
|
|
1934
|
+
message: MESSAGE$59
|
|
1867
1935
|
});
|
|
1868
1936
|
} })
|
|
1869
1937
|
});
|
|
@@ -2257,7 +2325,7 @@ const parseJsxValue = (value) => {
|
|
|
2257
2325
|
};
|
|
2258
2326
|
//#endregion
|
|
2259
2327
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2260
|
-
const MESSAGE$
|
|
2328
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2261
2329
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2262
2330
|
id: "aria-activedescendant-has-tabindex",
|
|
2263
2331
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2275,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2275
2343
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2276
2344
|
context.report({
|
|
2277
2345
|
node: node.name,
|
|
2278
|
-
message: MESSAGE$
|
|
2346
|
+
message: MESSAGE$58
|
|
2279
2347
|
});
|
|
2280
2348
|
return;
|
|
2281
2349
|
}
|
|
2282
2350
|
if (isInteractiveElement(tag, node)) return;
|
|
2283
2351
|
context.report({
|
|
2284
2352
|
node: node.name,
|
|
2285
|
-
message: MESSAGE$
|
|
2353
|
+
message: MESSAGE$58
|
|
2286
2354
|
});
|
|
2287
2355
|
} })
|
|
2288
2356
|
});
|
|
@@ -3071,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3071
3139
|
scan: scanByPattern({
|
|
3072
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3073
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,
|
|
3074
|
-
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],
|
|
3075
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3076
3144
|
})
|
|
3077
3145
|
});
|
|
@@ -3090,6 +3158,76 @@ const AUTH_FUNCTION_NAMES = new Set([
|
|
|
3090
3158
|
"getAuth",
|
|
3091
3159
|
"validateSession"
|
|
3092
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
|
+
]);
|
|
3093
3231
|
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3094
3232
|
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3095
3233
|
const SECRET_PATTERNS = [
|
|
@@ -4187,6 +4325,58 @@ const asyncParallel = defineRule({
|
|
|
4187
4325
|
}
|
|
4188
4326
|
});
|
|
4189
4327
|
//#endregion
|
|
4328
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4329
|
+
const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4330
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4331
|
+
const STORAGE_GLOBALS = new Set([
|
|
4332
|
+
"window",
|
|
4333
|
+
"globalThis",
|
|
4334
|
+
"self"
|
|
4335
|
+
]);
|
|
4336
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4337
|
+
const isWebStorageObject = (node) => {
|
|
4338
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4339
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4340
|
+
return false;
|
|
4341
|
+
};
|
|
4342
|
+
const staticMemberName = (member) => {
|
|
4343
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4344
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4345
|
+
return null;
|
|
4346
|
+
};
|
|
4347
|
+
const authTokenInWebStorage = defineRule({
|
|
4348
|
+
id: "auth-token-in-web-storage",
|
|
4349
|
+
title: "Auth token in web storage",
|
|
4350
|
+
severity: "warn",
|
|
4351
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4352
|
+
create: (context) => ({
|
|
4353
|
+
CallExpression(node) {
|
|
4354
|
+
const callee = node.callee;
|
|
4355
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4356
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4357
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4358
|
+
const keyArgument = node.arguments?.[0];
|
|
4359
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4360
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4361
|
+
context.report({
|
|
4362
|
+
node,
|
|
4363
|
+
message: MESSAGE$57
|
|
4364
|
+
});
|
|
4365
|
+
},
|
|
4366
|
+
AssignmentExpression(node) {
|
|
4367
|
+
const target = node.left;
|
|
4368
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4369
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4370
|
+
const propertyName = staticMemberName(target);
|
|
4371
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4372
|
+
context.report({
|
|
4373
|
+
node: target,
|
|
4374
|
+
message: MESSAGE$57
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
})
|
|
4378
|
+
});
|
|
4379
|
+
//#endregion
|
|
4190
4380
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4191
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.`;
|
|
4192
4382
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4558,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4558
4748
|
//#endregion
|
|
4559
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4560
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4561
|
-
const MESSAGE$
|
|
4751
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4562
4752
|
const KEY_HANDLERS = [
|
|
4563
4753
|
"onKeyUp",
|
|
4564
4754
|
"onKeyDown",
|
|
@@ -4590,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4590
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4591
4781
|
context.report({
|
|
4592
4782
|
node: node.name,
|
|
4593
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4594
4784
|
});
|
|
4595
4785
|
} };
|
|
4596
4786
|
}
|
|
@@ -4705,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4705
4895
|
};
|
|
4706
4896
|
//#endregion
|
|
4707
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4708
|
-
const MESSAGE$
|
|
4898
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4709
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4710
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4711
4901
|
"alt",
|
|
@@ -4866,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4866
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4867
5057
|
context.report({
|
|
4868
5058
|
node: opening,
|
|
4869
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
4870
5060
|
});
|
|
4871
5061
|
} };
|
|
4872
5062
|
}
|
|
@@ -5292,6 +5482,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5292
5482
|
} })
|
|
5293
5483
|
});
|
|
5294
5484
|
//#endregion
|
|
5485
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5486
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5487
|
+
//#endregion
|
|
5488
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5489
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5490
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5491
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5492
|
+
"aria-label",
|
|
5493
|
+
"aria-labelledby",
|
|
5494
|
+
"title"
|
|
5495
|
+
];
|
|
5496
|
+
const dialogHasAccessibleName = defineRule({
|
|
5497
|
+
id: "dialog-has-accessible-name",
|
|
5498
|
+
title: "Dialog without accessible name",
|
|
5499
|
+
severity: "warn",
|
|
5500
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5501
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5502
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5503
|
+
const tagName = node.name.name;
|
|
5504
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5505
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5506
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5507
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5508
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5509
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5510
|
+
context.report({
|
|
5511
|
+
node: node.name,
|
|
5512
|
+
message: MESSAGE$54
|
|
5513
|
+
});
|
|
5514
|
+
} })
|
|
5515
|
+
});
|
|
5516
|
+
//#endregion
|
|
5295
5517
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5296
5518
|
const PRAGMA$2 = "React";
|
|
5297
5519
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5326,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5326
5548
|
};
|
|
5327
5549
|
//#endregion
|
|
5328
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5329
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5330
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5331
5553
|
"observer",
|
|
5332
5554
|
"lazy",
|
|
@@ -5529,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5529
5751
|
const reportAt = (node) => {
|
|
5530
5752
|
context.report({
|
|
5531
5753
|
node,
|
|
5532
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5533
5755
|
});
|
|
5534
5756
|
};
|
|
5535
5757
|
return {
|
|
@@ -7677,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7677
7899
|
});
|
|
7678
7900
|
//#endregion
|
|
7679
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7680
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7681
7903
|
const forwardRefUsesRef = defineRule({
|
|
7682
7904
|
id: "forward-ref-uses-ref",
|
|
7683
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7697,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7697
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7698
7920
|
context.report({
|
|
7699
7921
|
node: inner,
|
|
7700
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7701
7923
|
});
|
|
7702
7924
|
} })
|
|
7703
7925
|
});
|
|
@@ -7734,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7734
7956
|
});
|
|
7735
7957
|
//#endregion
|
|
7736
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7737
|
-
const MESSAGE$
|
|
7959
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7738
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7739
7961
|
"h1",
|
|
7740
7962
|
"h2",
|
|
@@ -7767,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7767
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7768
7990
|
context.report({
|
|
7769
7991
|
node,
|
|
7770
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7771
7993
|
});
|
|
7772
7994
|
} };
|
|
7773
7995
|
}
|
|
@@ -7905,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7905
8127
|
});
|
|
7906
8128
|
//#endregion
|
|
7907
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7908
|
-
const MESSAGE$
|
|
8130
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7909
8131
|
const resolveSettings$38 = (settings) => {
|
|
7910
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
7911
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7953,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
7953
8175
|
if (!lang) {
|
|
7954
8176
|
context.report({
|
|
7955
8177
|
node: node.name,
|
|
7956
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
7957
8179
|
});
|
|
7958
8180
|
return;
|
|
7959
8181
|
}
|
|
@@ -7961,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
7961
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
7962
8184
|
context.report({
|
|
7963
8185
|
node: lang,
|
|
7964
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
7965
8187
|
});
|
|
7966
8188
|
return;
|
|
7967
8189
|
}
|
|
7968
8190
|
if (hasSpread && !lang) context.report({
|
|
7969
8191
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
7971
8193
|
});
|
|
7972
8194
|
} };
|
|
7973
8195
|
}
|
|
@@ -8181,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8181
8403
|
});
|
|
8182
8404
|
//#endregion
|
|
8183
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8184
|
-
const MESSAGE$
|
|
8406
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8185
8407
|
const evaluateTitleValue = (value) => {
|
|
8186
8408
|
if (!value) return "missing";
|
|
8187
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8221,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8221
8443
|
if (!titleAttr) {
|
|
8222
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8223
8445
|
node: node.name,
|
|
8224
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8225
8447
|
});
|
|
8226
8448
|
return;
|
|
8227
8449
|
}
|
|
8228
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8229
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8230
8452
|
node: titleAttr,
|
|
8231
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8232
8454
|
});
|
|
8233
8455
|
} })
|
|
8234
8456
|
});
|
|
@@ -8332,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8332
8554
|
});
|
|
8333
8555
|
//#endregion
|
|
8334
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8335
|
-
const MESSAGE$
|
|
8557
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8336
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8337
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8338
8560
|
"image",
|
|
@@ -8397,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8397
8619
|
if (!altAttribute) return;
|
|
8398
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8399
8621
|
node: altAttribute,
|
|
8400
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8401
8623
|
});
|
|
8402
8624
|
} };
|
|
8403
8625
|
}
|
|
@@ -8495,6 +8717,136 @@ const insecureCryptoRisk = defineRule({
|
|
|
8495
8717
|
}
|
|
8496
8718
|
});
|
|
8497
8719
|
//#endregion
|
|
8720
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
8721
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
8722
|
+
const open = content[openIndex];
|
|
8723
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
8724
|
+
if (close === "") return -1;
|
|
8725
|
+
let depth = 0;
|
|
8726
|
+
let stringDelimiter = null;
|
|
8727
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
8728
|
+
const character = content[index];
|
|
8729
|
+
if (stringDelimiter !== null) {
|
|
8730
|
+
if (character === "\\") index += 1;
|
|
8731
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8732
|
+
continue;
|
|
8733
|
+
}
|
|
8734
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8735
|
+
else if (character === open) depth += 1;
|
|
8736
|
+
else if (character === close) {
|
|
8737
|
+
depth -= 1;
|
|
8738
|
+
if (depth === 0) return index;
|
|
8739
|
+
}
|
|
8740
|
+
}
|
|
8741
|
+
return -1;
|
|
8742
|
+
};
|
|
8743
|
+
//#endregion
|
|
8744
|
+
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
8745
|
+
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
8746
|
+
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
8747
|
+
const AUTH_COOKIE_SET_CALL_PATTERN = new RegExp(`(?:\\.cookies\\.set|cookies\\(\\s*\\)\\.set|\\.cookie)\\s*\\(\\s*${AUTH_COOKIE_NAME_LITERAL}`, "gi");
|
|
8748
|
+
const HTTP_ONLY_DISABLED_PATTERN = /httpOnly\s*:\s*false\b/i;
|
|
8749
|
+
const STRING_LITERAL_PATTERN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
|
|
8750
|
+
const blankStringContents = (text) => {
|
|
8751
|
+
const characters = text.split("");
|
|
8752
|
+
let index = 0;
|
|
8753
|
+
let stringDelimiter = null;
|
|
8754
|
+
while (index < text.length) {
|
|
8755
|
+
const character = text[index];
|
|
8756
|
+
if (stringDelimiter !== null) {
|
|
8757
|
+
if (character === "\\") {
|
|
8758
|
+
index += 2;
|
|
8759
|
+
continue;
|
|
8760
|
+
}
|
|
8761
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
8762
|
+
else if (character !== "\n") characters[index] = " ";
|
|
8763
|
+
index += 1;
|
|
8764
|
+
continue;
|
|
8765
|
+
}
|
|
8766
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8767
|
+
index += 1;
|
|
8768
|
+
}
|
|
8769
|
+
return characters.join("");
|
|
8770
|
+
};
|
|
8771
|
+
const COOKIE_CONFIG_OPENER_PATTERN = /cookie\s*:\s*\{/gi;
|
|
8772
|
+
const CLIENT_AUTH_COOKIE_WRITE_PATTERN = new RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${AUTH_COOKIE_NAME_TOKEN}[^\`"'=;]*=`, "gi");
|
|
8773
|
+
const countTopLevelArguments = (argumentsSource) => {
|
|
8774
|
+
if (argumentsSource.trim().length === 0) return 0;
|
|
8775
|
+
let depth = 0;
|
|
8776
|
+
let stringDelimiter = null;
|
|
8777
|
+
let count = 1;
|
|
8778
|
+
for (let index = 0; index < argumentsSource.length; index += 1) {
|
|
8779
|
+
const character = argumentsSource[index];
|
|
8780
|
+
if (stringDelimiter !== null) {
|
|
8781
|
+
if (character === "\\") index += 1;
|
|
8782
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8783
|
+
continue;
|
|
8784
|
+
}
|
|
8785
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8786
|
+
else if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
8787
|
+
else if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
8788
|
+
else if (character === "," && depth === 0) count += 1;
|
|
8789
|
+
}
|
|
8790
|
+
return count;
|
|
8791
|
+
};
|
|
8792
|
+
const addMatchFindings = (content, pattern, message, isInsecure, findings) => {
|
|
8793
|
+
pattern.lastIndex = 0;
|
|
8794
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
8795
|
+
if (!isInsecure(match.index, match[0])) continue;
|
|
8796
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8797
|
+
findings.push({
|
|
8798
|
+
message,
|
|
8799
|
+
line: location.line,
|
|
8800
|
+
column: location.column
|
|
8801
|
+
});
|
|
8802
|
+
}
|
|
8803
|
+
};
|
|
8804
|
+
const insecureSessionCookie = defineRule({
|
|
8805
|
+
id: "insecure-session-cookie",
|
|
8806
|
+
title: "Auth cookie missing HttpOnly protection",
|
|
8807
|
+
severity: "warn",
|
|
8808
|
+
recommendation: "Set auth/session cookies server-side with `httpOnly: true`, `secure: true`, and `sameSite`. Cookies set via `document.cookie` or with `httpOnly: false` are readable by any XSS payload and can be stolen.",
|
|
8809
|
+
scan: (file) => {
|
|
8810
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
8811
|
+
const content = getScannableContent(file);
|
|
8812
|
+
if (!/cookie/i.test(content)) return [];
|
|
8813
|
+
const findings = [];
|
|
8814
|
+
const message = "An auth/session cookie is exposed to JavaScript (set via document.cookie, with httpOnly: false, or without cookie options), letting an XSS payload steal it.";
|
|
8815
|
+
AUTH_COOKIE_SET_CALL_PATTERN.lastIndex = 0;
|
|
8816
|
+
for (let match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content); match !== null; match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content)) {
|
|
8817
|
+
const openParenIndex = match.index + match[0].lastIndexOf("(");
|
|
8818
|
+
const closeParenIndex = findMatchingBracket(content, openParenIndex);
|
|
8819
|
+
if (closeParenIndex < 0) continue;
|
|
8820
|
+
const argumentsSource = content.slice(openParenIndex + 1, closeParenIndex);
|
|
8821
|
+
const hasNoOptions = countTopLevelArguments(argumentsSource) < 3;
|
|
8822
|
+
const argumentsWithoutStrings = argumentsSource.replace(STRING_LITERAL_PATTERN, "");
|
|
8823
|
+
if (!hasNoOptions && !HTTP_ONLY_DISABLED_PATTERN.test(argumentsWithoutStrings)) continue;
|
|
8824
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8825
|
+
findings.push({
|
|
8826
|
+
message,
|
|
8827
|
+
line: location.line,
|
|
8828
|
+
column: location.column
|
|
8829
|
+
});
|
|
8830
|
+
}
|
|
8831
|
+
const blankedContent = blankStringContents(content);
|
|
8832
|
+
COOKIE_CONFIG_OPENER_PATTERN.lastIndex = 0;
|
|
8833
|
+
for (let match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent); match !== null; match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent)) {
|
|
8834
|
+
const braceIndex = match.index + match[0].length - 1;
|
|
8835
|
+
const closeBraceIndex = findMatchingBracket(blankedContent, braceIndex);
|
|
8836
|
+
const block = closeBraceIndex >= 0 ? blankedContent.slice(braceIndex, closeBraceIndex) : blankedContent.slice(braceIndex, braceIndex + 400);
|
|
8837
|
+
if (!HTTP_ONLY_DISABLED_PATTERN.test(block)) continue;
|
|
8838
|
+
const location = getLocationAtIndex(blankedContent, match.index);
|
|
8839
|
+
findings.push({
|
|
8840
|
+
message,
|
|
8841
|
+
line: location.line,
|
|
8842
|
+
column: location.column
|
|
8843
|
+
});
|
|
8844
|
+
}
|
|
8845
|
+
addMatchFindings(content, CLIENT_AUTH_COOKIE_WRITE_PATTERN, message, () => true, findings);
|
|
8846
|
+
return findings;
|
|
8847
|
+
}
|
|
8848
|
+
});
|
|
8849
|
+
//#endregion
|
|
8498
8850
|
//#region src/plugin/constants/event-handlers.ts
|
|
8499
8851
|
const MOUSE_EVENT_HANDLERS = [
|
|
8500
8852
|
"onClick",
|
|
@@ -10624,7 +10976,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10624
10976
|
});
|
|
10625
10977
|
//#endregion
|
|
10626
10978
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10627
|
-
const MESSAGE$
|
|
10979
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10628
10980
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10629
10981
|
"code",
|
|
10630
10982
|
"pre",
|
|
@@ -10660,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10660
11012
|
if (isInsideLiteralTextTag(node)) return;
|
|
10661
11013
|
context.report({
|
|
10662
11014
|
node,
|
|
10663
|
-
message: MESSAGE$
|
|
11015
|
+
message: MESSAGE$47
|
|
10664
11016
|
});
|
|
10665
11017
|
} })
|
|
10666
11018
|
});
|
|
@@ -10691,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10691
11043
|
};
|
|
10692
11044
|
//#endregion
|
|
10693
11045
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10694
|
-
const MESSAGE$
|
|
11046
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10695
11047
|
const CONTEXT_MODULES$1 = [
|
|
10696
11048
|
"react",
|
|
10697
11049
|
"use-context-selector",
|
|
@@ -10789,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10789
11141
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10790
11142
|
context.report({
|
|
10791
11143
|
node: attribute,
|
|
10792
|
-
message: MESSAGE$
|
|
11144
|
+
message: MESSAGE$46
|
|
10793
11145
|
});
|
|
10794
11146
|
}
|
|
10795
11147
|
}
|
|
@@ -10875,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
10875
11227
|
};
|
|
10876
11228
|
//#endregion
|
|
10877
11229
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
10878
|
-
const MESSAGE$
|
|
11230
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
10879
11231
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
10880
11232
|
"icon",
|
|
10881
11233
|
"Icon",
|
|
@@ -11144,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11144
11496
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11145
11497
|
context.report({
|
|
11146
11498
|
node,
|
|
11147
|
-
message: MESSAGE$
|
|
11499
|
+
message: MESSAGE$45
|
|
11148
11500
|
});
|
|
11149
11501
|
}
|
|
11150
11502
|
};
|
|
@@ -11432,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11432
11784
|
];
|
|
11433
11785
|
//#endregion
|
|
11434
11786
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11435
|
-
const MESSAGE$
|
|
11787
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11436
11788
|
const isDataArrayPropName = (propName) => {
|
|
11437
11789
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11438
11790
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11516,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11516
11868
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11517
11869
|
context.report({
|
|
11518
11870
|
node,
|
|
11519
|
-
message: MESSAGE$
|
|
11871
|
+
message: MESSAGE$44
|
|
11520
11872
|
});
|
|
11521
11873
|
}
|
|
11522
11874
|
};
|
|
@@ -11774,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11774
12126
|
]);
|
|
11775
12127
|
//#endregion
|
|
11776
12128
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11777
|
-
const MESSAGE$
|
|
12129
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11778
12130
|
const isAccessorPredicateName = (propName) => {
|
|
11779
12131
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11780
12132
|
if (propName.length <= prefix.length) continue;
|
|
@@ -11980,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
11980
12332
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
11981
12333
|
context.report({
|
|
11982
12334
|
node,
|
|
11983
|
-
message: MESSAGE$
|
|
12335
|
+
message: MESSAGE$43
|
|
11984
12336
|
});
|
|
11985
12337
|
}
|
|
11986
12338
|
};
|
|
@@ -12200,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12200
12552
|
];
|
|
12201
12553
|
//#endregion
|
|
12202
12554
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12203
|
-
const MESSAGE$
|
|
12555
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12204
12556
|
const isConfigObjectPropName = (propName) => {
|
|
12205
12557
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12206
12558
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12288,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12288
12640
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12289
12641
|
context.report({
|
|
12290
12642
|
node,
|
|
12291
|
-
message: MESSAGE$
|
|
12643
|
+
message: MESSAGE$42
|
|
12292
12644
|
});
|
|
12293
12645
|
}
|
|
12294
12646
|
};
|
|
@@ -12296,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12296
12648
|
});
|
|
12297
12649
|
//#endregion
|
|
12298
12650
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12299
|
-
const MESSAGE$
|
|
12651
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12300
12652
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12301
12653
|
const resolveSettings$28 = (settings) => {
|
|
12302
12654
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12337,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12337
12689
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12338
12690
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12339
12691
|
node: attribute,
|
|
12340
|
-
message: MESSAGE$
|
|
12692
|
+
message: MESSAGE$41
|
|
12341
12693
|
});
|
|
12342
12694
|
}
|
|
12343
12695
|
} };
|
|
@@ -12652,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12652
13004
|
});
|
|
12653
13005
|
//#endregion
|
|
12654
13006
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12655
|
-
const MESSAGE$
|
|
13007
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12656
13008
|
const resolveSettings$25 = (settings) => {
|
|
12657
13009
|
const reactDoctor = settings?.["react-doctor"];
|
|
12658
13010
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12693,18 +13045,77 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12693
13045
|
}
|
|
12694
13046
|
context.report({
|
|
12695
13047
|
node: attribute,
|
|
12696
|
-
message: MESSAGE$
|
|
13048
|
+
message: MESSAGE$40
|
|
12697
13049
|
});
|
|
12698
13050
|
}
|
|
12699
13051
|
} };
|
|
12700
13052
|
}
|
|
12701
13053
|
});
|
|
12702
13054
|
//#endregion
|
|
13055
|
+
//#region src/plugin/rules/security-scan/jwt-insecure-verification.ts
|
|
13056
|
+
const NONE_ALGORITHM_PATTERN = /\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi;
|
|
13057
|
+
const isIndexInsideStringLiteral = (content, index) => {
|
|
13058
|
+
let stringDelimiter = null;
|
|
13059
|
+
const templateExpressionDepths = [];
|
|
13060
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
13061
|
+
const character = content[cursor];
|
|
13062
|
+
if (stringDelimiter === "`") {
|
|
13063
|
+
if (character === "\\") cursor += 1;
|
|
13064
|
+
else if (character === "`") stringDelimiter = null;
|
|
13065
|
+
else if (character === "$" && content[cursor + 1] === "{") {
|
|
13066
|
+
templateExpressionDepths.push(0);
|
|
13067
|
+
stringDelimiter = null;
|
|
13068
|
+
cursor += 1;
|
|
13069
|
+
}
|
|
13070
|
+
continue;
|
|
13071
|
+
}
|
|
13072
|
+
if (stringDelimiter !== null) {
|
|
13073
|
+
if (character === "\\") cursor += 1;
|
|
13074
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
13075
|
+
continue;
|
|
13076
|
+
}
|
|
13077
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
13078
|
+
else if (templateExpressionDepths.length > 0) {
|
|
13079
|
+
const top = templateExpressionDepths.length - 1;
|
|
13080
|
+
if (character === "{") templateExpressionDepths[top] += 1;
|
|
13081
|
+
else if (character === "}") if (templateExpressionDepths[top] === 0) {
|
|
13082
|
+
templateExpressionDepths.pop();
|
|
13083
|
+
stringDelimiter = "`";
|
|
13084
|
+
} else templateExpressionDepths[top] -= 1;
|
|
13085
|
+
}
|
|
13086
|
+
}
|
|
13087
|
+
return stringDelimiter !== null;
|
|
13088
|
+
};
|
|
13089
|
+
const jwtInsecureVerification = defineRule({
|
|
13090
|
+
id: "jwt-insecure-verification",
|
|
13091
|
+
title: "JWT verified with the 'none' algorithm",
|
|
13092
|
+
severity: "error",
|
|
13093
|
+
recommendation: "Never accept the `none` algorithm; it disables signature verification and lets any forged token through. Pin the real algorithm(s) explicitly (`jwt.verify(token, key, { algorithms: ['RS256'] })`).",
|
|
13094
|
+
scan: (file) => {
|
|
13095
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
13096
|
+
const content = getScannableContent(file);
|
|
13097
|
+
if (!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(content)) return [];
|
|
13098
|
+
const findings = [];
|
|
13099
|
+
NONE_ALGORITHM_PATTERN.lastIndex = 0;
|
|
13100
|
+
for (let noneMatch = NONE_ALGORITHM_PATTERN.exec(content); noneMatch !== null; noneMatch = NONE_ALGORITHM_PATTERN.exec(content)) {
|
|
13101
|
+
if (isIndexInsideStringLiteral(content, noneMatch.index)) continue;
|
|
13102
|
+
const location = getLocationAtIndex(content, noneMatch.index);
|
|
13103
|
+
findings.push({
|
|
13104
|
+
message: "JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.",
|
|
13105
|
+
line: location.line,
|
|
13106
|
+
column: location.column
|
|
13107
|
+
});
|
|
13108
|
+
}
|
|
13109
|
+
return findings;
|
|
13110
|
+
}
|
|
13111
|
+
});
|
|
13112
|
+
//#endregion
|
|
12703
13113
|
//#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
|
|
12704
13114
|
const keyLifecycleRisk = defineRule({
|
|
12705
13115
|
id: "key-lifecycle-risk",
|
|
12706
13116
|
title: "Long-lived key material in repository",
|
|
12707
13117
|
severity: "error",
|
|
13118
|
+
committedFilesOnly: true,
|
|
12708
13119
|
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
12709
13120
|
scan: scanByPattern({
|
|
12710
13121
|
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
@@ -12862,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
12862
13273
|
});
|
|
12863
13274
|
//#endregion
|
|
12864
13275
|
//#region src/plugin/rules/a11y/lang.ts
|
|
12865
|
-
const MESSAGE$
|
|
13276
|
+
const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
12866
13277
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
12867
13278
|
"aa",
|
|
12868
13279
|
"ab",
|
|
@@ -13074,7 +13485,7 @@ const lang = defineRule({
|
|
|
13074
13485
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13075
13486
|
context.report({
|
|
13076
13487
|
node: langAttr,
|
|
13077
|
-
message: MESSAGE$
|
|
13488
|
+
message: MESSAGE$39
|
|
13078
13489
|
});
|
|
13079
13490
|
return;
|
|
13080
13491
|
}
|
|
@@ -13083,7 +13494,7 @@ const lang = defineRule({
|
|
|
13083
13494
|
if (value === null) return;
|
|
13084
13495
|
if (!isValidLangTag(value)) context.report({
|
|
13085
13496
|
node: langAttr,
|
|
13086
|
-
message: MESSAGE$
|
|
13497
|
+
message: MESSAGE$39
|
|
13087
13498
|
});
|
|
13088
13499
|
} })
|
|
13089
13500
|
});
|
|
@@ -13109,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13109
13520
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13110
13521
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13111
13522
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13523
|
+
ignoreStringLiterals: true,
|
|
13112
13524
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13113
13525
|
})
|
|
13114
13526
|
});
|
|
@@ -13127,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13127
13539
|
});
|
|
13128
13540
|
//#endregion
|
|
13129
13541
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13130
|
-
const MESSAGE$
|
|
13542
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13131
13543
|
const DEFAULT_AUDIO = ["audio"];
|
|
13132
13544
|
const DEFAULT_VIDEO = ["video"];
|
|
13133
13545
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13168,7 +13580,7 @@ const mediaHasCaption = defineRule({
|
|
|
13168
13580
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13169
13581
|
context.report({
|
|
13170
13582
|
node: node.name,
|
|
13171
|
-
message: MESSAGE$
|
|
13583
|
+
message: MESSAGE$38
|
|
13172
13584
|
});
|
|
13173
13585
|
return;
|
|
13174
13586
|
}
|
|
@@ -13185,7 +13597,7 @@ const mediaHasCaption = defineRule({
|
|
|
13185
13597
|
return kindValue.value.toLowerCase() === "captions";
|
|
13186
13598
|
})) context.report({
|
|
13187
13599
|
node: node.name,
|
|
13188
|
-
message: MESSAGE$
|
|
13600
|
+
message: MESSAGE$38
|
|
13189
13601
|
});
|
|
13190
13602
|
} };
|
|
13191
13603
|
}
|
|
@@ -14986,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
14986
15398
|
});
|
|
14987
15399
|
//#endregion
|
|
14988
15400
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
14989
|
-
const MESSAGE$
|
|
15401
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
14990
15402
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
14991
15403
|
const noAccessKey = defineRule({
|
|
14992
15404
|
id: "no-access-key",
|
|
@@ -15003,7 +15415,7 @@ const noAccessKey = defineRule({
|
|
|
15003
15415
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15004
15416
|
context.report({
|
|
15005
15417
|
node: accessKey,
|
|
15006
|
-
message: MESSAGE$
|
|
15418
|
+
message: MESSAGE$37
|
|
15007
15419
|
});
|
|
15008
15420
|
return;
|
|
15009
15421
|
}
|
|
@@ -15013,7 +15425,7 @@ const noAccessKey = defineRule({
|
|
|
15013
15425
|
if (isUndefinedIdentifier(expression)) return;
|
|
15014
15426
|
context.report({
|
|
15015
15427
|
node: accessKey,
|
|
15016
|
-
message: MESSAGE$
|
|
15428
|
+
message: MESSAGE$37
|
|
15017
15429
|
});
|
|
15018
15430
|
}
|
|
15019
15431
|
} })
|
|
@@ -15496,7 +15908,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15496
15908
|
});
|
|
15497
15909
|
//#endregion
|
|
15498
15910
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15499
|
-
const MESSAGE$
|
|
15911
|
+
const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15500
15912
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15501
15913
|
id: "no-aria-hidden-on-focusable",
|
|
15502
15914
|
title: "aria-hidden on focusable element",
|
|
@@ -15523,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15523
15935
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15524
15936
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15525
15937
|
node: ariaHidden,
|
|
15526
|
-
message: MESSAGE$
|
|
15938
|
+
message: MESSAGE$36
|
|
15527
15939
|
});
|
|
15528
15940
|
} })
|
|
15529
15941
|
});
|
|
@@ -15891,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
15891
16303
|
});
|
|
15892
16304
|
//#endregion
|
|
15893
16305
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
15894
|
-
const MESSAGE$
|
|
16306
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
15895
16307
|
const SECOND_INDEX_METHODS = new Set([
|
|
15896
16308
|
"every",
|
|
15897
16309
|
"filter",
|
|
@@ -16095,7 +16507,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16095
16507
|
}
|
|
16096
16508
|
context.report({
|
|
16097
16509
|
node: keyAttribute,
|
|
16098
|
-
message: MESSAGE$
|
|
16510
|
+
message: MESSAGE$35
|
|
16099
16511
|
});
|
|
16100
16512
|
},
|
|
16101
16513
|
CallExpression(node) {
|
|
@@ -16115,15 +16527,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16115
16527
|
if (propName !== "key") continue;
|
|
16116
16528
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16117
16529
|
node: property,
|
|
16118
|
-
message: MESSAGE$
|
|
16530
|
+
message: MESSAGE$35
|
|
16119
16531
|
});
|
|
16120
16532
|
}
|
|
16121
16533
|
}
|
|
16122
16534
|
})
|
|
16123
16535
|
});
|
|
16124
16536
|
//#endregion
|
|
16537
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16538
|
+
const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16539
|
+
const noAsyncEffectCallback = defineRule({
|
|
16540
|
+
id: "no-async-effect-callback",
|
|
16541
|
+
title: "Async effect callback",
|
|
16542
|
+
severity: "warn",
|
|
16543
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16544
|
+
create: (context) => ({ CallExpression(node) {
|
|
16545
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16546
|
+
const callback = getEffectCallback(node);
|
|
16547
|
+
if (!callback) return;
|
|
16548
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16549
|
+
if (!callback.async) return;
|
|
16550
|
+
context.report({
|
|
16551
|
+
node: callback,
|
|
16552
|
+
message: MESSAGE$34
|
|
16553
|
+
});
|
|
16554
|
+
} })
|
|
16555
|
+
});
|
|
16556
|
+
//#endregion
|
|
16125
16557
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16126
|
-
const MESSAGE$
|
|
16558
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16127
16559
|
const resolveSettings$21 = (settings) => {
|
|
16128
16560
|
const reactDoctor = settings?.["react-doctor"];
|
|
16129
16561
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16179,7 +16611,7 @@ const noAutofocus = defineRule({
|
|
|
16179
16611
|
}
|
|
16180
16612
|
context.report({
|
|
16181
16613
|
node: autoFocusAttribute,
|
|
16182
|
-
message: MESSAGE$
|
|
16614
|
+
message: MESSAGE$33
|
|
16183
16615
|
});
|
|
16184
16616
|
} };
|
|
16185
16617
|
}
|
|
@@ -16429,6 +16861,109 @@ const noBarrelImport = defineRule({
|
|
|
16429
16861
|
}
|
|
16430
16862
|
});
|
|
16431
16863
|
//#endregion
|
|
16864
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16865
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16866
|
+
"FunctionDeclaration",
|
|
16867
|
+
"FunctionExpression",
|
|
16868
|
+
"ArrowFunctionExpression",
|
|
16869
|
+
"ClassDeclaration",
|
|
16870
|
+
"ClassExpression"
|
|
16871
|
+
]);
|
|
16872
|
+
const isReactImport$1 = (symbol) => {
|
|
16873
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16874
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16875
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16876
|
+
return importDeclaration.source.value === "react";
|
|
16877
|
+
};
|
|
16878
|
+
const getImportedName = (symbol) => {
|
|
16879
|
+
if (symbol.kind !== "import") return null;
|
|
16880
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16881
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16882
|
+
};
|
|
16883
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16884
|
+
if (symbol.kind !== "import") return false;
|
|
16885
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16886
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16887
|
+
};
|
|
16888
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16889
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16890
|
+
const symbol = scopes.symbolFor(callee);
|
|
16891
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16892
|
+
};
|
|
16893
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16894
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16895
|
+
if (callee.computed) return false;
|
|
16896
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16897
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16898
|
+
if (callee.property.name !== "createElement") return false;
|
|
16899
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16900
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16901
|
+
};
|
|
16902
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16903
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16904
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16905
|
+
};
|
|
16906
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16907
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16908
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16909
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16910
|
+
const nodeRecord = node;
|
|
16911
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16912
|
+
if (key === "parent") continue;
|
|
16913
|
+
const child = nodeRecord[key];
|
|
16914
|
+
if (Array.isArray(child)) {
|
|
16915
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16916
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16917
|
+
}
|
|
16918
|
+
return false;
|
|
16919
|
+
};
|
|
16920
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16921
|
+
//#endregion
|
|
16922
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16923
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16924
|
+
//#endregion
|
|
16925
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16926
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16927
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16928
|
+
const declaration = symbol.declarationNode;
|
|
16929
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16930
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16931
|
+
return false;
|
|
16932
|
+
};
|
|
16933
|
+
const noCallComponentAsFunction = defineRule({
|
|
16934
|
+
id: "no-call-component-as-function",
|
|
16935
|
+
title: "Component called as a function",
|
|
16936
|
+
severity: "warn",
|
|
16937
|
+
tags: ["test-noise"],
|
|
16938
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16939
|
+
create: (context) => {
|
|
16940
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16941
|
+
const candidateCalls = [];
|
|
16942
|
+
return {
|
|
16943
|
+
JSXOpeningElement(node) {
|
|
16944
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16945
|
+
},
|
|
16946
|
+
CallExpression(node) {
|
|
16947
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16948
|
+
node,
|
|
16949
|
+
callee: node.callee,
|
|
16950
|
+
name: node.callee.name
|
|
16951
|
+
});
|
|
16952
|
+
},
|
|
16953
|
+
"Program:exit"() {
|
|
16954
|
+
for (const candidate of candidateCalls) {
|
|
16955
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16956
|
+
if (!symbol) continue;
|
|
16957
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16958
|
+
node: candidate.node,
|
|
16959
|
+
message: message(candidate.name)
|
|
16960
|
+
});
|
|
16961
|
+
}
|
|
16962
|
+
}
|
|
16963
|
+
};
|
|
16964
|
+
}
|
|
16965
|
+
});
|
|
16966
|
+
//#endregion
|
|
16432
16967
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16433
16968
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16434
16969
|
//#endregion
|
|
@@ -16580,7 +17115,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16580
17115
|
});
|
|
16581
17116
|
//#endregion
|
|
16582
17117
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16583
|
-
const MESSAGE$
|
|
17118
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16584
17119
|
const noChildrenProp = defineRule({
|
|
16585
17120
|
id: "no-children-prop",
|
|
16586
17121
|
title: "Children passed as a prop",
|
|
@@ -16592,7 +17127,7 @@ const noChildrenProp = defineRule({
|
|
|
16592
17127
|
if (node.name.name !== "children") return;
|
|
16593
17128
|
context.report({
|
|
16594
17129
|
node: node.name,
|
|
16595
|
-
message: MESSAGE$
|
|
17130
|
+
message: MESSAGE$32
|
|
16596
17131
|
});
|
|
16597
17132
|
},
|
|
16598
17133
|
CallExpression(node) {
|
|
@@ -16605,7 +17140,7 @@ const noChildrenProp = defineRule({
|
|
|
16605
17140
|
const propertyKey = property.key;
|
|
16606
17141
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16607
17142
|
node: propertyKey,
|
|
16608
|
-
message: MESSAGE$
|
|
17143
|
+
message: MESSAGE$32
|
|
16609
17144
|
});
|
|
16610
17145
|
}
|
|
16611
17146
|
}
|
|
@@ -16613,7 +17148,7 @@ const noChildrenProp = defineRule({
|
|
|
16613
17148
|
});
|
|
16614
17149
|
//#endregion
|
|
16615
17150
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16616
|
-
const MESSAGE$
|
|
17151
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16617
17152
|
const noCloneElement = defineRule({
|
|
16618
17153
|
id: "no-clone-element",
|
|
16619
17154
|
title: "cloneElement makes child props fragile",
|
|
@@ -16626,7 +17161,7 @@ const noCloneElement = defineRule({
|
|
|
16626
17161
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16627
17162
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16628
17163
|
node: callee,
|
|
16629
|
-
message: MESSAGE$
|
|
17164
|
+
message: MESSAGE$31
|
|
16630
17165
|
});
|
|
16631
17166
|
return;
|
|
16632
17167
|
}
|
|
@@ -16639,7 +17174,7 @@ const noCloneElement = defineRule({
|
|
|
16639
17174
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16640
17175
|
context.report({
|
|
16641
17176
|
node: callee,
|
|
16642
|
-
message: MESSAGE$
|
|
17177
|
+
message: MESSAGE$31
|
|
16643
17178
|
});
|
|
16644
17179
|
}
|
|
16645
17180
|
} })
|
|
@@ -16688,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16688
17223
|
};
|
|
16689
17224
|
//#endregion
|
|
16690
17225
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16691
|
-
const MESSAGE$
|
|
17226
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16692
17227
|
const CONTEXT_MODULES = [
|
|
16693
17228
|
"react",
|
|
16694
17229
|
"use-context-selector",
|
|
@@ -16724,7 +17259,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16724
17259
|
if (!componentOrHookName) return;
|
|
16725
17260
|
context.report({
|
|
16726
17261
|
node,
|
|
16727
|
-
message: `${MESSAGE$
|
|
17262
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17263
|
+
});
|
|
17264
|
+
} })
|
|
17265
|
+
});
|
|
17266
|
+
//#endregion
|
|
17267
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17268
|
+
const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17269
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17270
|
+
id: "no-create-ref-in-function-component",
|
|
17271
|
+
title: "createRef in function component",
|
|
17272
|
+
severity: "warn",
|
|
17273
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17274
|
+
create: (context) => ({ CallExpression(node) {
|
|
17275
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17276
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17277
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17278
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17279
|
+
}
|
|
17280
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17281
|
+
if (!enclosingFunction) return;
|
|
17282
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17283
|
+
if (!displayName) return;
|
|
17284
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17285
|
+
context.report({
|
|
17286
|
+
node,
|
|
17287
|
+
message: MESSAGE$29
|
|
16728
17288
|
});
|
|
16729
17289
|
} })
|
|
16730
17290
|
});
|
|
@@ -16864,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
16864
17424
|
});
|
|
16865
17425
|
//#endregion
|
|
16866
17426
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
16867
|
-
const MESSAGE$
|
|
17427
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
16868
17428
|
const noDanger = defineRule({
|
|
16869
17429
|
id: "no-danger",
|
|
16870
17430
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -16877,7 +17437,7 @@ const noDanger = defineRule({
|
|
|
16877
17437
|
if (!propAttribute) return;
|
|
16878
17438
|
context.report({
|
|
16879
17439
|
node: propAttribute.name,
|
|
16880
|
-
message: MESSAGE$
|
|
17440
|
+
message: MESSAGE$28
|
|
16881
17441
|
});
|
|
16882
17442
|
},
|
|
16883
17443
|
CallExpression(node) {
|
|
@@ -16889,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
16889
17449
|
const propertyKey = property.key;
|
|
16890
17450
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
16891
17451
|
node: propertyKey,
|
|
16892
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$28
|
|
16893
17453
|
});
|
|
16894
17454
|
}
|
|
16895
17455
|
}
|
|
@@ -16897,7 +17457,7 @@ const noDanger = defineRule({
|
|
|
16897
17457
|
});
|
|
16898
17458
|
//#endregion
|
|
16899
17459
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
16900
|
-
const MESSAGE$
|
|
17460
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
16901
17461
|
const isLineBreak = (child) => {
|
|
16902
17462
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
16903
17463
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -16967,7 +17527,7 @@ const noDangerWithChildren = defineRule({
|
|
|
16967
17527
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
16968
17528
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
16969
17529
|
node: opening,
|
|
16970
|
-
message: MESSAGE$
|
|
17530
|
+
message: MESSAGE$27
|
|
16971
17531
|
});
|
|
16972
17532
|
},
|
|
16973
17533
|
CallExpression(node) {
|
|
@@ -16979,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
16979
17539
|
if (!propsShape.hasDangerously) return;
|
|
16980
17540
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
16981
17541
|
node,
|
|
16982
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$27
|
|
16983
17543
|
});
|
|
16984
17544
|
}
|
|
16985
17545
|
})
|
|
@@ -17556,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17556
18116
|
//#endregion
|
|
17557
18117
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17558
18118
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17559
|
-
const MESSAGE$
|
|
18119
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17560
18120
|
const resolveSettings$20 = (settings) => {
|
|
17561
18121
|
const reactDoctor = settings?.["react-doctor"];
|
|
17562
18122
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17575,7 +18135,7 @@ const noDidMountSetState = defineRule({
|
|
|
17575
18135
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17576
18136
|
context.report({
|
|
17577
18137
|
node: node.callee,
|
|
17578
|
-
message: MESSAGE$
|
|
18138
|
+
message: MESSAGE$26
|
|
17579
18139
|
});
|
|
17580
18140
|
} };
|
|
17581
18141
|
}
|
|
@@ -17583,7 +18143,7 @@ const noDidMountSetState = defineRule({
|
|
|
17583
18143
|
//#endregion
|
|
17584
18144
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17585
18145
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17586
|
-
const MESSAGE$
|
|
18146
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17587
18147
|
const resolveSettings$19 = (settings) => {
|
|
17588
18148
|
const reactDoctor = settings?.["react-doctor"];
|
|
17589
18149
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17602,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17602
18162
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17603
18163
|
context.report({
|
|
17604
18164
|
node: node.callee,
|
|
17605
|
-
message: MESSAGE$
|
|
18165
|
+
message: MESSAGE$25
|
|
17606
18166
|
});
|
|
17607
18167
|
} };
|
|
17608
18168
|
}
|
|
@@ -17625,7 +18185,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17625
18185
|
};
|
|
17626
18186
|
//#endregion
|
|
17627
18187
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17628
|
-
const MESSAGE$
|
|
18188
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17629
18189
|
const shouldIgnoreMutation = (node) => {
|
|
17630
18190
|
let isConstructor = false;
|
|
17631
18191
|
let isInsideCallExpression = false;
|
|
@@ -17647,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17647
18207
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17648
18208
|
context.report({
|
|
17649
18209
|
node: reportNode,
|
|
17650
|
-
message: MESSAGE$
|
|
18210
|
+
message: MESSAGE$24
|
|
17651
18211
|
});
|
|
17652
18212
|
};
|
|
17653
18213
|
const noDirectMutationState = defineRule({
|
|
@@ -17857,6 +18417,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
17857
18417
|
} })
|
|
17858
18418
|
});
|
|
17859
18419
|
//#endregion
|
|
18420
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18421
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18422
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18423
|
+
const noDocumentWrite = defineRule({
|
|
18424
|
+
id: "no-document-write",
|
|
18425
|
+
title: "document.write/writeln",
|
|
18426
|
+
severity: "warn",
|
|
18427
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18428
|
+
create: (context) => ({ CallExpression(node) {
|
|
18429
|
+
const callee = node.callee;
|
|
18430
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18431
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18432
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18433
|
+
context.report({
|
|
18434
|
+
node,
|
|
18435
|
+
message: MESSAGE$23
|
|
18436
|
+
});
|
|
18437
|
+
} })
|
|
18438
|
+
});
|
|
18439
|
+
//#endregion
|
|
17860
18440
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
17861
18441
|
const noDynamicImportPath = defineRule({
|
|
17862
18442
|
id: "no-dynamic-import-path",
|
|
@@ -19235,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19235
19815
|
"ReactDOM",
|
|
19236
19816
|
"ReactDom"
|
|
19237
19817
|
]);
|
|
19238
|
-
const MESSAGE$
|
|
19818
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19239
19819
|
const noFindDomNode = defineRule({
|
|
19240
19820
|
id: "no-find-dom-node",
|
|
19241
19821
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19246,7 +19826,7 @@ const noFindDomNode = defineRule({
|
|
|
19246
19826
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19247
19827
|
context.report({
|
|
19248
19828
|
node: callee,
|
|
19249
|
-
message: MESSAGE$
|
|
19829
|
+
message: MESSAGE$22
|
|
19250
19830
|
});
|
|
19251
19831
|
return;
|
|
19252
19832
|
}
|
|
@@ -19257,7 +19837,7 @@ const noFindDomNode = defineRule({
|
|
|
19257
19837
|
if (callee.property.name !== "findDOMNode") return;
|
|
19258
19838
|
context.report({
|
|
19259
19839
|
node: callee.property,
|
|
19260
|
-
message: MESSAGE$
|
|
19840
|
+
message: MESSAGE$22
|
|
19261
19841
|
});
|
|
19262
19842
|
}
|
|
19263
19843
|
} })
|
|
@@ -19320,64 +19900,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19320
19900
|
} })
|
|
19321
19901
|
});
|
|
19322
19902
|
//#endregion
|
|
19323
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19324
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19325
|
-
"FunctionDeclaration",
|
|
19326
|
-
"FunctionExpression",
|
|
19327
|
-
"ArrowFunctionExpression",
|
|
19328
|
-
"ClassDeclaration",
|
|
19329
|
-
"ClassExpression"
|
|
19330
|
-
]);
|
|
19331
|
-
const isReactImport$1 = (symbol) => {
|
|
19332
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19333
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19334
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19335
|
-
return importDeclaration.source.value === "react";
|
|
19336
|
-
};
|
|
19337
|
-
const getImportedName = (symbol) => {
|
|
19338
|
-
if (symbol.kind !== "import") return null;
|
|
19339
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19340
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19341
|
-
};
|
|
19342
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19343
|
-
if (symbol.kind !== "import") return false;
|
|
19344
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19345
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19346
|
-
};
|
|
19347
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19348
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19349
|
-
const symbol = scopes.symbolFor(callee);
|
|
19350
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19351
|
-
};
|
|
19352
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19353
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19354
|
-
if (callee.computed) return false;
|
|
19355
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19356
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19357
|
-
if (callee.property.name !== "createElement") return false;
|
|
19358
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19359
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19360
|
-
};
|
|
19361
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19362
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19363
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19364
|
-
};
|
|
19365
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19366
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19367
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19368
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19369
|
-
const nodeRecord = node;
|
|
19370
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19371
|
-
if (key === "parent") continue;
|
|
19372
|
-
const child = nodeRecord[key];
|
|
19373
|
-
if (Array.isArray(child)) {
|
|
19374
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19375
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19376
|
-
}
|
|
19377
|
-
return false;
|
|
19378
|
-
};
|
|
19379
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19380
|
-
//#endregion
|
|
19381
19903
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19382
19904
|
const noGiantComponent = defineRule({
|
|
19383
19905
|
id: "no-giant-component",
|
|
@@ -19556,6 +20078,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19556
20078
|
} })
|
|
19557
20079
|
});
|
|
19558
20080
|
//#endregion
|
|
20081
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20082
|
+
const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20083
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20084
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20085
|
+
title: "Lazy image with high fetchPriority",
|
|
20086
|
+
severity: "warn",
|
|
20087
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
20088
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20089
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20090
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20091
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20092
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20093
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20094
|
+
context.report({
|
|
20095
|
+
node: node.name,
|
|
20096
|
+
message: MESSAGE$21
|
|
20097
|
+
});
|
|
20098
|
+
} })
|
|
20099
|
+
});
|
|
20100
|
+
//#endregion
|
|
19559
20101
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19560
20102
|
const noInitializeState = defineRule({
|
|
19561
20103
|
id: "no-initialize-state",
|
|
@@ -19785,8 +20327,31 @@ const noIsMounted = defineRule({
|
|
|
19785
20327
|
} })
|
|
19786
20328
|
});
|
|
19787
20329
|
//#endregion
|
|
20330
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20331
|
+
const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20332
|
+
const isJsonMethodCall = (node, method) => {
|
|
20333
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20334
|
+
const callee = node.callee;
|
|
20335
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20336
|
+
};
|
|
20337
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20338
|
+
id: "no-json-parse-stringify-clone",
|
|
20339
|
+
title: "JSON parse/stringify deep clone",
|
|
20340
|
+
severity: "warn",
|
|
20341
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20342
|
+
create: (context) => ({ CallExpression(node) {
|
|
20343
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20344
|
+
const firstArgument = node.arguments?.[0];
|
|
20345
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20346
|
+
context.report({
|
|
20347
|
+
node,
|
|
20348
|
+
message: MESSAGE$20
|
|
20349
|
+
});
|
|
20350
|
+
} })
|
|
20351
|
+
});
|
|
20352
|
+
//#endregion
|
|
19788
20353
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19789
|
-
const MESSAGE$
|
|
20354
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19790
20355
|
const isJsxElementTypeReference = (node) => {
|
|
19791
20356
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
19792
20357
|
const typeName = node.typeName;
|
|
@@ -19803,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
19803
20368
|
if (!typeAnnotation) return;
|
|
19804
20369
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
19805
20370
|
node: typeAnnotation,
|
|
19806
|
-
message: MESSAGE$
|
|
20371
|
+
message: MESSAGE$19
|
|
19807
20372
|
});
|
|
19808
20373
|
};
|
|
19809
20374
|
const noJsxElementType = defineRule({
|
|
@@ -20107,9 +20672,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20107
20672
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20108
20673
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20109
20674
|
//#endregion
|
|
20110
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20111
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20112
|
-
//#endregion
|
|
20113
20675
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20114
20676
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20115
20677
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -20261,7 +20823,7 @@ const noMoment = defineRule({
|
|
|
20261
20823
|
});
|
|
20262
20824
|
//#endregion
|
|
20263
20825
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20264
|
-
const MESSAGE$
|
|
20826
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20265
20827
|
const resolveSettings$16 = (settings) => {
|
|
20266
20828
|
const reactDoctor = settings?.["react-doctor"];
|
|
20267
20829
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -20583,7 +21145,7 @@ const noMultiComp = defineRule({
|
|
|
20583
21145
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
20584
21146
|
for (const component of flagged.slice(1)) context.report({
|
|
20585
21147
|
node: component.reportNode,
|
|
20586
|
-
message: MESSAGE$
|
|
21148
|
+
message: MESSAGE$18
|
|
20587
21149
|
});
|
|
20588
21150
|
} };
|
|
20589
21151
|
}
|
|
@@ -20751,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
20751
21313
|
};
|
|
20752
21314
|
//#endregion
|
|
20753
21315
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
20754
|
-
const MESSAGE$
|
|
21316
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
20755
21317
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
20756
21318
|
"copyWithin",
|
|
20757
21319
|
"fill",
|
|
@@ -20961,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
20961
21523
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
20962
21524
|
context.report({
|
|
20963
21525
|
node: options.crossFileConsumerCallSite,
|
|
20964
|
-
message: `${MESSAGE$
|
|
21526
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
20965
21527
|
});
|
|
20966
21528
|
return;
|
|
20967
21529
|
}
|
|
@@ -20970,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
20970
21532
|
reportedNodes.add(mutation.node);
|
|
20971
21533
|
context.report({
|
|
20972
21534
|
node: mutation.node,
|
|
20973
|
-
message: MESSAGE$
|
|
21535
|
+
message: MESSAGE$17
|
|
20974
21536
|
});
|
|
20975
21537
|
}
|
|
20976
21538
|
};
|
|
@@ -21242,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21242
21804
|
});
|
|
21243
21805
|
//#endregion
|
|
21244
21806
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21245
|
-
const MESSAGE$
|
|
21807
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21246
21808
|
const resolveSettings$14 = (settings) => {
|
|
21247
21809
|
const reactDoctor = settings?.["react-doctor"];
|
|
21248
21810
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21270,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21270
21832
|
if (numeric === null) {
|
|
21271
21833
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21272
21834
|
node: tabIndex,
|
|
21273
|
-
message: MESSAGE$
|
|
21835
|
+
message: MESSAGE$16
|
|
21274
21836
|
});
|
|
21275
21837
|
return;
|
|
21276
21838
|
}
|
|
@@ -21283,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21283
21845
|
if (!roleAttribute) {
|
|
21284
21846
|
context.report({
|
|
21285
21847
|
node: tabIndex,
|
|
21286
|
-
message: MESSAGE$
|
|
21848
|
+
message: MESSAGE$16
|
|
21287
21849
|
});
|
|
21288
21850
|
return;
|
|
21289
21851
|
}
|
|
@@ -21297,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21297
21859
|
}
|
|
21298
21860
|
context.report({
|
|
21299
21861
|
node: tabIndex,
|
|
21300
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
21301
21863
|
});
|
|
21302
21864
|
} };
|
|
21303
21865
|
}
|
|
@@ -21988,7 +22550,7 @@ const noRandomKey = defineRule({
|
|
|
21988
22550
|
});
|
|
21989
22551
|
//#endregion
|
|
21990
22552
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
21991
|
-
const MESSAGE$
|
|
22553
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
21992
22554
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
21993
22555
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
21994
22556
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22014,13 +22576,13 @@ const noReactChildren = defineRule({
|
|
|
22014
22576
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22015
22577
|
context.report({
|
|
22016
22578
|
node: calleeOuter,
|
|
22017
|
-
message: MESSAGE$
|
|
22579
|
+
message: MESSAGE$15
|
|
22018
22580
|
});
|
|
22019
22581
|
return;
|
|
22020
22582
|
}
|
|
22021
22583
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22022
22584
|
node: calleeOuter,
|
|
22023
|
-
message: MESSAGE$
|
|
22585
|
+
message: MESSAGE$15
|
|
22024
22586
|
});
|
|
22025
22587
|
} })
|
|
22026
22588
|
});
|
|
@@ -22343,7 +22905,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22343
22905
|
});
|
|
22344
22906
|
//#endregion
|
|
22345
22907
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22346
|
-
const MESSAGE$
|
|
22908
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22347
22909
|
const isReactDomRenderCall = (node) => {
|
|
22348
22910
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22349
22911
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22367,7 +22929,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22367
22929
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22368
22930
|
context.report({
|
|
22369
22931
|
node: node.callee,
|
|
22370
|
-
message: MESSAGE$
|
|
22932
|
+
message: MESSAGE$14
|
|
22371
22933
|
});
|
|
22372
22934
|
} })
|
|
22373
22935
|
});
|
|
@@ -22527,11 +23089,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22527
23089
|
return "unknown";
|
|
22528
23090
|
};
|
|
22529
23091
|
//#endregion
|
|
22530
|
-
//#region src/plugin/utils/
|
|
22531
|
-
const
|
|
22532
|
-
|
|
23092
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23093
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23094
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23095
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23096
|
+
if (!words) return [];
|
|
23097
|
+
return words.map((word) => word.toLowerCase());
|
|
22533
23098
|
};
|
|
22534
23099
|
//#endregion
|
|
23100
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23101
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23102
|
+
//#endregion
|
|
22535
23103
|
//#region src/plugin/constants/tanstack.ts
|
|
22536
23104
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22537
23105
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23059,7 +23627,7 @@ const getParentComponent = (node) => {
|
|
|
23059
23627
|
};
|
|
23060
23628
|
//#endregion
|
|
23061
23629
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23062
|
-
const MESSAGE$
|
|
23630
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23063
23631
|
const noSetState = defineRule({
|
|
23064
23632
|
id: "no-set-state",
|
|
23065
23633
|
title: "Local class state forbidden",
|
|
@@ -23074,7 +23642,7 @@ const noSetState = defineRule({
|
|
|
23074
23642
|
if (!getParentComponent(node)) return;
|
|
23075
23643
|
context.report({
|
|
23076
23644
|
node: node.callee,
|
|
23077
|
-
message: MESSAGE$
|
|
23645
|
+
message: MESSAGE$13
|
|
23078
23646
|
});
|
|
23079
23647
|
} })
|
|
23080
23648
|
});
|
|
@@ -23236,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23236
23804
|
};
|
|
23237
23805
|
//#endregion
|
|
23238
23806
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23239
|
-
const MESSAGE$
|
|
23807
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
23240
23808
|
const DEFAULT_HANDLERS = [
|
|
23241
23809
|
"onClick",
|
|
23242
23810
|
"onMouseDown",
|
|
@@ -23296,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23296
23864
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23297
23865
|
context.report({
|
|
23298
23866
|
node: node.name,
|
|
23299
|
-
message: MESSAGE$
|
|
23867
|
+
message: MESSAGE$12
|
|
23300
23868
|
});
|
|
23301
23869
|
return;
|
|
23302
23870
|
}
|
|
@@ -23306,19 +23874,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23306
23874
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23307
23875
|
context.report({
|
|
23308
23876
|
node: node.name,
|
|
23309
|
-
message: MESSAGE$
|
|
23877
|
+
message: MESSAGE$12
|
|
23310
23878
|
});
|
|
23311
23879
|
return;
|
|
23312
23880
|
}
|
|
23313
23881
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23314
23882
|
context.report({
|
|
23315
23883
|
node: node.name,
|
|
23316
|
-
message: MESSAGE$
|
|
23884
|
+
message: MESSAGE$12
|
|
23317
23885
|
});
|
|
23318
23886
|
} };
|
|
23319
23887
|
}
|
|
23320
23888
|
});
|
|
23321
23889
|
//#endregion
|
|
23890
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
23891
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
23892
|
+
"disabled",
|
|
23893
|
+
"checked",
|
|
23894
|
+
"readonly",
|
|
23895
|
+
"required",
|
|
23896
|
+
"selected",
|
|
23897
|
+
"multiple",
|
|
23898
|
+
"autofocus",
|
|
23899
|
+
"autoplay",
|
|
23900
|
+
"controls",
|
|
23901
|
+
"loop",
|
|
23902
|
+
"muted",
|
|
23903
|
+
"open",
|
|
23904
|
+
"reversed",
|
|
23905
|
+
"default",
|
|
23906
|
+
"novalidate",
|
|
23907
|
+
"formnovalidate",
|
|
23908
|
+
"playsinline",
|
|
23909
|
+
"itemscope",
|
|
23910
|
+
"allowfullscreen"
|
|
23911
|
+
]);
|
|
23912
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
23913
|
+
id: "no-string-false-on-boolean-attribute",
|
|
23914
|
+
title: "String true/false on a boolean attribute",
|
|
23915
|
+
severity: "warn",
|
|
23916
|
+
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
23917
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23918
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23919
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
23920
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
23921
|
+
for (const attribute of node.attributes) {
|
|
23922
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23923
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23924
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
23925
|
+
const value = getJsxPropStringValue(attribute);
|
|
23926
|
+
if (value !== "false" && value !== "true") continue;
|
|
23927
|
+
const attributeName = attribute.name.name;
|
|
23928
|
+
const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
|
|
23929
|
+
context.report({
|
|
23930
|
+
node: attribute,
|
|
23931
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
23932
|
+
});
|
|
23933
|
+
}
|
|
23934
|
+
} })
|
|
23935
|
+
});
|
|
23936
|
+
//#endregion
|
|
23322
23937
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23323
23938
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23324
23939
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23369,6 +23984,27 @@ const noStringRefs = defineRule({
|
|
|
23369
23984
|
}
|
|
23370
23985
|
});
|
|
23371
23986
|
//#endregion
|
|
23987
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
23988
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
23989
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
23990
|
+
const noSyncXhr = defineRule({
|
|
23991
|
+
id: "no-sync-xhr",
|
|
23992
|
+
title: "Synchronous XMLHttpRequest",
|
|
23993
|
+
severity: "warn",
|
|
23994
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
23995
|
+
create: (context) => ({ CallExpression(node) {
|
|
23996
|
+
const callee = node.callee;
|
|
23997
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
23998
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
23999
|
+
const asyncArgument = node.arguments?.[2];
|
|
24000
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24001
|
+
context.report({
|
|
24002
|
+
node,
|
|
24003
|
+
message: MESSAGE$11
|
|
24004
|
+
});
|
|
24005
|
+
} })
|
|
24006
|
+
});
|
|
24007
|
+
//#endregion
|
|
23372
24008
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23373
24009
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23374
24010
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -27035,6 +27671,8 @@ const publicEnvSecretName = defineRule({
|
|
|
27035
27671
|
});
|
|
27036
27672
|
//#endregion
|
|
27037
27673
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
27674
|
+
const TANSTACK_QUERY_PACKAGE_PATTERN = /^@tanstack\/[\w-]*query[\w-]*$/;
|
|
27675
|
+
const isTanstackQuerySource = (source) => TANSTACK_QUERY_PACKAGE_PATTERN.test(source) || source === "react-query";
|
|
27038
27676
|
const queryDestructureResult = defineRule({
|
|
27039
27677
|
id: "query-destructure-result",
|
|
27040
27678
|
title: "Whole query result subscribes to every field",
|
|
@@ -27047,6 +27685,8 @@ const queryDestructureResult = defineRule({
|
|
|
27047
27685
|
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
27048
27686
|
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
27049
27687
|
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
27688
|
+
const importSource = getImportSourceForName(node, calleeName);
|
|
27689
|
+
if (importSource !== null && !isTanstackQuerySource(importSource)) return;
|
|
27050
27690
|
context.report({
|
|
27051
27691
|
node: node.id,
|
|
27052
27692
|
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
@@ -27889,6 +28529,7 @@ const repositorySecretFile = defineRule({
|
|
|
27889
28529
|
id: "repository-secret-file",
|
|
27890
28530
|
title: "Secret file checked into repository",
|
|
27891
28531
|
severity: "error",
|
|
28532
|
+
committedFilesOnly: true,
|
|
27892
28533
|
recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
|
|
27893
28534
|
scan: (file) => {
|
|
27894
28535
|
if (!isRepositorySecretFilePath(file.relativePath)) return [];
|
|
@@ -27905,6 +28546,20 @@ const repositorySecretFile = defineRule({
|
|
|
27905
28546
|
}
|
|
27906
28547
|
});
|
|
27907
28548
|
//#endregion
|
|
28549
|
+
//#region src/plugin/rules/security-scan/request-body-mass-assignment.ts
|
|
28550
|
+
const REQUEST_INPUT_SOURCE = "(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)";
|
|
28551
|
+
const requestBodyMassAssignment = defineRule({
|
|
28552
|
+
id: "request-body-mass-assignment",
|
|
28553
|
+
title: "Request input spread without field allowlist",
|
|
28554
|
+
severity: "warn",
|
|
28555
|
+
recommendation: "Assign explicit, allowlisted fields (or validate with a strict schema and no `.passthrough()`) instead of spreading/merging request input. Otherwise the client can set ownership, role, or price columns (mass assignment) or pollute the prototype.",
|
|
28556
|
+
scan: scanByPattern({
|
|
28557
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
28558
|
+
pattern: [new RegExp(`\\.\\.\\.\\s*(?:${REQUEST_INPUT_SOURCE})`, "i"), new RegExp(`(?:Object\\.assign\\s*\\(|_\\.(?:merge|mergeWith|defaultsDeep)\\s*\\(|(?:^|[^.\\w])(?:merge|defaultsDeep)\\s*\\()[\\s\\S]{0,80}?(?:${REQUEST_INPUT_SOURCE})`, "i")],
|
|
28559
|
+
message: "Request input is spread or merged into an object without a field allowlist, enabling mass assignment (client-set owner/role/price fields) or prototype pollution."
|
|
28560
|
+
})
|
|
28561
|
+
});
|
|
28562
|
+
//#endregion
|
|
27908
28563
|
//#region src/plugin/utils/function-body-has-return-with-value.ts
|
|
27909
28564
|
const functionBodyHasReturnWithValue = (functionNode) => {
|
|
27910
28565
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
@@ -34330,6 +34985,17 @@ const scope = defineRule({
|
|
|
34330
34985
|
});
|
|
34331
34986
|
} })
|
|
34332
34987
|
});
|
|
34988
|
+
const secretInFallback = defineRule({
|
|
34989
|
+
id: "secret-in-fallback",
|
|
34990
|
+
title: "Hardcoded secret fallback for env var",
|
|
34991
|
+
severity: "error",
|
|
34992
|
+
recommendation: "Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",
|
|
34993
|
+
scan: scanByPattern({
|
|
34994
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
34995
|
+
pattern: /\bprocess\.env\.(?![A-Z0-9_]*(?:PUBLIC|PUBLISHABLE|ANON)\b)[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
34996
|
+
message: "A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset."
|
|
34997
|
+
})
|
|
34998
|
+
});
|
|
34333
34999
|
//#endregion
|
|
34334
35000
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
34335
35001
|
const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
@@ -34448,6 +35114,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34448
35114
|
}
|
|
34449
35115
|
});
|
|
34450
35116
|
//#endregion
|
|
35117
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
35118
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
35119
|
+
"signed",
|
|
35120
|
+
"logged",
|
|
35121
|
+
"sign"
|
|
35122
|
+
]);
|
|
35123
|
+
const mergeSignedInTokens = (tokens) => {
|
|
35124
|
+
const mergedTokens = [];
|
|
35125
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
35126
|
+
const currentToken = tokens[tokenIndex];
|
|
35127
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
35128
|
+
mergedTokens.push(`${currentToken}in`);
|
|
35129
|
+
tokenIndex += 1;
|
|
35130
|
+
continue;
|
|
35131
|
+
}
|
|
35132
|
+
mergedTokens.push(currentToken);
|
|
35133
|
+
}
|
|
35134
|
+
return mergedTokens;
|
|
35135
|
+
};
|
|
35136
|
+
const isAuthGuardName = (calleeName) => {
|
|
35137
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
35138
|
+
if (tokens.length === 0) return false;
|
|
35139
|
+
let hasAssertiveVerb = false;
|
|
35140
|
+
let hasGetterVerb = false;
|
|
35141
|
+
let hasQualifier = false;
|
|
35142
|
+
let hasStrongNoun = false;
|
|
35143
|
+
let hasWeakNoun = false;
|
|
35144
|
+
for (const token of tokens) {
|
|
35145
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35146
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35147
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35148
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35149
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35150
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35151
|
+
}
|
|
35152
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35153
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35154
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35155
|
+
return false;
|
|
35156
|
+
};
|
|
35157
|
+
//#endregion
|
|
34451
35158
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34452
35159
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34453
35160
|
if (!node) return false;
|
|
@@ -34490,9 +35197,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34490
35197
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34491
35198
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34492
35199
|
if (!calleeNode) return null;
|
|
34493
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35200
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35201
|
+
const calleeName = calleeNode.name;
|
|
35202
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35203
|
+
}
|
|
34494
35204
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34495
35205
|
const methodName = calleeNode.property.name;
|
|
35206
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34496
35207
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34497
35208
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34498
35209
|
return methodName;
|
|
@@ -34869,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
34869
35580
|
const collectDeclaredNames = (declaration) => {
|
|
34870
35581
|
const names = /* @__PURE__ */ new Set();
|
|
34871
35582
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
34872
|
-
for (const declarator of declaration.declarations ?? [])
|
|
34873
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34874
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
34875
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
34876
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
34877
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
34878
|
-
}
|
|
35583
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
34879
35584
|
return names;
|
|
34880
35585
|
};
|
|
34881
35586
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -34885,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
34885
35590
|
};
|
|
34886
35591
|
const declarationReadsAnyName = (declaration, names) => {
|
|
34887
35592
|
if (names.size === 0) return false;
|
|
35593
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
34888
35594
|
let didRead = false;
|
|
34889
|
-
|
|
34890
|
-
if (
|
|
34891
|
-
|
|
34892
|
-
|
|
35595
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35596
|
+
if (!declarator.init) continue;
|
|
35597
|
+
walkAst(declarator.init, (child) => {
|
|
35598
|
+
if (didRead) return;
|
|
35599
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35600
|
+
});
|
|
35601
|
+
}
|
|
34893
35602
|
return didRead;
|
|
34894
35603
|
};
|
|
34895
35604
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -35139,8 +35848,11 @@ const supabaseClientOwnedAuthzField = defineRule({
|
|
|
35139
35848
|
})
|
|
35140
35849
|
});
|
|
35141
35850
|
//#endregion
|
|
35851
|
+
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|
|
35852
|
+
const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
|
|
35853
|
+
//#endregion
|
|
35142
35854
|
//#region src/plugin/rules/security-scan/utils/is-sql-path.ts
|
|
35143
|
-
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") ||
|
|
35855
|
+
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
|
|
35144
35856
|
const supabaseRlsPolicyRisk = defineRule({
|
|
35145
35857
|
id: "supabase-rls-policy-risk",
|
|
35146
35858
|
title: "Permissive Supabase RLS policy",
|
|
@@ -35158,6 +35870,210 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
35158
35870
|
})
|
|
35159
35871
|
});
|
|
35160
35872
|
//#endregion
|
|
35873
|
+
//#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
|
|
35874
|
+
const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
|
|
35875
|
+
const CODE_BODY_KEYWORDS = new Set([
|
|
35876
|
+
"do",
|
|
35877
|
+
"plpgsql",
|
|
35878
|
+
"sql",
|
|
35879
|
+
"plpython3u",
|
|
35880
|
+
"plpythonu",
|
|
35881
|
+
"plperl",
|
|
35882
|
+
"plperlu",
|
|
35883
|
+
"plv8"
|
|
35884
|
+
]);
|
|
35885
|
+
const precedingKeyword = (content, beforeIndex) => {
|
|
35886
|
+
let lookBack = beforeIndex - 1;
|
|
35887
|
+
while (lookBack >= 0 && /\s/.test(content[lookBack] ?? "")) lookBack -= 1;
|
|
35888
|
+
let wordStart = lookBack;
|
|
35889
|
+
while (wordStart >= 0 && /[A-Za-z0-9_]/.test(content[wordStart] ?? "")) wordStart -= 1;
|
|
35890
|
+
return content.slice(wordStart + 1, lookBack + 1).toLowerCase();
|
|
35891
|
+
};
|
|
35892
|
+
const blankCodeBodyInterior = (content, characters, start, end) => {
|
|
35893
|
+
let index = start;
|
|
35894
|
+
let inExecuteStatement = false;
|
|
35895
|
+
while (index < end) {
|
|
35896
|
+
const character = content[index];
|
|
35897
|
+
if (character === ";") {
|
|
35898
|
+
inExecuteStatement = false;
|
|
35899
|
+
index += 1;
|
|
35900
|
+
continue;
|
|
35901
|
+
}
|
|
35902
|
+
if (/[A-Za-z_]/.test(character)) {
|
|
35903
|
+
let wordEnd = index;
|
|
35904
|
+
while (wordEnd < end && /[A-Za-z0-9_]/.test(content[wordEnd] ?? "")) wordEnd += 1;
|
|
35905
|
+
if (content.slice(index, wordEnd).toLowerCase() === "execute") inExecuteStatement = true;
|
|
35906
|
+
index = wordEnd;
|
|
35907
|
+
continue;
|
|
35908
|
+
}
|
|
35909
|
+
if (character === "'") {
|
|
35910
|
+
const keepVisible = inExecuteStatement;
|
|
35911
|
+
if (!keepVisible) characters[index] = " ";
|
|
35912
|
+
index += 1;
|
|
35913
|
+
while (index < end) {
|
|
35914
|
+
if (content[index] === "'") {
|
|
35915
|
+
if (content[index + 1] === "'") {
|
|
35916
|
+
if (!keepVisible) {
|
|
35917
|
+
characters[index] = " ";
|
|
35918
|
+
characters[index + 1] = " ";
|
|
35919
|
+
}
|
|
35920
|
+
index += 2;
|
|
35921
|
+
continue;
|
|
35922
|
+
}
|
|
35923
|
+
if (!keepVisible) characters[index] = " ";
|
|
35924
|
+
index += 1;
|
|
35925
|
+
break;
|
|
35926
|
+
}
|
|
35927
|
+
if (!keepVisible && content[index] !== "\n") characters[index] = " ";
|
|
35928
|
+
index += 1;
|
|
35929
|
+
}
|
|
35930
|
+
continue;
|
|
35931
|
+
}
|
|
35932
|
+
if (character === "\"") {
|
|
35933
|
+
index += 1;
|
|
35934
|
+
while (index < end) {
|
|
35935
|
+
if (content[index] === "\"") {
|
|
35936
|
+
if (content[index + 1] === "\"") {
|
|
35937
|
+
index += 2;
|
|
35938
|
+
continue;
|
|
35939
|
+
}
|
|
35940
|
+
index += 1;
|
|
35941
|
+
break;
|
|
35942
|
+
}
|
|
35943
|
+
index += 1;
|
|
35944
|
+
}
|
|
35945
|
+
continue;
|
|
35946
|
+
}
|
|
35947
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35948
|
+
while (index < end && content[index] !== "\n") {
|
|
35949
|
+
characters[index] = " ";
|
|
35950
|
+
index += 1;
|
|
35951
|
+
}
|
|
35952
|
+
continue;
|
|
35953
|
+
}
|
|
35954
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35955
|
+
while (index < end) {
|
|
35956
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35957
|
+
characters[index] = " ";
|
|
35958
|
+
characters[index + 1] = " ";
|
|
35959
|
+
index += 2;
|
|
35960
|
+
break;
|
|
35961
|
+
}
|
|
35962
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35963
|
+
index += 1;
|
|
35964
|
+
}
|
|
35965
|
+
continue;
|
|
35966
|
+
}
|
|
35967
|
+
index += 1;
|
|
35968
|
+
}
|
|
35969
|
+
};
|
|
35970
|
+
const sanitizeSqlForScan = (content) => {
|
|
35971
|
+
const characters = content.split("");
|
|
35972
|
+
let index = 0;
|
|
35973
|
+
while (index < content.length) {
|
|
35974
|
+
const character = content[index];
|
|
35975
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35976
|
+
while (index < content.length && content[index] !== "\n") {
|
|
35977
|
+
characters[index] = " ";
|
|
35978
|
+
index += 1;
|
|
35979
|
+
}
|
|
35980
|
+
continue;
|
|
35981
|
+
}
|
|
35982
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35983
|
+
while (index < content.length) {
|
|
35984
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35985
|
+
characters[index] = " ";
|
|
35986
|
+
characters[index + 1] = " ";
|
|
35987
|
+
index += 2;
|
|
35988
|
+
break;
|
|
35989
|
+
}
|
|
35990
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35991
|
+
index += 1;
|
|
35992
|
+
}
|
|
35993
|
+
continue;
|
|
35994
|
+
}
|
|
35995
|
+
if (character === "'") {
|
|
35996
|
+
characters[index] = " ";
|
|
35997
|
+
index += 1;
|
|
35998
|
+
while (index < content.length) {
|
|
35999
|
+
if (content[index] === "'") {
|
|
36000
|
+
if (content[index + 1] === "'") {
|
|
36001
|
+
characters[index] = " ";
|
|
36002
|
+
characters[index + 1] = " ";
|
|
36003
|
+
index += 2;
|
|
36004
|
+
continue;
|
|
36005
|
+
}
|
|
36006
|
+
characters[index] = " ";
|
|
36007
|
+
index += 1;
|
|
36008
|
+
break;
|
|
36009
|
+
}
|
|
36010
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
36011
|
+
index += 1;
|
|
36012
|
+
}
|
|
36013
|
+
continue;
|
|
36014
|
+
}
|
|
36015
|
+
if (character === "$") {
|
|
36016
|
+
const tagMatch = DOLLAR_QUOTE_TAG_PATTERN.exec(content.slice(index));
|
|
36017
|
+
if (tagMatch !== null) {
|
|
36018
|
+
const tag = tagMatch[0];
|
|
36019
|
+
const closeIndex = content.indexOf(tag, index + tag.length);
|
|
36020
|
+
const endIndex = closeIndex < 0 ? content.length : closeIndex + tag.length;
|
|
36021
|
+
const keyword = precedingKeyword(content, index);
|
|
36022
|
+
if (CODE_BODY_KEYWORDS.has(keyword)) blankCodeBodyInterior(content, characters, index + tag.length, endIndex);
|
|
36023
|
+
else for (let blankIndex = index; blankIndex < endIndex; blankIndex += 1) if (content[blankIndex] !== "\n") characters[blankIndex] = " ";
|
|
36024
|
+
index = endIndex;
|
|
36025
|
+
continue;
|
|
36026
|
+
}
|
|
36027
|
+
}
|
|
36028
|
+
if (character === "\"") {
|
|
36029
|
+
index += 1;
|
|
36030
|
+
while (index < content.length) {
|
|
36031
|
+
if (content[index] === "\"") {
|
|
36032
|
+
if (content[index + 1] === "\"") {
|
|
36033
|
+
index += 2;
|
|
36034
|
+
continue;
|
|
36035
|
+
}
|
|
36036
|
+
index += 1;
|
|
36037
|
+
break;
|
|
36038
|
+
}
|
|
36039
|
+
index += 1;
|
|
36040
|
+
}
|
|
36041
|
+
continue;
|
|
36042
|
+
}
|
|
36043
|
+
index += 1;
|
|
36044
|
+
}
|
|
36045
|
+
return characters.join("");
|
|
36046
|
+
};
|
|
36047
|
+
//#endregion
|
|
36048
|
+
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
36049
|
+
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
36050
|
+
const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
|
|
36051
|
+
const supabaseTableMissingRls = defineRule({
|
|
36052
|
+
id: "supabase-table-missing-rls",
|
|
36053
|
+
title: "Supabase table created without Row Level Security",
|
|
36054
|
+
severity: "error",
|
|
36055
|
+
recommendation: "Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",
|
|
36056
|
+
scan: (file) => {
|
|
36057
|
+
if (!isSupabaseMigrationPath(file.relativePath)) return [];
|
|
36058
|
+
const content = sanitizeSqlForScan(file.content);
|
|
36059
|
+
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
36060
|
+
const findings = [];
|
|
36061
|
+
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
36062
|
+
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
36063
|
+
const tableName = match[1];
|
|
36064
|
+
if (tableName === void 0) continue;
|
|
36065
|
+
if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
|
|
36066
|
+
const location = getLocationAtIndex(content, match.index);
|
|
36067
|
+
findings.push({
|
|
36068
|
+
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
36069
|
+
line: location.line,
|
|
36070
|
+
column: location.column
|
|
36071
|
+
});
|
|
36072
|
+
}
|
|
36073
|
+
return findings;
|
|
36074
|
+
}
|
|
36075
|
+
});
|
|
36076
|
+
//#endregion
|
|
35161
36077
|
//#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
|
|
35162
36078
|
const svgFilterClickjackingRisk = defineRule({
|
|
35163
36079
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -35856,6 +36772,47 @@ const tenantStaticProxyRisk = defineRule({
|
|
|
35856
36772
|
})
|
|
35857
36773
|
});
|
|
35858
36774
|
//#endregion
|
|
36775
|
+
//#region src/plugin/rules/security-scan/unsafe-json-in-html.ts
|
|
36776
|
+
const SINK_JSON_STRINGIFY_PATTERNS = [/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi, /<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi];
|
|
36777
|
+
const RETURN_ESCAPE_PATTERN = /^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|<|<)/;
|
|
36778
|
+
const ESCAPE_WRAPPER_PATTERN = /(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i;
|
|
36779
|
+
const JSON_STRINGIFY_TOKEN_PATTERN = /\bJSON\.stringify\s*\($/i;
|
|
36780
|
+
const RETURN_LOOKAHEAD_CHARS = 160;
|
|
36781
|
+
const unsafeJsonInHtml = defineRule({
|
|
36782
|
+
id: "unsafe-json-in-html",
|
|
36783
|
+
title: "Unescaped JSON in HTML or script sink",
|
|
36784
|
+
severity: "warn",
|
|
36785
|
+
recommendation: "JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type=\"application/json\">` read with JSON.parse.",
|
|
36786
|
+
scan: (file) => {
|
|
36787
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
36788
|
+
const content = getScannableContent(file);
|
|
36789
|
+
if (!content.includes("JSON.stringify")) return [];
|
|
36790
|
+
const findings = [];
|
|
36791
|
+
const seenIndices = /* @__PURE__ */ new Set();
|
|
36792
|
+
for (const pattern of SINK_JSON_STRINGIFY_PATTERNS) {
|
|
36793
|
+
pattern.lastIndex = 0;
|
|
36794
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
36795
|
+
const beforeStringify = match[0].replace(JSON_STRINGIFY_TOKEN_PATTERN, "");
|
|
36796
|
+
if (ESCAPE_WRAPPER_PATTERN.test(beforeStringify)) continue;
|
|
36797
|
+
const closeParenIndex = findMatchingBracket(content, match.index + match[0].length - 1);
|
|
36798
|
+
if (closeParenIndex >= 0) {
|
|
36799
|
+
const afterReturn = content.slice(closeParenIndex + 1, closeParenIndex + 1 + RETURN_LOOKAHEAD_CHARS);
|
|
36800
|
+
if (RETURN_ESCAPE_PATTERN.test(afterReturn)) continue;
|
|
36801
|
+
}
|
|
36802
|
+
if (seenIndices.has(match.index)) continue;
|
|
36803
|
+
seenIndices.add(match.index);
|
|
36804
|
+
const location = getLocationAtIndex(content, match.index);
|
|
36805
|
+
findings.push({
|
|
36806
|
+
message: "JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",
|
|
36807
|
+
line: location.line,
|
|
36808
|
+
column: location.column
|
|
36809
|
+
});
|
|
36810
|
+
}
|
|
36811
|
+
}
|
|
36812
|
+
return findings;
|
|
36813
|
+
}
|
|
36814
|
+
});
|
|
36815
|
+
//#endregion
|
|
35859
36816
|
//#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
|
|
35860
36817
|
const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
|
|
35861
36818
|
const CALLER_STYLE_URL_NAME_PATTERN = /\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i;
|
|
@@ -35901,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
35901
36858
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
35902
36859
|
scan: scanByPattern({
|
|
35903
36860
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
35904
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36861
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
35905
36862
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
35906
36863
|
})
|
|
35907
36864
|
});
|
|
@@ -36003,7 +36960,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
36003
36960
|
//#region src/plugin/rules/security-scan/webhook-signature-risk.ts
|
|
36004
36961
|
const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
|
|
36005
36962
|
const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
|
|
36006
|
-
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']
|
|
36963
|
+
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = new RegExp(`${/verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/.source}|${/\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\s*\(/.source}`, "i");
|
|
36007
36964
|
const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
|
|
36008
36965
|
const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
|
|
36009
36966
|
const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
|
|
@@ -36663,6 +37620,17 @@ const reactDoctorRules = [
|
|
|
36663
37620
|
category: "Performance"
|
|
36664
37621
|
}
|
|
36665
37622
|
},
|
|
37623
|
+
{
|
|
37624
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37625
|
+
id: "auth-token-in-web-storage",
|
|
37626
|
+
source: "react-doctor",
|
|
37627
|
+
originallyExternal: false,
|
|
37628
|
+
rule: {
|
|
37629
|
+
...authTokenInWebStorage,
|
|
37630
|
+
framework: "global",
|
|
37631
|
+
category: "Security"
|
|
37632
|
+
}
|
|
37633
|
+
},
|
|
36666
37634
|
{
|
|
36667
37635
|
key: "react-doctor/autocomplete-valid",
|
|
36668
37636
|
id: "autocomplete-valid",
|
|
@@ -36879,6 +37847,18 @@ const reactDoctorRules = [
|
|
|
36879
37847
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
36880
37848
|
}
|
|
36881
37849
|
},
|
|
37850
|
+
{
|
|
37851
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37852
|
+
id: "dialog-has-accessible-name",
|
|
37853
|
+
source: "react-doctor",
|
|
37854
|
+
originallyExternal: false,
|
|
37855
|
+
rule: {
|
|
37856
|
+
...dialogHasAccessibleName,
|
|
37857
|
+
framework: "global",
|
|
37858
|
+
category: "Accessibility",
|
|
37859
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37860
|
+
}
|
|
37861
|
+
},
|
|
36882
37862
|
{
|
|
36883
37863
|
key: "react-doctor/display-name",
|
|
36884
37864
|
id: "display-name",
|
|
@@ -37164,6 +38144,18 @@ const reactDoctorRules = [
|
|
|
37164
38144
|
tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
|
|
37165
38145
|
}
|
|
37166
38146
|
},
|
|
38147
|
+
{
|
|
38148
|
+
key: "react-doctor/insecure-session-cookie",
|
|
38149
|
+
id: "insecure-session-cookie",
|
|
38150
|
+
source: "react-doctor",
|
|
38151
|
+
originallyExternal: false,
|
|
38152
|
+
rule: {
|
|
38153
|
+
...insecureSessionCookie,
|
|
38154
|
+
framework: "global",
|
|
38155
|
+
category: "Security",
|
|
38156
|
+
tags: [...new Set(["security-scan", ...insecureSessionCookie.tags ?? []])]
|
|
38157
|
+
}
|
|
38158
|
+
},
|
|
37167
38159
|
{
|
|
37168
38160
|
key: "react-doctor/interactive-supports-focus",
|
|
37169
38161
|
id: "interactive-supports-focus",
|
|
@@ -37606,6 +38598,18 @@ const reactDoctorRules = [
|
|
|
37606
38598
|
requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
|
|
37607
38599
|
}
|
|
37608
38600
|
},
|
|
38601
|
+
{
|
|
38602
|
+
key: "react-doctor/jwt-insecure-verification",
|
|
38603
|
+
id: "jwt-insecure-verification",
|
|
38604
|
+
source: "react-doctor",
|
|
38605
|
+
originallyExternal: false,
|
|
38606
|
+
rule: {
|
|
38607
|
+
...jwtInsecureVerification,
|
|
38608
|
+
framework: "global",
|
|
38609
|
+
category: "Security",
|
|
38610
|
+
tags: [...new Set(["security-scan", ...jwtInsecureVerification.tags ?? []])]
|
|
38611
|
+
}
|
|
38612
|
+
},
|
|
37609
38613
|
{
|
|
37610
38614
|
key: "react-doctor/key-lifecycle-risk",
|
|
37611
38615
|
id: "key-lifecycle-risk",
|
|
@@ -38014,6 +39018,18 @@ const reactDoctorRules = [
|
|
|
38014
39018
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38015
39019
|
}
|
|
38016
39020
|
},
|
|
39021
|
+
{
|
|
39022
|
+
key: "react-doctor/no-async-effect-callback",
|
|
39023
|
+
id: "no-async-effect-callback",
|
|
39024
|
+
source: "react-doctor",
|
|
39025
|
+
originallyExternal: false,
|
|
39026
|
+
rule: {
|
|
39027
|
+
...noAsyncEffectCallback,
|
|
39028
|
+
framework: "global",
|
|
39029
|
+
category: "Bugs",
|
|
39030
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
39031
|
+
}
|
|
39032
|
+
},
|
|
38017
39033
|
{
|
|
38018
39034
|
key: "react-doctor/no-autofocus",
|
|
38019
39035
|
id: "no-autofocus",
|
|
@@ -38037,6 +39053,18 @@ const reactDoctorRules = [
|
|
|
38037
39053
|
category: "Performance"
|
|
38038
39054
|
}
|
|
38039
39055
|
},
|
|
39056
|
+
{
|
|
39057
|
+
key: "react-doctor/no-call-component-as-function",
|
|
39058
|
+
id: "no-call-component-as-function",
|
|
39059
|
+
source: "react-doctor",
|
|
39060
|
+
originallyExternal: false,
|
|
39061
|
+
rule: {
|
|
39062
|
+
...noCallComponentAsFunction,
|
|
39063
|
+
framework: "global",
|
|
39064
|
+
category: "Bugs",
|
|
39065
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
39066
|
+
}
|
|
39067
|
+
},
|
|
38040
39068
|
{
|
|
38041
39069
|
key: "react-doctor/no-cascading-set-state",
|
|
38042
39070
|
id: "no-cascading-set-state",
|
|
@@ -38097,6 +39125,18 @@ const reactDoctorRules = [
|
|
|
38097
39125
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38098
39126
|
}
|
|
38099
39127
|
},
|
|
39128
|
+
{
|
|
39129
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
39130
|
+
id: "no-create-ref-in-function-component",
|
|
39131
|
+
source: "react-doctor",
|
|
39132
|
+
originallyExternal: false,
|
|
39133
|
+
rule: {
|
|
39134
|
+
...noCreateRefInFunctionComponent,
|
|
39135
|
+
framework: "global",
|
|
39136
|
+
category: "Bugs",
|
|
39137
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
39138
|
+
}
|
|
39139
|
+
},
|
|
38100
39140
|
{
|
|
38101
39141
|
key: "react-doctor/no-create-store-in-render",
|
|
38102
39142
|
id: "no-create-store-in-render",
|
|
@@ -38274,6 +39314,17 @@ const reactDoctorRules = [
|
|
|
38274
39314
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
38275
39315
|
}
|
|
38276
39316
|
},
|
|
39317
|
+
{
|
|
39318
|
+
key: "react-doctor/no-document-write",
|
|
39319
|
+
id: "no-document-write",
|
|
39320
|
+
source: "react-doctor",
|
|
39321
|
+
originallyExternal: false,
|
|
39322
|
+
rule: {
|
|
39323
|
+
...noDocumentWrite,
|
|
39324
|
+
framework: "global",
|
|
39325
|
+
category: "Performance"
|
|
39326
|
+
}
|
|
39327
|
+
},
|
|
38277
39328
|
{
|
|
38278
39329
|
key: "react-doctor/no-dynamic-import-path",
|
|
38279
39330
|
id: "no-dynamic-import-path",
|
|
@@ -38471,6 +39522,18 @@ const reactDoctorRules = [
|
|
|
38471
39522
|
category: "Accessibility"
|
|
38472
39523
|
}
|
|
38473
39524
|
},
|
|
39525
|
+
{
|
|
39526
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39527
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39528
|
+
source: "react-doctor",
|
|
39529
|
+
originallyExternal: false,
|
|
39530
|
+
rule: {
|
|
39531
|
+
...noImgLazyWithHighFetchpriority,
|
|
39532
|
+
framework: "global",
|
|
39533
|
+
category: "Performance",
|
|
39534
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39535
|
+
}
|
|
39536
|
+
},
|
|
38474
39537
|
{
|
|
38475
39538
|
key: "react-doctor/no-initialize-state",
|
|
38476
39539
|
id: "no-initialize-state",
|
|
@@ -38541,6 +39604,17 @@ const reactDoctorRules = [
|
|
|
38541
39604
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
38542
39605
|
}
|
|
38543
39606
|
},
|
|
39607
|
+
{
|
|
39608
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39609
|
+
id: "no-json-parse-stringify-clone",
|
|
39610
|
+
source: "react-doctor",
|
|
39611
|
+
originallyExternal: false,
|
|
39612
|
+
rule: {
|
|
39613
|
+
...noJsonParseStringifyClone,
|
|
39614
|
+
framework: "global",
|
|
39615
|
+
category: "Performance"
|
|
39616
|
+
}
|
|
39617
|
+
},
|
|
38544
39618
|
{
|
|
38545
39619
|
key: "react-doctor/no-jsx-element-type",
|
|
38546
39620
|
id: "no-jsx-element-type",
|
|
@@ -39060,6 +40134,18 @@ const reactDoctorRules = [
|
|
|
39060
40134
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39061
40135
|
}
|
|
39062
40136
|
},
|
|
40137
|
+
{
|
|
40138
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40139
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40140
|
+
source: "react-doctor",
|
|
40141
|
+
originallyExternal: false,
|
|
40142
|
+
rule: {
|
|
40143
|
+
...noStringFalseOnBooleanAttribute,
|
|
40144
|
+
framework: "global",
|
|
40145
|
+
category: "Bugs",
|
|
40146
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40147
|
+
}
|
|
40148
|
+
},
|
|
39063
40149
|
{
|
|
39064
40150
|
key: "react-doctor/no-string-refs",
|
|
39065
40151
|
id: "no-string-refs",
|
|
@@ -39072,6 +40158,17 @@ const reactDoctorRules = [
|
|
|
39072
40158
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39073
40159
|
}
|
|
39074
40160
|
},
|
|
40161
|
+
{
|
|
40162
|
+
key: "react-doctor/no-sync-xhr",
|
|
40163
|
+
id: "no-sync-xhr",
|
|
40164
|
+
source: "react-doctor",
|
|
40165
|
+
originallyExternal: false,
|
|
40166
|
+
rule: {
|
|
40167
|
+
...noSyncXhr,
|
|
40168
|
+
framework: "global",
|
|
40169
|
+
category: "Performance"
|
|
40170
|
+
}
|
|
40171
|
+
},
|
|
39075
40172
|
{
|
|
39076
40173
|
key: "react-doctor/no-this-in-sfc",
|
|
39077
40174
|
id: "no-this-in-sfc",
|
|
@@ -39756,6 +40853,18 @@ const reactDoctorRules = [
|
|
|
39756
40853
|
tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
|
|
39757
40854
|
}
|
|
39758
40855
|
},
|
|
40856
|
+
{
|
|
40857
|
+
key: "react-doctor/request-body-mass-assignment",
|
|
40858
|
+
id: "request-body-mass-assignment",
|
|
40859
|
+
source: "react-doctor",
|
|
40860
|
+
originallyExternal: false,
|
|
40861
|
+
rule: {
|
|
40862
|
+
...requestBodyMassAssignment,
|
|
40863
|
+
framework: "global",
|
|
40864
|
+
category: "Security",
|
|
40865
|
+
tags: [...new Set(["security-scan", ...requestBodyMassAssignment.tags ?? []])]
|
|
40866
|
+
}
|
|
40867
|
+
},
|
|
39759
40868
|
{
|
|
39760
40869
|
key: "react-doctor/require-render-return",
|
|
39761
40870
|
id: "require-render-return",
|
|
@@ -40344,6 +41453,18 @@ const reactDoctorRules = [
|
|
|
40344
41453
|
requires: [...new Set(["react", ...scope.requires ?? []])]
|
|
40345
41454
|
}
|
|
40346
41455
|
},
|
|
41456
|
+
{
|
|
41457
|
+
key: "react-doctor/secret-in-fallback",
|
|
41458
|
+
id: "secret-in-fallback",
|
|
41459
|
+
source: "react-doctor",
|
|
41460
|
+
originallyExternal: false,
|
|
41461
|
+
rule: {
|
|
41462
|
+
...secretInFallback,
|
|
41463
|
+
framework: "global",
|
|
41464
|
+
category: "Security",
|
|
41465
|
+
tags: [...new Set(["security-scan", ...secretInFallback.tags ?? []])]
|
|
41466
|
+
}
|
|
41467
|
+
},
|
|
40347
41468
|
{
|
|
40348
41469
|
key: "react-doctor/self-closing-comp",
|
|
40349
41470
|
id: "self-closing-comp",
|
|
@@ -40500,6 +41621,18 @@ const reactDoctorRules = [
|
|
|
40500
41621
|
tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
|
|
40501
41622
|
}
|
|
40502
41623
|
},
|
|
41624
|
+
{
|
|
41625
|
+
key: "react-doctor/supabase-table-missing-rls",
|
|
41626
|
+
id: "supabase-table-missing-rls",
|
|
41627
|
+
source: "react-doctor",
|
|
41628
|
+
originallyExternal: false,
|
|
41629
|
+
rule: {
|
|
41630
|
+
...supabaseTableMissingRls,
|
|
41631
|
+
framework: "global",
|
|
41632
|
+
category: "Security",
|
|
41633
|
+
tags: [...new Set(["security-scan", ...supabaseTableMissingRls.tags ?? []])]
|
|
41634
|
+
}
|
|
41635
|
+
},
|
|
40503
41636
|
{
|
|
40504
41637
|
key: "react-doctor/svg-filter-clickjacking-risk",
|
|
40505
41638
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -40690,6 +41823,18 @@ const reactDoctorRules = [
|
|
|
40690
41823
|
tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
|
|
40691
41824
|
}
|
|
40692
41825
|
},
|
|
41826
|
+
{
|
|
41827
|
+
key: "react-doctor/unsafe-json-in-html",
|
|
41828
|
+
id: "unsafe-json-in-html",
|
|
41829
|
+
source: "react-doctor",
|
|
41830
|
+
originallyExternal: false,
|
|
41831
|
+
rule: {
|
|
41832
|
+
...unsafeJsonInHtml,
|
|
41833
|
+
framework: "global",
|
|
41834
|
+
category: "Security",
|
|
41835
|
+
tags: [...new Set(["security-scan", ...unsafeJsonInHtml.tags ?? []])]
|
|
41836
|
+
}
|
|
41837
|
+
},
|
|
40693
41838
|
{
|
|
40694
41839
|
key: "react-doctor/untrusted-redirect-following",
|
|
40695
41840
|
id: "untrusted-redirect-following",
|