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.
Files changed (3) hide show
  1. package/dist/index.d.ts +1423 -4
  2. package/dist/index.js +1338 -193
  3. 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 isServerOnlyBuildArtifactPath = (relativePath) => /(?:^|\/)(?:\.next\/server|\.output\/server)\//.test(relativePath);
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 (isServerOnlyBuildArtifactPath(relativePath)) return false;
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 stripCommentsPreservingPositions = (content) => {
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) stringDelimiter = null;
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 === "'" || 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
- if (content[index] !== "\n") characters[index] = " ";
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 getScannableContent = (file) => {
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 cachedContent = strippedContentCache.get(file);
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
- strippedContentCache.set(file, strippedContent);
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$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
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$51
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$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
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$50
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$50
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|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
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$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
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$49
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$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
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$48
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$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
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$47
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$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
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$46
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$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
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$45
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$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
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$44
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$44
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$44
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$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
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$43
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$43
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$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
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$42
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$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
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$41
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$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
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$40
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$39 = "This child redraws every render because the prop gets brand new JSX each time.";
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$39
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$38 = "This child redraws every render because the prop gets a brand new array each time.";
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$38
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$37 = "This child redraws every render because the prop gets a brand new function each time.";
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$37
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$36 = "This child redraws every render because the prop gets a brand new object each time.";
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$36
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$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
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$35
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$34 = "You can't tell what props reach this element when you spread them.";
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$34
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$33 = "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`.";
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$33
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$33
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$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
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$32
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$32
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$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
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$31
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$31
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$30 = "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.";
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$30
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$29 = "Your users can see & submit the wrong data when this list reorders.";
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$29
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$29
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$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
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$28
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$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
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$27
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$27
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$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
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$26
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$26
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$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
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$25} (called inside "${componentOrHookName}")`
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$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
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$24
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$24
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$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
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$23
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$23
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$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
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$22
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$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
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$21
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$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
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$20
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$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
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$19
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$19
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$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
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$18
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$17 = "This file declares several components, so each component is harder to find, test, and change.";
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$17
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$16 = "This reducer changes state in place, so your update is silently skipped.";
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$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
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$16
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$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
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$15
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$15
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$15
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$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
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$14
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$14
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$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
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$13
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/get-identifier-trailing-word.ts
22531
- const getIdentifierTrailingWord = (identifierName) => {
22532
- return identifierName.match(/[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g)?.at(-1)?.toLowerCase() ?? identifierName.toLowerCase();
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$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
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$12
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$11 = "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.";
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$11
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$11
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$11
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")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
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 ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
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
- walkAst(declaration, (child) => {
34890
- if (didRead) return;
34891
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
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") || /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
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]|&lt;|<)/;
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["']/i;
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",