oxlint-plugin-react-doctor 0.5.5 → 0.5.6-dev.03301fc

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 +1293 -0
  2. package/dist/index.js +1118 -148
  3. package/package.json +1 -1
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));
@@ -1108,6 +1117,11 @@ const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSourc
1108
1117
  if (info.source !== moduleSource) return null;
1109
1118
  return info.imported;
1110
1119
  };
1120
+ const getImportSourceForName = (contextNode, localIdentifierName) => {
1121
+ const lookup = getImportLookup(contextNode);
1122
+ if (!lookup) return null;
1123
+ return lookup.get(localIdentifierName)?.source ?? null;
1124
+ };
1111
1125
  //#endregion
1112
1126
  //#region src/plugin/utils/find-variable-initializer.ts
1113
1127
  const FUNCTION_LIKE_TYPES$1 = new Set([
@@ -1847,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
1847
1861
  });
1848
1862
  //#endregion
1849
1863
  //#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`.";
1864
+ const MESSAGE$57 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1851
1865
  const anchorHasContent = defineRule({
1852
1866
  id: "anchor-has-content",
1853
1867
  title: "Anchor has no content",
@@ -1863,7 +1877,7 @@ const anchorHasContent = defineRule({
1863
1877
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1864
1878
  context.report({
1865
1879
  node: opening.name,
1866
- message: MESSAGE$51
1880
+ message: MESSAGE$57
1867
1881
  });
1868
1882
  } })
1869
1883
  });
@@ -2257,7 +2271,7 @@ const parseJsxValue = (value) => {
2257
2271
  };
2258
2272
  //#endregion
2259
2273
  //#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}`.";
2274
+ const MESSAGE$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2261
2275
  const ariaActivedescendantHasTabindex = defineRule({
2262
2276
  id: "aria-activedescendant-has-tabindex",
2263
2277
  title: "aria-activedescendant missing tabindex",
@@ -2275,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2275
2289
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2276
2290
  context.report({
2277
2291
  node: node.name,
2278
- message: MESSAGE$50
2292
+ message: MESSAGE$56
2279
2293
  });
2280
2294
  return;
2281
2295
  }
2282
2296
  if (isInteractiveElement(tag, node)) return;
2283
2297
  context.report({
2284
2298
  node: node.name,
2285
- message: MESSAGE$50
2299
+ message: MESSAGE$56
2286
2300
  });
2287
2301
  } })
2288
2302
  });
@@ -3090,6 +3104,76 @@ const AUTH_FUNCTION_NAMES = new Set([
3090
3104
  "getAuth",
3091
3105
  "validateSession"
3092
3106
  ]);
3107
+ const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
3108
+ const AUTH_STANDALONE_NOUN_TOKENS = new Set([
3109
+ "signedin",
3110
+ "loggedin",
3111
+ "signin"
3112
+ ]);
3113
+ const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
3114
+ "require",
3115
+ "ensure",
3116
+ "assert",
3117
+ "verify",
3118
+ "validate",
3119
+ "check",
3120
+ "protect",
3121
+ "enforce",
3122
+ "guard",
3123
+ "gate",
3124
+ "restrict",
3125
+ "is",
3126
+ "has",
3127
+ "can",
3128
+ "must"
3129
+ ]);
3130
+ const AUTH_GETTER_VERB_TOKENS = new Set([
3131
+ "get",
3132
+ "fetch",
3133
+ "load",
3134
+ "read",
3135
+ "resolve",
3136
+ "retrieve",
3137
+ "use"
3138
+ ]);
3139
+ const AUTH_QUALIFIER_TOKENS = new Set([
3140
+ "current",
3141
+ "my",
3142
+ "own"
3143
+ ]);
3144
+ const AUTH_STRONG_NOUN_TOKENS = new Set([
3145
+ "session",
3146
+ "sessions",
3147
+ "login",
3148
+ "admin",
3149
+ "admins",
3150
+ "superadmin",
3151
+ "superuser",
3152
+ "role",
3153
+ "roles",
3154
+ "permission",
3155
+ "permissions",
3156
+ "jwt",
3157
+ "identity",
3158
+ "principal",
3159
+ "credential",
3160
+ "credentials"
3161
+ ]);
3162
+ const AUTH_WEAK_NOUN_TOKENS = new Set([
3163
+ "user",
3164
+ "users",
3165
+ "account",
3166
+ "accounts",
3167
+ "token",
3168
+ "tokens",
3169
+ "access",
3170
+ "me",
3171
+ "viewer",
3172
+ "caller",
3173
+ "subject",
3174
+ "scope",
3175
+ "scopes"
3176
+ ]);
3093
3177
  const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
3094
3178
  const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
3095
3179
  const SECRET_PATTERNS = [
@@ -4187,6 +4271,58 @@ const asyncParallel = defineRule({
4187
4271
  }
4188
4272
  });
4189
4273
  //#endregion
4274
+ //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4275
+ const MESSAGE$55 = "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.";
4276
+ const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4277
+ const STORAGE_GLOBALS = new Set([
4278
+ "window",
4279
+ "globalThis",
4280
+ "self"
4281
+ ]);
4282
+ const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
4283
+ const isWebStorageObject = (node) => {
4284
+ if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
4285
+ 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);
4286
+ return false;
4287
+ };
4288
+ const staticMemberName = (member) => {
4289
+ if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
4290
+ if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
4291
+ return null;
4292
+ };
4293
+ const authTokenInWebStorage = defineRule({
4294
+ id: "auth-token-in-web-storage",
4295
+ title: "Auth token in web storage",
4296
+ severity: "warn",
4297
+ 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.",
4298
+ create: (context) => ({
4299
+ CallExpression(node) {
4300
+ const callee = node.callee;
4301
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
4302
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
4303
+ if (!isWebStorageObject(callee.object)) return;
4304
+ const keyArgument = node.arguments?.[0];
4305
+ if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
4306
+ if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4307
+ context.report({
4308
+ node,
4309
+ message: MESSAGE$55
4310
+ });
4311
+ },
4312
+ AssignmentExpression(node) {
4313
+ const target = node.left;
4314
+ if (!isNodeOfType(target, "MemberExpression")) return;
4315
+ if (!isWebStorageObject(target.object)) return;
4316
+ const propertyName = staticMemberName(target);
4317
+ if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4318
+ context.report({
4319
+ node: target,
4320
+ message: MESSAGE$55
4321
+ });
4322
+ }
4323
+ })
4324
+ });
4325
+ //#endregion
4190
4326
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
4191
4327
  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
4328
  const AUTOFILL_TOKENS = new Set([
@@ -4558,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
4558
4694
  //#endregion
4559
4695
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4560
4696
  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`.";
4697
+ const MESSAGE$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4562
4698
  const KEY_HANDLERS = [
4563
4699
  "onKeyUp",
4564
4700
  "onKeyDown",
@@ -4590,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
4590
4726
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4591
4727
  context.report({
4592
4728
  node: node.name,
4593
- message: MESSAGE$49
4729
+ message: MESSAGE$54
4594
4730
  });
4595
4731
  } };
4596
4732
  }
@@ -4705,7 +4841,7 @@ const isReactComponentName = (name) => {
4705
4841
  };
4706
4842
  //#endregion
4707
4843
  //#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`.";
4844
+ const MESSAGE$53 = "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
4845
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4710
4846
  const DEFAULT_LABELLING_PROPS = [
4711
4847
  "alt",
@@ -4866,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
4866
5002
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4867
5003
  context.report({
4868
5004
  node: opening,
4869
- message: MESSAGE$48
5005
+ message: MESSAGE$53
4870
5006
  });
4871
5007
  } };
4872
5008
  }
@@ -5292,6 +5428,38 @@ const noVagueButtonLabel = defineRule({
5292
5428
  } })
5293
5429
  });
5294
5430
  //#endregion
5431
+ //#region src/plugin/utils/has-jsx-spread-attribute.ts
5432
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
+ //#endregion
5434
+ //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5435
+ const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5436
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5437
+ const NAME_PROVIDING_ATTRIBUTES = [
5438
+ "aria-label",
5439
+ "aria-labelledby",
5440
+ "title"
5441
+ ];
5442
+ const dialogHasAccessibleName = defineRule({
5443
+ id: "dialog-has-accessible-name",
5444
+ title: "Dialog without accessible name",
5445
+ severity: "warn",
5446
+ recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
5447
+ create: (context) => ({ JSXOpeningElement(node) {
5448
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
5449
+ const tagName = node.name.name;
5450
+ if (tagName[0] !== tagName[0]?.toLowerCase()) return;
5451
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5452
+ const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5453
+ if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5454
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5455
+ if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5456
+ context.report({
5457
+ node: node.name,
5458
+ message: MESSAGE$52
5459
+ });
5460
+ } })
5461
+ });
5462
+ //#endregion
5295
5463
  //#region src/plugin/utils/is-es5-component.ts
5296
5464
  const PRAGMA$2 = "React";
5297
5465
  const CREATE_CLASS = "createReactClass";
@@ -5326,7 +5494,7 @@ const isEs6Component = (node) => {
5326
5494
  };
5327
5495
  //#endregion
5328
5496
  //#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`.";
5497
+ const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5330
5498
  const DEFAULT_ADDITIONAL_HOCS = [
5331
5499
  "observer",
5332
5500
  "lazy",
@@ -5529,7 +5697,7 @@ const displayName = defineRule({
5529
5697
  const reportAt = (node) => {
5530
5698
  context.report({
5531
5699
  node,
5532
- message: MESSAGE$47
5700
+ message: MESSAGE$51
5533
5701
  });
5534
5702
  };
5535
5703
  return {
@@ -7677,7 +7845,7 @@ const forbidElements = defineRule({
7677
7845
  });
7678
7846
  //#endregion
7679
7847
  //#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`.";
7848
+ const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7681
7849
  const forwardRefUsesRef = defineRule({
7682
7850
  id: "forward-ref-uses-ref",
7683
7851
  title: "forwardRef without ref parameter",
@@ -7697,7 +7865,7 @@ const forwardRefUsesRef = defineRule({
7697
7865
  if (isNodeOfType(onlyParam, "RestElement")) return;
7698
7866
  context.report({
7699
7867
  node: inner,
7700
- message: MESSAGE$46
7868
+ message: MESSAGE$50
7701
7869
  });
7702
7870
  } })
7703
7871
  });
@@ -7734,7 +7902,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7734
7902
  });
7735
7903
  //#endregion
7736
7904
  //#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`.";
7905
+ const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7738
7906
  const DEFAULT_HEADING_TAGS = [
7739
7907
  "h1",
7740
7908
  "h2",
@@ -7767,7 +7935,7 @@ const headingHasContent = defineRule({
7767
7935
  if (isHiddenFromScreenReader(node, context.settings)) return;
7768
7936
  context.report({
7769
7937
  node,
7770
- message: MESSAGE$45
7938
+ message: MESSAGE$49
7771
7939
  });
7772
7940
  } };
7773
7941
  }
@@ -7905,7 +8073,7 @@ const hooksNoNanInDeps = defineRule({
7905
8073
  });
7906
8074
  //#endregion
7907
8075
  //#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`.";
8076
+ const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
7909
8077
  const resolveSettings$38 = (settings) => {
7910
8078
  const reactDoctor = settings?.["react-doctor"];
7911
8079
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -7953,7 +8121,7 @@ const htmlHasLang = defineRule({
7953
8121
  if (!lang) {
7954
8122
  context.report({
7955
8123
  node: node.name,
7956
- message: MESSAGE$44
8124
+ message: MESSAGE$48
7957
8125
  });
7958
8126
  return;
7959
8127
  }
@@ -7961,13 +8129,13 @@ const htmlHasLang = defineRule({
7961
8129
  if (verdict === "missing" || verdict === "empty") {
7962
8130
  context.report({
7963
8131
  node: lang,
7964
- message: MESSAGE$44
8132
+ message: MESSAGE$48
7965
8133
  });
7966
8134
  return;
7967
8135
  }
7968
8136
  if (hasSpread && !lang) context.report({
7969
8137
  node: node.name,
7970
- message: MESSAGE$44
8138
+ message: MESSAGE$48
7971
8139
  });
7972
8140
  } };
7973
8141
  }
@@ -8181,7 +8349,7 @@ const htmlNoNestedInteractive = defineRule({
8181
8349
  });
8182
8350
  //#endregion
8183
8351
  //#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.";
8352
+ const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8185
8353
  const evaluateTitleValue = (value) => {
8186
8354
  if (!value) return "missing";
8187
8355
  if (isNodeOfType(value, "Literal")) {
@@ -8221,14 +8389,14 @@ const iframeHasTitle = defineRule({
8221
8389
  if (!titleAttr) {
8222
8390
  if (hasSpread || tag === "iframe") context.report({
8223
8391
  node: node.name,
8224
- message: MESSAGE$43
8392
+ message: MESSAGE$47
8225
8393
  });
8226
8394
  return;
8227
8395
  }
8228
8396
  const verdict = evaluateTitleValue(titleAttr.value);
8229
8397
  if (verdict === "missing" || verdict === "empty") context.report({
8230
8398
  node: titleAttr,
8231
- message: MESSAGE$43
8399
+ message: MESSAGE$47
8232
8400
  });
8233
8401
  } })
8234
8402
  });
@@ -8332,7 +8500,7 @@ const iframeMissingSandbox = defineRule({
8332
8500
  });
8333
8501
  //#endregion
8334
8502
  //#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.";
8503
+ const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8336
8504
  const DEFAULT_COMPONENTS = ["img"];
8337
8505
  const DEFAULT_REDUNDANT_WORDS = [
8338
8506
  "image",
@@ -8397,7 +8565,7 @@ const imgRedundantAlt = defineRule({
8397
8565
  if (!altAttribute) return;
8398
8566
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8399
8567
  node: altAttribute,
8400
- message: MESSAGE$42
8568
+ message: MESSAGE$46
8401
8569
  });
8402
8570
  } };
8403
8571
  }
@@ -8495,6 +8663,136 @@ const insecureCryptoRisk = defineRule({
8495
8663
  }
8496
8664
  });
8497
8665
  //#endregion
8666
+ //#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
8667
+ const findMatchingBracket = (content, openIndex) => {
8668
+ const open = content[openIndex];
8669
+ const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
8670
+ if (close === "") return -1;
8671
+ let depth = 0;
8672
+ let stringDelimiter = null;
8673
+ for (let index = openIndex; index < content.length; index += 1) {
8674
+ const character = content[index];
8675
+ if (stringDelimiter !== null) {
8676
+ if (character === "\\") index += 1;
8677
+ else if (character === stringDelimiter) stringDelimiter = null;
8678
+ continue;
8679
+ }
8680
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
8681
+ else if (character === open) depth += 1;
8682
+ else if (character === close) {
8683
+ depth -= 1;
8684
+ if (depth === 0) return index;
8685
+ }
8686
+ }
8687
+ return -1;
8688
+ };
8689
+ //#endregion
8690
+ //#region src/plugin/rules/security-scan/insecure-session-cookie.ts
8691
+ 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])`;
8692
+ const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
8693
+ const AUTH_COOKIE_SET_CALL_PATTERN = new RegExp(`(?:\\.cookies\\.set|cookies\\(\\s*\\)\\.set|\\.cookie)\\s*\\(\\s*${AUTH_COOKIE_NAME_LITERAL}`, "gi");
8694
+ const HTTP_ONLY_DISABLED_PATTERN = /httpOnly\s*:\s*false\b/i;
8695
+ const STRING_LITERAL_PATTERN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
8696
+ const blankStringContents = (text) => {
8697
+ const characters = text.split("");
8698
+ let index = 0;
8699
+ let stringDelimiter = null;
8700
+ while (index < text.length) {
8701
+ const character = text[index];
8702
+ if (stringDelimiter !== null) {
8703
+ if (character === "\\") {
8704
+ index += 2;
8705
+ continue;
8706
+ }
8707
+ if (character === stringDelimiter) stringDelimiter = null;
8708
+ else if (character !== "\n") characters[index] = " ";
8709
+ index += 1;
8710
+ continue;
8711
+ }
8712
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
8713
+ index += 1;
8714
+ }
8715
+ return characters.join("");
8716
+ };
8717
+ const COOKIE_CONFIG_OPENER_PATTERN = /cookie\s*:\s*\{/gi;
8718
+ const CLIENT_AUTH_COOKIE_WRITE_PATTERN = new RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${AUTH_COOKIE_NAME_TOKEN}[^\`"'=;]*=`, "gi");
8719
+ const countTopLevelArguments = (argumentsSource) => {
8720
+ if (argumentsSource.trim().length === 0) return 0;
8721
+ let depth = 0;
8722
+ let stringDelimiter = null;
8723
+ let count = 1;
8724
+ for (let index = 0; index < argumentsSource.length; index += 1) {
8725
+ const character = argumentsSource[index];
8726
+ if (stringDelimiter !== null) {
8727
+ if (character === "\\") index += 1;
8728
+ else if (character === stringDelimiter) stringDelimiter = null;
8729
+ continue;
8730
+ }
8731
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
8732
+ else if (character === "(" || character === "[" || character === "{") depth += 1;
8733
+ else if (character === ")" || character === "]" || character === "}") depth -= 1;
8734
+ else if (character === "," && depth === 0) count += 1;
8735
+ }
8736
+ return count;
8737
+ };
8738
+ const addMatchFindings = (content, pattern, message, isInsecure, findings) => {
8739
+ pattern.lastIndex = 0;
8740
+ for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
8741
+ if (!isInsecure(match.index, match[0])) continue;
8742
+ const location = getLocationAtIndex(content, match.index);
8743
+ findings.push({
8744
+ message,
8745
+ line: location.line,
8746
+ column: location.column
8747
+ });
8748
+ }
8749
+ };
8750
+ const insecureSessionCookie = defineRule({
8751
+ id: "insecure-session-cookie",
8752
+ title: "Auth cookie missing HttpOnly protection",
8753
+ severity: "warn",
8754
+ 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.",
8755
+ scan: (file) => {
8756
+ if (!isProductionSourcePath(file.relativePath)) return [];
8757
+ const content = getScannableContent(file);
8758
+ if (!/cookie/i.test(content)) return [];
8759
+ const findings = [];
8760
+ 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.";
8761
+ AUTH_COOKIE_SET_CALL_PATTERN.lastIndex = 0;
8762
+ for (let match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content); match !== null; match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content)) {
8763
+ const openParenIndex = match.index + match[0].lastIndexOf("(");
8764
+ const closeParenIndex = findMatchingBracket(content, openParenIndex);
8765
+ if (closeParenIndex < 0) continue;
8766
+ const argumentsSource = content.slice(openParenIndex + 1, closeParenIndex);
8767
+ const hasNoOptions = countTopLevelArguments(argumentsSource) < 3;
8768
+ const argumentsWithoutStrings = argumentsSource.replace(STRING_LITERAL_PATTERN, "");
8769
+ if (!hasNoOptions && !HTTP_ONLY_DISABLED_PATTERN.test(argumentsWithoutStrings)) continue;
8770
+ const location = getLocationAtIndex(content, match.index);
8771
+ findings.push({
8772
+ message,
8773
+ line: location.line,
8774
+ column: location.column
8775
+ });
8776
+ }
8777
+ const blankedContent = blankStringContents(content);
8778
+ COOKIE_CONFIG_OPENER_PATTERN.lastIndex = 0;
8779
+ for (let match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent); match !== null; match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent)) {
8780
+ const braceIndex = match.index + match[0].length - 1;
8781
+ const closeBraceIndex = findMatchingBracket(blankedContent, braceIndex);
8782
+ const block = closeBraceIndex >= 0 ? blankedContent.slice(braceIndex, closeBraceIndex) : blankedContent.slice(braceIndex, braceIndex + 400);
8783
+ if (!HTTP_ONLY_DISABLED_PATTERN.test(block)) continue;
8784
+ const location = getLocationAtIndex(blankedContent, match.index);
8785
+ findings.push({
8786
+ message,
8787
+ line: location.line,
8788
+ column: location.column
8789
+ });
8790
+ }
8791
+ addMatchFindings(content, CLIENT_AUTH_COOKIE_WRITE_PATTERN, message, () => true, findings);
8792
+ return findings;
8793
+ }
8794
+ });
8795
+ //#endregion
8498
8796
  //#region src/plugin/constants/event-handlers.ts
8499
8797
  const MOUSE_EVENT_HANDLERS = [
8500
8798
  "onClick",
@@ -10624,7 +10922,7 @@ const jsxMaxDepth = defineRule({
10624
10922
  });
10625
10923
  //#endregion
10626
10924
  //#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.";
10925
+ const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10628
10926
  const LITERAL_TEXT_TAGS = new Set([
10629
10927
  "code",
10630
10928
  "pre",
@@ -10660,7 +10958,7 @@ const jsxNoCommentTextnodes = defineRule({
10660
10958
  if (isInsideLiteralTextTag(node)) return;
10661
10959
  context.report({
10662
10960
  node,
10663
- message: MESSAGE$41
10961
+ message: MESSAGE$45
10664
10962
  });
10665
10963
  } })
10666
10964
  });
@@ -10691,7 +10989,7 @@ const isInsideFunctionScope = (node) => {
10691
10989
  };
10692
10990
  //#endregion
10693
10991
  //#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.";
10992
+ const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
10695
10993
  const CONTEXT_MODULES$1 = [
10696
10994
  "react",
10697
10995
  "use-context-selector",
@@ -10789,7 +11087,7 @@ const jsxNoConstructedContextValues = defineRule({
10789
11087
  if (!isConstructedValue(innerExpression)) continue;
10790
11088
  context.report({
10791
11089
  node: attribute,
10792
- message: MESSAGE$40
11090
+ message: MESSAGE$44
10793
11091
  });
10794
11092
  }
10795
11093
  }
@@ -10875,7 +11173,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
10875
11173
  };
10876
11174
  //#endregion
10877
11175
  //#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.";
11176
+ const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
10879
11177
  const KNOWN_SLOT_PROP_NAMES = new Set([
10880
11178
  "icon",
10881
11179
  "Icon",
@@ -11144,7 +11442,7 @@ const jsxNoJsxAsProp = defineRule({
11144
11442
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11145
11443
  context.report({
11146
11444
  node,
11147
- message: MESSAGE$39
11445
+ message: MESSAGE$43
11148
11446
  });
11149
11447
  }
11150
11448
  };
@@ -11432,7 +11730,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11432
11730
  ];
11433
11731
  //#endregion
11434
11732
  //#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.";
11733
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
11436
11734
  const isDataArrayPropName = (propName) => {
11437
11735
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11438
11736
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11516,7 +11814,7 @@ const jsxNoNewArrayAsProp = defineRule({
11516
11814
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11517
11815
  context.report({
11518
11816
  node,
11519
- message: MESSAGE$38
11817
+ message: MESSAGE$42
11520
11818
  });
11521
11819
  }
11522
11820
  };
@@ -11774,7 +12072,7 @@ const SAFE_RECEIVER_NAMES = new Set([
11774
12072
  ]);
11775
12073
  //#endregion
11776
12074
  //#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.";
12075
+ const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
11778
12076
  const isAccessorPredicateName = (propName) => {
11779
12077
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
11780
12078
  if (propName.length <= prefix.length) continue;
@@ -11980,7 +12278,7 @@ const jsxNoNewFunctionAsProp = defineRule({
11980
12278
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
11981
12279
  context.report({
11982
12280
  node,
11983
- message: MESSAGE$37
12281
+ message: MESSAGE$41
11984
12282
  });
11985
12283
  }
11986
12284
  };
@@ -12200,7 +12498,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12200
12498
  ];
12201
12499
  //#endregion
12202
12500
  //#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.";
12501
+ const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
12204
12502
  const isConfigObjectPropName = (propName) => {
12205
12503
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12206
12504
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12288,7 +12586,7 @@ const jsxNoNewObjectAsProp = defineRule({
12288
12586
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12289
12587
  context.report({
12290
12588
  node,
12291
- message: MESSAGE$36
12589
+ message: MESSAGE$40
12292
12590
  });
12293
12591
  }
12294
12592
  };
@@ -12296,7 +12594,7 @@ const jsxNoNewObjectAsProp = defineRule({
12296
12594
  });
12297
12595
  //#endregion
12298
12596
  //#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.";
12597
+ const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12300
12598
  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
12599
  const resolveSettings$28 = (settings) => {
12302
12600
  const reactDoctor = settings?.["react-doctor"];
@@ -12337,7 +12635,7 @@ const jsxNoScriptUrl = defineRule({
12337
12635
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12338
12636
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12339
12637
  node: attribute,
12340
- message: MESSAGE$35
12638
+ message: MESSAGE$39
12341
12639
  });
12342
12640
  }
12343
12641
  } };
@@ -12652,7 +12950,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12652
12950
  });
12653
12951
  //#endregion
12654
12952
  //#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.";
12953
+ const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
12656
12954
  const resolveSettings$25 = (settings) => {
12657
12955
  const reactDoctor = settings?.["react-doctor"];
12658
12956
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12693,18 +12991,77 @@ const jsxPropsNoSpreading = defineRule({
12693
12991
  }
12694
12992
  context.report({
12695
12993
  node: attribute,
12696
- message: MESSAGE$34
12994
+ message: MESSAGE$38
12697
12995
  });
12698
12996
  }
12699
12997
  } };
12700
12998
  }
12701
12999
  });
12702
13000
  //#endregion
13001
+ //#region src/plugin/rules/security-scan/jwt-insecure-verification.ts
13002
+ const NONE_ALGORITHM_PATTERN = /\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi;
13003
+ const isIndexInsideStringLiteral = (content, index) => {
13004
+ let stringDelimiter = null;
13005
+ const templateExpressionDepths = [];
13006
+ for (let cursor = 0; cursor < index; cursor += 1) {
13007
+ const character = content[cursor];
13008
+ if (stringDelimiter === "`") {
13009
+ if (character === "\\") cursor += 1;
13010
+ else if (character === "`") stringDelimiter = null;
13011
+ else if (character === "$" && content[cursor + 1] === "{") {
13012
+ templateExpressionDepths.push(0);
13013
+ stringDelimiter = null;
13014
+ cursor += 1;
13015
+ }
13016
+ continue;
13017
+ }
13018
+ if (stringDelimiter !== null) {
13019
+ if (character === "\\") cursor += 1;
13020
+ else if (character === stringDelimiter) stringDelimiter = null;
13021
+ continue;
13022
+ }
13023
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
13024
+ else if (templateExpressionDepths.length > 0) {
13025
+ const top = templateExpressionDepths.length - 1;
13026
+ if (character === "{") templateExpressionDepths[top] += 1;
13027
+ else if (character === "}") if (templateExpressionDepths[top] === 0) {
13028
+ templateExpressionDepths.pop();
13029
+ stringDelimiter = "`";
13030
+ } else templateExpressionDepths[top] -= 1;
13031
+ }
13032
+ }
13033
+ return stringDelimiter !== null;
13034
+ };
13035
+ const jwtInsecureVerification = defineRule({
13036
+ id: "jwt-insecure-verification",
13037
+ title: "JWT verified with the 'none' algorithm",
13038
+ severity: "error",
13039
+ 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'] })`).",
13040
+ scan: (file) => {
13041
+ if (!isProductionSourcePath(file.relativePath)) return [];
13042
+ const content = getScannableContent(file);
13043
+ if (!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(content)) return [];
13044
+ const findings = [];
13045
+ NONE_ALGORITHM_PATTERN.lastIndex = 0;
13046
+ for (let noneMatch = NONE_ALGORITHM_PATTERN.exec(content); noneMatch !== null; noneMatch = NONE_ALGORITHM_PATTERN.exec(content)) {
13047
+ if (isIndexInsideStringLiteral(content, noneMatch.index)) continue;
13048
+ const location = getLocationAtIndex(content, noneMatch.index);
13049
+ findings.push({
13050
+ message: "JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.",
13051
+ line: location.line,
13052
+ column: location.column
13053
+ });
13054
+ }
13055
+ return findings;
13056
+ }
13057
+ });
13058
+ //#endregion
12703
13059
  //#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
12704
13060
  const keyLifecycleRisk = defineRule({
12705
13061
  id: "key-lifecycle-risk",
12706
13062
  title: "Long-lived key material in repository",
12707
13063
  severity: "error",
13064
+ committedFilesOnly: true,
12708
13065
  recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
12709
13066
  scan: scanByPattern({
12710
13067
  shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
@@ -12862,7 +13219,7 @@ const labelHasAssociatedControl = defineRule({
12862
13219
  });
12863
13220
  //#endregion
12864
13221
  //#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`.";
13222
+ const MESSAGE$37 = "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
13223
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
12867
13224
  "aa",
12868
13225
  "ab",
@@ -13074,7 +13431,7 @@ const lang = defineRule({
13074
13431
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13075
13432
  context.report({
13076
13433
  node: langAttr,
13077
- message: MESSAGE$33
13434
+ message: MESSAGE$37
13078
13435
  });
13079
13436
  return;
13080
13437
  }
@@ -13083,7 +13440,7 @@ const lang = defineRule({
13083
13440
  if (value === null) return;
13084
13441
  if (!isValidLangTag(value)) context.report({
13085
13442
  node: langAttr,
13086
- message: MESSAGE$33
13443
+ message: MESSAGE$37
13087
13444
  });
13088
13445
  } })
13089
13446
  });
@@ -13127,7 +13484,7 @@ const mdxSsrExecutionRisk = defineRule({
13127
13484
  });
13128
13485
  //#endregion
13129
13486
  //#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>`.";
13487
+ const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13131
13488
  const DEFAULT_AUDIO = ["audio"];
13132
13489
  const DEFAULT_VIDEO = ["video"];
13133
13490
  const DEFAULT_TRACK = ["track"];
@@ -13168,7 +13525,7 @@ const mediaHasCaption = defineRule({
13168
13525
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13169
13526
  context.report({
13170
13527
  node: node.name,
13171
- message: MESSAGE$32
13528
+ message: MESSAGE$36
13172
13529
  });
13173
13530
  return;
13174
13531
  }
@@ -13185,7 +13542,7 @@ const mediaHasCaption = defineRule({
13185
13542
  return kindValue.value.toLowerCase() === "captions";
13186
13543
  })) context.report({
13187
13544
  node: node.name,
13188
- message: MESSAGE$32
13545
+ message: MESSAGE$36
13189
13546
  });
13190
13547
  } };
13191
13548
  }
@@ -14986,7 +15343,7 @@ const nextjsNoVercelOgImport = defineRule({
14986
15343
  });
14987
15344
  //#endregion
14988
15345
  //#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.";
15346
+ const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
14990
15347
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
14991
15348
  const noAccessKey = defineRule({
14992
15349
  id: "no-access-key",
@@ -15003,7 +15360,7 @@ const noAccessKey = defineRule({
15003
15360
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15004
15361
  context.report({
15005
15362
  node: accessKey,
15006
- message: MESSAGE$31
15363
+ message: MESSAGE$35
15007
15364
  });
15008
15365
  return;
15009
15366
  }
@@ -15013,7 +15370,7 @@ const noAccessKey = defineRule({
15013
15370
  if (isUndefinedIdentifier(expression)) return;
15014
15371
  context.report({
15015
15372
  node: accessKey,
15016
- message: MESSAGE$31
15373
+ message: MESSAGE$35
15017
15374
  });
15018
15375
  }
15019
15376
  } })
@@ -15496,7 +15853,7 @@ const noAdjustStateOnPropChange = defineRule({
15496
15853
  });
15497
15854
  //#endregion
15498
15855
  //#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.";
15856
+ const MESSAGE$34 = "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
15857
  const noAriaHiddenOnFocusable = defineRule({
15501
15858
  id: "no-aria-hidden-on-focusable",
15502
15859
  title: "aria-hidden on focusable element",
@@ -15523,7 +15880,7 @@ const noAriaHiddenOnFocusable = defineRule({
15523
15880
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15524
15881
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15525
15882
  node: ariaHidden,
15526
- message: MESSAGE$30
15883
+ message: MESSAGE$34
15527
15884
  });
15528
15885
  } })
15529
15886
  });
@@ -15891,7 +16248,7 @@ const noArrayIndexAsKey = defineRule({
15891
16248
  });
15892
16249
  //#endregion
15893
16250
  //#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.";
16251
+ const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
15895
16252
  const SECOND_INDEX_METHODS = new Set([
15896
16253
  "every",
15897
16254
  "filter",
@@ -16095,7 +16452,7 @@ const noArrayIndexKey = defineRule({
16095
16452
  }
16096
16453
  context.report({
16097
16454
  node: keyAttribute,
16098
- message: MESSAGE$29
16455
+ message: MESSAGE$33
16099
16456
  });
16100
16457
  },
16101
16458
  CallExpression(node) {
@@ -16115,15 +16472,35 @@ const noArrayIndexKey = defineRule({
16115
16472
  if (propName !== "key") continue;
16116
16473
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16117
16474
  node: property,
16118
- message: MESSAGE$29
16475
+ message: MESSAGE$33
16119
16476
  });
16120
16477
  }
16121
16478
  }
16122
16479
  })
16123
16480
  });
16124
16481
  //#endregion
16482
+ //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16483
+ const MESSAGE$32 = "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.";
16484
+ const noAsyncEffectCallback = defineRule({
16485
+ id: "no-async-effect-callback",
16486
+ title: "Async effect callback",
16487
+ severity: "warn",
16488
+ 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.",
16489
+ create: (context) => ({ CallExpression(node) {
16490
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
16491
+ const callback = getEffectCallback(node);
16492
+ if (!callback) return;
16493
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
16494
+ if (!callback.async) return;
16495
+ context.report({
16496
+ node: callback,
16497
+ message: MESSAGE$32
16498
+ });
16499
+ } })
16500
+ });
16501
+ //#endregion
16125
16502
  //#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.";
16503
+ const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16127
16504
  const resolveSettings$21 = (settings) => {
16128
16505
  const reactDoctor = settings?.["react-doctor"];
16129
16506
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16179,7 +16556,7 @@ const noAutofocus = defineRule({
16179
16556
  }
16180
16557
  context.report({
16181
16558
  node: autoFocusAttribute,
16182
- message: MESSAGE$28
16559
+ message: MESSAGE$31
16183
16560
  });
16184
16561
  } };
16185
16562
  }
@@ -16429,6 +16806,109 @@ const noBarrelImport = defineRule({
16429
16806
  }
16430
16807
  });
16431
16808
  //#endregion
16809
+ //#region src/plugin/utils/function-contains-react-render-output.ts
16810
+ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
16811
+ "FunctionDeclaration",
16812
+ "FunctionExpression",
16813
+ "ArrowFunctionExpression",
16814
+ "ClassDeclaration",
16815
+ "ClassExpression"
16816
+ ]);
16817
+ const isReactImport$1 = (symbol) => {
16818
+ let importDeclaration = symbol.declarationNode?.parent;
16819
+ while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
16820
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
16821
+ return importDeclaration.source.value === "react";
16822
+ };
16823
+ const getImportedName = (symbol) => {
16824
+ if (symbol.kind !== "import") return null;
16825
+ if (!isReactImport$1(symbol)) return null;
16826
+ return getImportedName$1(symbol.declarationNode) ?? null;
16827
+ };
16828
+ const isReactNamespaceImport = (symbol) => {
16829
+ if (symbol.kind !== "import") return false;
16830
+ if (!isReactImport$1(symbol)) return false;
16831
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
16832
+ };
16833
+ const isReactCreateElementIdentifierCall = (callee, scopes) => {
16834
+ if (!isNodeOfType(callee, "Identifier")) return false;
16835
+ const symbol = scopes.symbolFor(callee);
16836
+ return Boolean(symbol && getImportedName(symbol) === "createElement");
16837
+ };
16838
+ const isReactCreateElementMemberCall = (callee, scopes) => {
16839
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
16840
+ if (callee.computed) return false;
16841
+ if (!isNodeOfType(callee.object, "Identifier")) return false;
16842
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
16843
+ if (callee.property.name !== "createElement") return false;
16844
+ const symbol = scopes.symbolFor(callee.object);
16845
+ return Boolean(symbol && isReactNamespaceImport(symbol));
16846
+ };
16847
+ const isReactCreateElementCall = (node, scopes) => {
16848
+ if (!isNodeOfType(node, "CallExpression")) return false;
16849
+ return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
16850
+ };
16851
+ const containsRenderOutput = (node, rootNode, scopes) => {
16852
+ if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
16853
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
16854
+ if (isReactCreateElementCall(node, scopes)) return true;
16855
+ const nodeRecord = node;
16856
+ for (const key of Object.keys(nodeRecord)) {
16857
+ if (key === "parent") continue;
16858
+ const child = nodeRecord[key];
16859
+ if (Array.isArray(child)) {
16860
+ for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
16861
+ } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
16862
+ }
16863
+ return false;
16864
+ };
16865
+ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
16866
+ //#endregion
16867
+ //#region src/plugin/utils/is-component-declaration.ts
16868
+ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
16869
+ //#endregion
16870
+ //#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
16871
+ 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.`;
16872
+ const symbolIsLocalComponent = (symbol, context) => {
16873
+ const declaration = symbol.declarationNode;
16874
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
16875
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
16876
+ return false;
16877
+ };
16878
+ const noCallComponentAsFunction = defineRule({
16879
+ id: "no-call-component-as-function",
16880
+ title: "Component called as a function",
16881
+ severity: "warn",
16882
+ tags: ["test-noise"],
16883
+ 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.",
16884
+ create: (context) => {
16885
+ const renderedJsxNames = /* @__PURE__ */ new Set();
16886
+ const candidateCalls = [];
16887
+ return {
16888
+ JSXOpeningElement(node) {
16889
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
16890
+ },
16891
+ CallExpression(node) {
16892
+ if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
16893
+ node,
16894
+ callee: node.callee,
16895
+ name: node.callee.name
16896
+ });
16897
+ },
16898
+ "Program:exit"() {
16899
+ for (const candidate of candidateCalls) {
16900
+ const symbol = context.scopes.symbolFor(candidate.callee);
16901
+ if (!symbol) continue;
16902
+ if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
16903
+ node: candidate.node,
16904
+ message: message(candidate.name)
16905
+ });
16906
+ }
16907
+ }
16908
+ };
16909
+ }
16910
+ });
16911
+ //#endregion
16432
16912
  //#region src/plugin/utils/is-setter-identifier.ts
16433
16913
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
16434
16914
  //#endregion
@@ -16580,7 +17060,7 @@ const noChainStateUpdates = defineRule({
16580
17060
  });
16581
17061
  //#endregion
16582
17062
  //#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.";
17063
+ const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16584
17064
  const noChildrenProp = defineRule({
16585
17065
  id: "no-children-prop",
16586
17066
  title: "Children passed as a prop",
@@ -16592,7 +17072,7 @@ const noChildrenProp = defineRule({
16592
17072
  if (node.name.name !== "children") return;
16593
17073
  context.report({
16594
17074
  node: node.name,
16595
- message: MESSAGE$27
17075
+ message: MESSAGE$30
16596
17076
  });
16597
17077
  },
16598
17078
  CallExpression(node) {
@@ -16605,7 +17085,7 @@ const noChildrenProp = defineRule({
16605
17085
  const propertyKey = property.key;
16606
17086
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
16607
17087
  node: propertyKey,
16608
- message: MESSAGE$27
17088
+ message: MESSAGE$30
16609
17089
  });
16610
17090
  }
16611
17091
  }
@@ -16613,7 +17093,7 @@ const noChildrenProp = defineRule({
16613
17093
  });
16614
17094
  //#endregion
16615
17095
  //#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.";
17096
+ const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
16617
17097
  const noCloneElement = defineRule({
16618
17098
  id: "no-clone-element",
16619
17099
  title: "cloneElement makes child props fragile",
@@ -16626,7 +17106,7 @@ const noCloneElement = defineRule({
16626
17106
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
16627
17107
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
16628
17108
  node: callee,
16629
- message: MESSAGE$26
17109
+ message: MESSAGE$29
16630
17110
  });
16631
17111
  return;
16632
17112
  }
@@ -16639,7 +17119,7 @@ const noCloneElement = defineRule({
16639
17119
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
16640
17120
  context.report({
16641
17121
  node: callee,
16642
- message: MESSAGE$26
17122
+ message: MESSAGE$29
16643
17123
  });
16644
17124
  }
16645
17125
  } })
@@ -16688,7 +17168,7 @@ const enclosingComponentOrHookName = (node) => {
16688
17168
  };
16689
17169
  //#endregion
16690
17170
  //#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.";
17171
+ const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
16692
17172
  const CONTEXT_MODULES = [
16693
17173
  "react",
16694
17174
  "use-context-selector",
@@ -16724,7 +17204,32 @@ const noCreateContextInRender = defineRule({
16724
17204
  if (!componentOrHookName) return;
16725
17205
  context.report({
16726
17206
  node,
16727
- message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
17207
+ message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
17208
+ });
17209
+ } })
17210
+ });
17211
+ //#endregion
17212
+ //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17213
+ const MESSAGE$27 = "`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.";
17214
+ const noCreateRefInFunctionComponent = defineRule({
17215
+ id: "no-create-ref-in-function-component",
17216
+ title: "createRef in function component",
17217
+ severity: "warn",
17218
+ recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
17219
+ create: (context) => ({ CallExpression(node) {
17220
+ if (!isReactFunctionCall(node, "createRef")) return;
17221
+ if (isNodeOfType(node.callee, "Identifier")) {
17222
+ const symbol = context.scopes.symbolFor(node.callee);
17223
+ if (symbol && symbol.kind !== "import") return;
17224
+ }
17225
+ const enclosingFunction = nearestEnclosingFunction(node);
17226
+ if (!enclosingFunction) return;
17227
+ const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
17228
+ if (!displayName) return;
17229
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17230
+ context.report({
17231
+ node,
17232
+ message: MESSAGE$27
16728
17233
  });
16729
17234
  } })
16730
17235
  });
@@ -16864,7 +17369,7 @@ const noCreateStoreInRender = defineRule({
16864
17369
  });
16865
17370
  //#endregion
16866
17371
  //#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.";
17372
+ const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
16868
17373
  const noDanger = defineRule({
16869
17374
  id: "no-danger",
16870
17375
  title: "Raw HTML injection can run unsafe markup",
@@ -16877,7 +17382,7 @@ const noDanger = defineRule({
16877
17382
  if (!propAttribute) return;
16878
17383
  context.report({
16879
17384
  node: propAttribute.name,
16880
- message: MESSAGE$24
17385
+ message: MESSAGE$26
16881
17386
  });
16882
17387
  },
16883
17388
  CallExpression(node) {
@@ -16889,7 +17394,7 @@ const noDanger = defineRule({
16889
17394
  const propertyKey = property.key;
16890
17395
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
16891
17396
  node: propertyKey,
16892
- message: MESSAGE$24
17397
+ message: MESSAGE$26
16893
17398
  });
16894
17399
  }
16895
17400
  }
@@ -16897,7 +17402,7 @@ const noDanger = defineRule({
16897
17402
  });
16898
17403
  //#endregion
16899
17404
  //#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`.";
17405
+ const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
16901
17406
  const isLineBreak = (child) => {
16902
17407
  if (!isNodeOfType(child, "JSXText")) return false;
16903
17408
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -16967,7 +17472,7 @@ const noDangerWithChildren = defineRule({
16967
17472
  if (!hasChildrenProp && !hasNestedChildren) return;
16968
17473
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
16969
17474
  node: opening,
16970
- message: MESSAGE$23
17475
+ message: MESSAGE$25
16971
17476
  });
16972
17477
  },
16973
17478
  CallExpression(node) {
@@ -16979,7 +17484,7 @@ const noDangerWithChildren = defineRule({
16979
17484
  if (!propsShape.hasDangerously) return;
16980
17485
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
16981
17486
  node,
16982
- message: MESSAGE$23
17487
+ message: MESSAGE$25
16983
17488
  });
16984
17489
  }
16985
17490
  })
@@ -17556,7 +18061,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17556
18061
  //#endregion
17557
18062
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17558
18063
  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`.";
18064
+ const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17560
18065
  const resolveSettings$20 = (settings) => {
17561
18066
  const reactDoctor = settings?.["react-doctor"];
17562
18067
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -17575,7 +18080,7 @@ const noDidMountSetState = defineRule({
17575
18080
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17576
18081
  context.report({
17577
18082
  node: node.callee,
17578
- message: MESSAGE$22
18083
+ message: MESSAGE$24
17579
18084
  });
17580
18085
  } };
17581
18086
  }
@@ -17583,7 +18088,7 @@ const noDidMountSetState = defineRule({
17583
18088
  //#endregion
17584
18089
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
17585
18090
  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.";
18091
+ const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
17587
18092
  const resolveSettings$19 = (settings) => {
17588
18093
  const reactDoctor = settings?.["react-doctor"];
17589
18094
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -17602,7 +18107,7 @@ const noDidUpdateSetState = defineRule({
17602
18107
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17603
18108
  context.report({
17604
18109
  node: node.callee,
17605
- message: MESSAGE$21
18110
+ message: MESSAGE$23
17606
18111
  });
17607
18112
  } };
17608
18113
  }
@@ -17625,7 +18130,7 @@ const isStateMemberExpression = (node) => {
17625
18130
  };
17626
18131
  //#endregion
17627
18132
  //#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.";
18133
+ const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
17629
18134
  const shouldIgnoreMutation = (node) => {
17630
18135
  let isConstructor = false;
17631
18136
  let isInsideCallExpression = false;
@@ -17647,7 +18152,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
17647
18152
  if (shouldIgnoreMutation(reportNode)) return;
17648
18153
  context.report({
17649
18154
  node: reportNode,
17650
- message: MESSAGE$20
18155
+ message: MESSAGE$22
17651
18156
  });
17652
18157
  };
17653
18158
  const noDirectMutationState = defineRule({
@@ -19235,7 +19740,7 @@ const ALLOWED_NAMESPACES = new Set([
19235
19740
  "ReactDOM",
19236
19741
  "ReactDom"
19237
19742
  ]);
19238
- const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19743
+ const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19239
19744
  const noFindDomNode = defineRule({
19240
19745
  id: "no-find-dom-node",
19241
19746
  title: "findDOMNode breaks component encapsulation",
@@ -19246,7 +19751,7 @@ const noFindDomNode = defineRule({
19246
19751
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19247
19752
  context.report({
19248
19753
  node: callee,
19249
- message: MESSAGE$19
19754
+ message: MESSAGE$21
19250
19755
  });
19251
19756
  return;
19252
19757
  }
@@ -19257,7 +19762,7 @@ const noFindDomNode = defineRule({
19257
19762
  if (callee.property.name !== "findDOMNode") return;
19258
19763
  context.report({
19259
19764
  node: callee.property,
19260
- message: MESSAGE$19
19765
+ message: MESSAGE$21
19261
19766
  });
19262
19767
  }
19263
19768
  } })
@@ -19320,64 +19825,6 @@ const noGenericHandlerNames = defineRule({
19320
19825
  } })
19321
19826
  });
19322
19827
  //#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
19828
  //#region src/plugin/rules/architecture/no-giant-component.ts
19382
19829
  const noGiantComponent = defineRule({
19383
19830
  id: "no-giant-component",
@@ -19556,6 +20003,26 @@ const noGrayOnColoredBackground = defineRule({
19556
20003
  } })
19557
20004
  });
19558
20005
  //#endregion
20006
+ //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20007
+ const MESSAGE$20 = "`<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.";
20008
+ const noImgLazyWithHighFetchpriority = defineRule({
20009
+ id: "no-img-lazy-with-high-fetchpriority",
20010
+ title: "Lazy image with high fetchPriority",
20011
+ severity: "warn",
20012
+ 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.",
20013
+ create: (context) => ({ JSXOpeningElement(node) {
20014
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
20015
+ const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
20016
+ if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
20017
+ const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
20018
+ if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20019
+ context.report({
20020
+ node: node.name,
20021
+ message: MESSAGE$20
20022
+ });
20023
+ } })
20024
+ });
20025
+ //#endregion
19559
20026
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
19560
20027
  const noInitializeState = defineRule({
19561
20028
  id: "no-initialize-state",
@@ -19785,6 +20252,29 @@ const noIsMounted = defineRule({
19785
20252
  } })
19786
20253
  });
19787
20254
  //#endregion
20255
+ //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20256
+ const MESSAGE$19 = "`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)`.";
20257
+ const isJsonMethodCall = (node, method) => {
20258
+ if (!isNodeOfType(node, "CallExpression")) return false;
20259
+ const callee = node.callee;
20260
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
20261
+ };
20262
+ const noJsonParseStringifyClone = defineRule({
20263
+ id: "no-json-parse-stringify-clone",
20264
+ title: "JSON parse/stringify deep clone",
20265
+ severity: "warn",
20266
+ recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
20267
+ create: (context) => ({ CallExpression(node) {
20268
+ if (!isJsonMethodCall(node, "parse")) return;
20269
+ const firstArgument = node.arguments?.[0];
20270
+ if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20271
+ context.report({
20272
+ node,
20273
+ message: MESSAGE$19
20274
+ });
20275
+ } })
20276
+ });
20277
+ //#endregion
19788
20278
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
19789
20279
  const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
19790
20280
  const isJsxElementTypeReference = (node) => {
@@ -20107,9 +20597,6 @@ const noLongTransitionDuration = defineRule({
20107
20597
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20108
20598
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
20109
20599
  //#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
20600
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
20114
20601
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
20115
20602
  const found = /* @__PURE__ */ new Set();
@@ -22527,11 +23014,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
22527
23014
  return "unknown";
22528
23015
  };
22529
23016
  //#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();
23017
+ //#region src/plugin/utils/tokenize-identifier-words.ts
23018
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
23019
+ const tokenizeIdentifierWords = (identifierName) => {
23020
+ const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
23021
+ if (!words) return [];
23022
+ return words.map((word) => word.toLowerCase());
22533
23023
  };
22534
23024
  //#endregion
23025
+ //#region src/plugin/utils/get-identifier-trailing-word.ts
23026
+ const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
23027
+ //#endregion
22535
23028
  //#region src/plugin/constants/tanstack.ts
22536
23029
  const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
22537
23030
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
@@ -27035,6 +27528,8 @@ const publicEnvSecretName = defineRule({
27035
27528
  });
27036
27529
  //#endregion
27037
27530
  //#region src/plugin/rules/tanstack-query/query-destructure-result.ts
27531
+ const TANSTACK_QUERY_PACKAGE_PATTERN = /^@tanstack\/[\w-]*query[\w-]*$/;
27532
+ const isTanstackQuerySource = (source) => TANSTACK_QUERY_PACKAGE_PATTERN.test(source) || source === "react-query";
27038
27533
  const queryDestructureResult = defineRule({
27039
27534
  id: "query-destructure-result",
27040
27535
  title: "Whole query result subscribes to every field",
@@ -27047,6 +27542,8 @@ const queryDestructureResult = defineRule({
27047
27542
  if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
27048
27543
  const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
27049
27544
  if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
27545
+ const importSource = getImportSourceForName(node, calleeName);
27546
+ if (importSource !== null && !isTanstackQuerySource(importSource)) return;
27050
27547
  context.report({
27051
27548
  node: node.id,
27052
27549
  message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
@@ -27889,6 +28386,7 @@ const repositorySecretFile = defineRule({
27889
28386
  id: "repository-secret-file",
27890
28387
  title: "Secret file checked into repository",
27891
28388
  severity: "error",
28389
+ committedFilesOnly: true,
27892
28390
  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
28391
  scan: (file) => {
27894
28392
  if (!isRepositorySecretFilePath(file.relativePath)) return [];
@@ -27905,6 +28403,20 @@ const repositorySecretFile = defineRule({
27905
28403
  }
27906
28404
  });
27907
28405
  //#endregion
28406
+ //#region src/plugin/rules/security-scan/request-body-mass-assignment.ts
28407
+ const REQUEST_INPUT_SOURCE = "(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)";
28408
+ const requestBodyMassAssignment = defineRule({
28409
+ id: "request-body-mass-assignment",
28410
+ title: "Request input spread without field allowlist",
28411
+ severity: "warn",
28412
+ 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.",
28413
+ scan: scanByPattern({
28414
+ shouldScan: (file) => isProductionSourcePath(file.relativePath),
28415
+ 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")],
28416
+ 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."
28417
+ })
28418
+ });
28419
+ //#endregion
27908
28420
  //#region src/plugin/utils/function-body-has-return-with-value.ts
27909
28421
  const functionBodyHasReturnWithValue = (functionNode) => {
27910
28422
  if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
@@ -34330,6 +34842,17 @@ const scope = defineRule({
34330
34842
  });
34331
34843
  } })
34332
34844
  });
34845
+ const secretInFallback = defineRule({
34846
+ id: "secret-in-fallback",
34847
+ title: "Hardcoded secret fallback for env var",
34848
+ severity: "error",
34849
+ 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.",
34850
+ scan: scanByPattern({
34851
+ shouldScan: (file) => isProductionSourcePath(file.relativePath),
34852
+ 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,
34853
+ 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."
34854
+ })
34855
+ });
34333
34856
  //#endregion
34334
34857
  //#region src/plugin/rules/react-builtins/self-closing-comp.ts
34335
34858
  const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
@@ -34448,6 +34971,47 @@ const serverAfterNonblocking = defineRule({
34448
34971
  }
34449
34972
  });
34450
34973
  //#endregion
34974
+ //#region src/plugin/utils/is-auth-guard-name.ts
34975
+ const SIGNED_IN_HEAD_TOKENS = new Set([
34976
+ "signed",
34977
+ "logged",
34978
+ "sign"
34979
+ ]);
34980
+ const mergeSignedInTokens = (tokens) => {
34981
+ const mergedTokens = [];
34982
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
34983
+ const currentToken = tokens[tokenIndex];
34984
+ if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
34985
+ mergedTokens.push(`${currentToken}in`);
34986
+ tokenIndex += 1;
34987
+ continue;
34988
+ }
34989
+ mergedTokens.push(currentToken);
34990
+ }
34991
+ return mergedTokens;
34992
+ };
34993
+ const isAuthGuardName = (calleeName) => {
34994
+ const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
34995
+ if (tokens.length === 0) return false;
34996
+ let hasAssertiveVerb = false;
34997
+ let hasGetterVerb = false;
34998
+ let hasQualifier = false;
34999
+ let hasStrongNoun = false;
35000
+ let hasWeakNoun = false;
35001
+ for (const token of tokens) {
35002
+ if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
35003
+ if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
35004
+ if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
35005
+ if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
35006
+ if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
35007
+ if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
35008
+ }
35009
+ if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
35010
+ if (hasGetterVerb && hasStrongNoun) return true;
35011
+ if (hasQualifier && hasWeakNoun) return true;
35012
+ return false;
35013
+ };
35014
+ //#endregion
34451
35015
  //#region src/plugin/rules/server/server-auth-actions.ts
34452
35016
  const isAsyncFunctionLikeNode = (node) => {
34453
35017
  if (!node) return false;
@@ -34490,9 +35054,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
34490
35054
  const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
34491
35055
  const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
34492
35056
  if (!calleeNode) return null;
34493
- if (isNodeOfType(calleeNode, "Identifier")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
35057
+ if (isNodeOfType(calleeNode, "Identifier")) {
35058
+ const calleeName = calleeNode.name;
35059
+ return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
35060
+ }
34494
35061
  if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
34495
35062
  const methodName = calleeNode.property.name;
35063
+ if (isAuthGuardName(methodName)) return methodName;
34496
35064
  if (!allowedFunctionNames.has(methodName)) return null;
34497
35065
  if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
34498
35066
  return methodName;
@@ -35139,8 +35707,11 @@ const supabaseClientOwnedAuthzField = defineRule({
35139
35707
  })
35140
35708
  });
35141
35709
  //#endregion
35710
+ //#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
35711
+ const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
35712
+ //#endregion
35142
35713
  //#region src/plugin/rules/security-scan/utils/is-sql-path.ts
35143
- const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
35714
+ const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
35144
35715
  const supabaseRlsPolicyRisk = defineRule({
35145
35716
  id: "supabase-rls-policy-risk",
35146
35717
  title: "Permissive Supabase RLS policy",
@@ -35158,6 +35729,210 @@ const supabaseRlsPolicyRisk = defineRule({
35158
35729
  })
35159
35730
  });
35160
35731
  //#endregion
35732
+ //#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
35733
+ const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
35734
+ const CODE_BODY_KEYWORDS = new Set([
35735
+ "do",
35736
+ "plpgsql",
35737
+ "sql",
35738
+ "plpython3u",
35739
+ "plpythonu",
35740
+ "plperl",
35741
+ "plperlu",
35742
+ "plv8"
35743
+ ]);
35744
+ const precedingKeyword = (content, beforeIndex) => {
35745
+ let lookBack = beforeIndex - 1;
35746
+ while (lookBack >= 0 && /\s/.test(content[lookBack] ?? "")) lookBack -= 1;
35747
+ let wordStart = lookBack;
35748
+ while (wordStart >= 0 && /[A-Za-z0-9_]/.test(content[wordStart] ?? "")) wordStart -= 1;
35749
+ return content.slice(wordStart + 1, lookBack + 1).toLowerCase();
35750
+ };
35751
+ const blankCodeBodyInterior = (content, characters, start, end) => {
35752
+ let index = start;
35753
+ let inExecuteStatement = false;
35754
+ while (index < end) {
35755
+ const character = content[index];
35756
+ if (character === ";") {
35757
+ inExecuteStatement = false;
35758
+ index += 1;
35759
+ continue;
35760
+ }
35761
+ if (/[A-Za-z_]/.test(character)) {
35762
+ let wordEnd = index;
35763
+ while (wordEnd < end && /[A-Za-z0-9_]/.test(content[wordEnd] ?? "")) wordEnd += 1;
35764
+ if (content.slice(index, wordEnd).toLowerCase() === "execute") inExecuteStatement = true;
35765
+ index = wordEnd;
35766
+ continue;
35767
+ }
35768
+ if (character === "'") {
35769
+ const keepVisible = inExecuteStatement;
35770
+ if (!keepVisible) characters[index] = " ";
35771
+ index += 1;
35772
+ while (index < end) {
35773
+ if (content[index] === "'") {
35774
+ if (content[index + 1] === "'") {
35775
+ if (!keepVisible) {
35776
+ characters[index] = " ";
35777
+ characters[index + 1] = " ";
35778
+ }
35779
+ index += 2;
35780
+ continue;
35781
+ }
35782
+ if (!keepVisible) characters[index] = " ";
35783
+ index += 1;
35784
+ break;
35785
+ }
35786
+ if (!keepVisible && content[index] !== "\n") characters[index] = " ";
35787
+ index += 1;
35788
+ }
35789
+ continue;
35790
+ }
35791
+ if (character === "\"") {
35792
+ index += 1;
35793
+ while (index < end) {
35794
+ if (content[index] === "\"") {
35795
+ if (content[index + 1] === "\"") {
35796
+ index += 2;
35797
+ continue;
35798
+ }
35799
+ index += 1;
35800
+ break;
35801
+ }
35802
+ index += 1;
35803
+ }
35804
+ continue;
35805
+ }
35806
+ if (character === "-" && content[index + 1] === "-") {
35807
+ while (index < end && content[index] !== "\n") {
35808
+ characters[index] = " ";
35809
+ index += 1;
35810
+ }
35811
+ continue;
35812
+ }
35813
+ if (character === "/" && content[index + 1] === "*") {
35814
+ while (index < end) {
35815
+ if (content[index] === "*" && content[index + 1] === "/") {
35816
+ characters[index] = " ";
35817
+ characters[index + 1] = " ";
35818
+ index += 2;
35819
+ break;
35820
+ }
35821
+ if (content[index] !== "\n") characters[index] = " ";
35822
+ index += 1;
35823
+ }
35824
+ continue;
35825
+ }
35826
+ index += 1;
35827
+ }
35828
+ };
35829
+ const sanitizeSqlForScan = (content) => {
35830
+ const characters = content.split("");
35831
+ let index = 0;
35832
+ while (index < content.length) {
35833
+ const character = content[index];
35834
+ if (character === "-" && content[index + 1] === "-") {
35835
+ while (index < content.length && content[index] !== "\n") {
35836
+ characters[index] = " ";
35837
+ index += 1;
35838
+ }
35839
+ continue;
35840
+ }
35841
+ if (character === "/" && content[index + 1] === "*") {
35842
+ while (index < content.length) {
35843
+ if (content[index] === "*" && content[index + 1] === "/") {
35844
+ characters[index] = " ";
35845
+ characters[index + 1] = " ";
35846
+ index += 2;
35847
+ break;
35848
+ }
35849
+ if (content[index] !== "\n") characters[index] = " ";
35850
+ index += 1;
35851
+ }
35852
+ continue;
35853
+ }
35854
+ if (character === "'") {
35855
+ characters[index] = " ";
35856
+ index += 1;
35857
+ while (index < content.length) {
35858
+ if (content[index] === "'") {
35859
+ if (content[index + 1] === "'") {
35860
+ characters[index] = " ";
35861
+ characters[index + 1] = " ";
35862
+ index += 2;
35863
+ continue;
35864
+ }
35865
+ characters[index] = " ";
35866
+ index += 1;
35867
+ break;
35868
+ }
35869
+ if (content[index] !== "\n") characters[index] = " ";
35870
+ index += 1;
35871
+ }
35872
+ continue;
35873
+ }
35874
+ if (character === "$") {
35875
+ const tagMatch = DOLLAR_QUOTE_TAG_PATTERN.exec(content.slice(index));
35876
+ if (tagMatch !== null) {
35877
+ const tag = tagMatch[0];
35878
+ const closeIndex = content.indexOf(tag, index + tag.length);
35879
+ const endIndex = closeIndex < 0 ? content.length : closeIndex + tag.length;
35880
+ const keyword = precedingKeyword(content, index);
35881
+ if (CODE_BODY_KEYWORDS.has(keyword)) blankCodeBodyInterior(content, characters, index + tag.length, endIndex);
35882
+ else for (let blankIndex = index; blankIndex < endIndex; blankIndex += 1) if (content[blankIndex] !== "\n") characters[blankIndex] = " ";
35883
+ index = endIndex;
35884
+ continue;
35885
+ }
35886
+ }
35887
+ if (character === "\"") {
35888
+ index += 1;
35889
+ while (index < content.length) {
35890
+ if (content[index] === "\"") {
35891
+ if (content[index + 1] === "\"") {
35892
+ index += 2;
35893
+ continue;
35894
+ }
35895
+ index += 1;
35896
+ break;
35897
+ }
35898
+ index += 1;
35899
+ }
35900
+ continue;
35901
+ }
35902
+ index += 1;
35903
+ }
35904
+ return characters.join("");
35905
+ };
35906
+ //#endregion
35907
+ //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
35908
+ 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;
35909
+ 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");
35910
+ const supabaseTableMissingRls = defineRule({
35911
+ id: "supabase-table-missing-rls",
35912
+ title: "Supabase table created without Row Level Security",
35913
+ severity: "error",
35914
+ 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.",
35915
+ scan: (file) => {
35916
+ if (!isSupabaseMigrationPath(file.relativePath)) return [];
35917
+ const content = sanitizeSqlForScan(file.content);
35918
+ if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
35919
+ const findings = [];
35920
+ CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
35921
+ for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
35922
+ const tableName = match[1];
35923
+ if (tableName === void 0) continue;
35924
+ if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
35925
+ const location = getLocationAtIndex(content, match.index);
35926
+ findings.push({
35927
+ message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
35928
+ line: location.line,
35929
+ column: location.column
35930
+ });
35931
+ }
35932
+ return findings;
35933
+ }
35934
+ });
35935
+ //#endregion
35161
35936
  //#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
35162
35937
  const svgFilterClickjackingRisk = defineRule({
35163
35938
  id: "svg-filter-clickjacking-risk",
@@ -35856,6 +36631,47 @@ const tenantStaticProxyRisk = defineRule({
35856
36631
  })
35857
36632
  });
35858
36633
  //#endregion
36634
+ //#region src/plugin/rules/security-scan/unsafe-json-in-html.ts
36635
+ 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];
36636
+ const RETURN_ESCAPE_PATTERN = /^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|&lt;|<)/;
36637
+ const ESCAPE_WRAPPER_PATTERN = /(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i;
36638
+ const JSON_STRINGIFY_TOKEN_PATTERN = /\bJSON\.stringify\s*\($/i;
36639
+ const RETURN_LOOKAHEAD_CHARS = 160;
36640
+ const unsafeJsonInHtml = defineRule({
36641
+ id: "unsafe-json-in-html",
36642
+ title: "Unescaped JSON in HTML or script sink",
36643
+ severity: "warn",
36644
+ 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.",
36645
+ scan: (file) => {
36646
+ if (!isProductionSourcePath(file.relativePath)) return [];
36647
+ const content = getScannableContent(file);
36648
+ if (!content.includes("JSON.stringify")) return [];
36649
+ const findings = [];
36650
+ const seenIndices = /* @__PURE__ */ new Set();
36651
+ for (const pattern of SINK_JSON_STRINGIFY_PATTERNS) {
36652
+ pattern.lastIndex = 0;
36653
+ for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
36654
+ const beforeStringify = match[0].replace(JSON_STRINGIFY_TOKEN_PATTERN, "");
36655
+ if (ESCAPE_WRAPPER_PATTERN.test(beforeStringify)) continue;
36656
+ const closeParenIndex = findMatchingBracket(content, match.index + match[0].length - 1);
36657
+ if (closeParenIndex >= 0) {
36658
+ const afterReturn = content.slice(closeParenIndex + 1, closeParenIndex + 1 + RETURN_LOOKAHEAD_CHARS);
36659
+ if (RETURN_ESCAPE_PATTERN.test(afterReturn)) continue;
36660
+ }
36661
+ if (seenIndices.has(match.index)) continue;
36662
+ seenIndices.add(match.index);
36663
+ const location = getLocationAtIndex(content, match.index);
36664
+ findings.push({
36665
+ message: "JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",
36666
+ line: location.line,
36667
+ column: location.column
36668
+ });
36669
+ }
36670
+ }
36671
+ return findings;
36672
+ }
36673
+ });
36674
+ //#endregion
35859
36675
  //#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
35860
36676
  const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
35861
36677
  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;
@@ -36003,7 +36819,7 @@ const voidDomElementsNoChildren = defineRule({
36003
36819
  //#region src/plugin/rules/security-scan/webhook-signature-risk.ts
36004
36820
  const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
36005
36821
  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;
36822
+ 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
36823
  const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
36008
36824
  const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
36009
36825
  const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
@@ -36663,6 +37479,17 @@ const reactDoctorRules = [
36663
37479
  category: "Performance"
36664
37480
  }
36665
37481
  },
37482
+ {
37483
+ key: "react-doctor/auth-token-in-web-storage",
37484
+ id: "auth-token-in-web-storage",
37485
+ source: "react-doctor",
37486
+ originallyExternal: false,
37487
+ rule: {
37488
+ ...authTokenInWebStorage,
37489
+ framework: "global",
37490
+ category: "Security"
37491
+ }
37492
+ },
36666
37493
  {
36667
37494
  key: "react-doctor/autocomplete-valid",
36668
37495
  id: "autocomplete-valid",
@@ -36879,6 +37706,18 @@ const reactDoctorRules = [
36879
37706
  requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
36880
37707
  }
36881
37708
  },
37709
+ {
37710
+ key: "react-doctor/dialog-has-accessible-name",
37711
+ id: "dialog-has-accessible-name",
37712
+ source: "react-doctor",
37713
+ originallyExternal: false,
37714
+ rule: {
37715
+ ...dialogHasAccessibleName,
37716
+ framework: "global",
37717
+ category: "Accessibility",
37718
+ requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
37719
+ }
37720
+ },
36882
37721
  {
36883
37722
  key: "react-doctor/display-name",
36884
37723
  id: "display-name",
@@ -37164,6 +38003,18 @@ const reactDoctorRules = [
37164
38003
  tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
37165
38004
  }
37166
38005
  },
38006
+ {
38007
+ key: "react-doctor/insecure-session-cookie",
38008
+ id: "insecure-session-cookie",
38009
+ source: "react-doctor",
38010
+ originallyExternal: false,
38011
+ rule: {
38012
+ ...insecureSessionCookie,
38013
+ framework: "global",
38014
+ category: "Security",
38015
+ tags: [...new Set(["security-scan", ...insecureSessionCookie.tags ?? []])]
38016
+ }
38017
+ },
37167
38018
  {
37168
38019
  key: "react-doctor/interactive-supports-focus",
37169
38020
  id: "interactive-supports-focus",
@@ -37606,6 +38457,18 @@ const reactDoctorRules = [
37606
38457
  requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
37607
38458
  }
37608
38459
  },
38460
+ {
38461
+ key: "react-doctor/jwt-insecure-verification",
38462
+ id: "jwt-insecure-verification",
38463
+ source: "react-doctor",
38464
+ originallyExternal: false,
38465
+ rule: {
38466
+ ...jwtInsecureVerification,
38467
+ framework: "global",
38468
+ category: "Security",
38469
+ tags: [...new Set(["security-scan", ...jwtInsecureVerification.tags ?? []])]
38470
+ }
38471
+ },
37609
38472
  {
37610
38473
  key: "react-doctor/key-lifecycle-risk",
37611
38474
  id: "key-lifecycle-risk",
@@ -38014,6 +38877,18 @@ const reactDoctorRules = [
38014
38877
  requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
38015
38878
  }
38016
38879
  },
38880
+ {
38881
+ key: "react-doctor/no-async-effect-callback",
38882
+ id: "no-async-effect-callback",
38883
+ source: "react-doctor",
38884
+ originallyExternal: false,
38885
+ rule: {
38886
+ ...noAsyncEffectCallback,
38887
+ framework: "global",
38888
+ category: "Bugs",
38889
+ requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
38890
+ }
38891
+ },
38017
38892
  {
38018
38893
  key: "react-doctor/no-autofocus",
38019
38894
  id: "no-autofocus",
@@ -38037,6 +38912,18 @@ const reactDoctorRules = [
38037
38912
  category: "Performance"
38038
38913
  }
38039
38914
  },
38915
+ {
38916
+ key: "react-doctor/no-call-component-as-function",
38917
+ id: "no-call-component-as-function",
38918
+ source: "react-doctor",
38919
+ originallyExternal: false,
38920
+ rule: {
38921
+ ...noCallComponentAsFunction,
38922
+ framework: "global",
38923
+ category: "Bugs",
38924
+ requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
38925
+ }
38926
+ },
38040
38927
  {
38041
38928
  key: "react-doctor/no-cascading-set-state",
38042
38929
  id: "no-cascading-set-state",
@@ -38097,6 +38984,18 @@ const reactDoctorRules = [
38097
38984
  requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
38098
38985
  }
38099
38986
  },
38987
+ {
38988
+ key: "react-doctor/no-create-ref-in-function-component",
38989
+ id: "no-create-ref-in-function-component",
38990
+ source: "react-doctor",
38991
+ originallyExternal: false,
38992
+ rule: {
38993
+ ...noCreateRefInFunctionComponent,
38994
+ framework: "global",
38995
+ category: "Bugs",
38996
+ requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
38997
+ }
38998
+ },
38100
38999
  {
38101
39000
  key: "react-doctor/no-create-store-in-render",
38102
39001
  id: "no-create-store-in-render",
@@ -38471,6 +39370,18 @@ const reactDoctorRules = [
38471
39370
  category: "Accessibility"
38472
39371
  }
38473
39372
  },
39373
+ {
39374
+ key: "react-doctor/no-img-lazy-with-high-fetchpriority",
39375
+ id: "no-img-lazy-with-high-fetchpriority",
39376
+ source: "react-doctor",
39377
+ originallyExternal: false,
39378
+ rule: {
39379
+ ...noImgLazyWithHighFetchpriority,
39380
+ framework: "global",
39381
+ category: "Performance",
39382
+ requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
39383
+ }
39384
+ },
38474
39385
  {
38475
39386
  key: "react-doctor/no-initialize-state",
38476
39387
  id: "no-initialize-state",
@@ -38541,6 +39452,17 @@ const reactDoctorRules = [
38541
39452
  requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
38542
39453
  }
38543
39454
  },
39455
+ {
39456
+ key: "react-doctor/no-json-parse-stringify-clone",
39457
+ id: "no-json-parse-stringify-clone",
39458
+ source: "react-doctor",
39459
+ originallyExternal: false,
39460
+ rule: {
39461
+ ...noJsonParseStringifyClone,
39462
+ framework: "global",
39463
+ category: "Performance"
39464
+ }
39465
+ },
38544
39466
  {
38545
39467
  key: "react-doctor/no-jsx-element-type",
38546
39468
  id: "no-jsx-element-type",
@@ -39756,6 +40678,18 @@ const reactDoctorRules = [
39756
40678
  tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
39757
40679
  }
39758
40680
  },
40681
+ {
40682
+ key: "react-doctor/request-body-mass-assignment",
40683
+ id: "request-body-mass-assignment",
40684
+ source: "react-doctor",
40685
+ originallyExternal: false,
40686
+ rule: {
40687
+ ...requestBodyMassAssignment,
40688
+ framework: "global",
40689
+ category: "Security",
40690
+ tags: [...new Set(["security-scan", ...requestBodyMassAssignment.tags ?? []])]
40691
+ }
40692
+ },
39759
40693
  {
39760
40694
  key: "react-doctor/require-render-return",
39761
40695
  id: "require-render-return",
@@ -40344,6 +41278,18 @@ const reactDoctorRules = [
40344
41278
  requires: [...new Set(["react", ...scope.requires ?? []])]
40345
41279
  }
40346
41280
  },
41281
+ {
41282
+ key: "react-doctor/secret-in-fallback",
41283
+ id: "secret-in-fallback",
41284
+ source: "react-doctor",
41285
+ originallyExternal: false,
41286
+ rule: {
41287
+ ...secretInFallback,
41288
+ framework: "global",
41289
+ category: "Security",
41290
+ tags: [...new Set(["security-scan", ...secretInFallback.tags ?? []])]
41291
+ }
41292
+ },
40347
41293
  {
40348
41294
  key: "react-doctor/self-closing-comp",
40349
41295
  id: "self-closing-comp",
@@ -40500,6 +41446,18 @@ const reactDoctorRules = [
40500
41446
  tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
40501
41447
  }
40502
41448
  },
41449
+ {
41450
+ key: "react-doctor/supabase-table-missing-rls",
41451
+ id: "supabase-table-missing-rls",
41452
+ source: "react-doctor",
41453
+ originallyExternal: false,
41454
+ rule: {
41455
+ ...supabaseTableMissingRls,
41456
+ framework: "global",
41457
+ category: "Security",
41458
+ tags: [...new Set(["security-scan", ...supabaseTableMissingRls.tags ?? []])]
41459
+ }
41460
+ },
40503
41461
  {
40504
41462
  key: "react-doctor/svg-filter-clickjacking-risk",
40505
41463
  id: "svg-filter-clickjacking-risk",
@@ -40690,6 +41648,18 @@ const reactDoctorRules = [
40690
41648
  tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
40691
41649
  }
40692
41650
  },
41651
+ {
41652
+ key: "react-doctor/unsafe-json-in-html",
41653
+ id: "unsafe-json-in-html",
41654
+ source: "react-doctor",
41655
+ originallyExternal: false,
41656
+ rule: {
41657
+ ...unsafeJsonInHtml,
41658
+ framework: "global",
41659
+ category: "Security",
41660
+ tags: [...new Set(["security-scan", ...unsafeJsonInHtml.tags ?? []])]
41661
+ }
41662
+ },
40693
41663
  {
40694
41664
  key: "react-doctor/untrusted-redirect-following",
40695
41665
  id: "untrusted-redirect-following",