oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.17389ba

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 +420 -0
  2. package/dist/index.js +781 -189
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
890
890
  });
891
891
  //#endregion
892
892
  //#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
893
- const stripCommentsPreservingPositions = (content) => {
893
+ const WHITESPACE_PATTERN = /\s/;
894
+ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
895
+ for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
896
+ const character = content[cursor];
897
+ if (character === "\\") {
898
+ cursor += 1;
899
+ continue;
900
+ }
901
+ if (character === delimiter) return false;
902
+ if (WHITESPACE_PATTERN.test(character)) return true;
903
+ }
904
+ return false;
905
+ };
906
+ const blankNonCodePreservingPositions = (content, blankStringContents) => {
894
907
  const characters = content.split("");
895
908
  let stringDelimiter = null;
909
+ let isBlankingString = false;
910
+ const templateExpressionDepths = [];
896
911
  let index = 0;
912
+ const blankUnlessNewline = (offset) => {
913
+ if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
914
+ };
897
915
  while (index < content.length) {
898
916
  const character = content[index];
899
917
  const nextCharacter = content[index + 1];
900
918
  if (stringDelimiter !== null) {
901
919
  if (character === "\\") {
920
+ if (isBlankingString) {
921
+ blankUnlessNewline(index);
922
+ blankUnlessNewline(index + 1);
923
+ }
902
924
  index += 2;
903
925
  continue;
904
926
  }
905
- if (character === stringDelimiter) stringDelimiter = null;
927
+ if (character === stringDelimiter) {
928
+ stringDelimiter = null;
929
+ index += 1;
930
+ continue;
931
+ }
932
+ if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
933
+ templateExpressionDepths.push(0);
934
+ stringDelimiter = null;
935
+ index += 2;
936
+ continue;
937
+ }
938
+ if (isBlankingString) blankUnlessNewline(index);
906
939
  index += 1;
907
940
  continue;
908
941
  }
909
- if (character === "\"" || character === "'" || character === "`") {
942
+ if (character === "\"" || character === "'") {
910
943
  stringDelimiter = character;
944
+ isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
945
+ index += 1;
946
+ continue;
947
+ }
948
+ if (character === "`") {
949
+ stringDelimiter = "`";
950
+ isBlankingString = blankStringContents;
911
951
  index += 1;
912
952
  continue;
913
953
  }
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
926
966
  index += 2;
927
967
  break;
928
968
  }
929
- if (content[index] !== "\n") characters[index] = " ";
969
+ blankUnlessNewline(index);
930
970
  index += 1;
931
971
  }
932
972
  continue;
933
973
  }
974
+ if (templateExpressionDepths.length > 0) {
975
+ const innermost = templateExpressionDepths.length - 1;
976
+ if (character === "{") templateExpressionDepths[innermost] += 1;
977
+ else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
978
+ templateExpressionDepths.pop();
979
+ stringDelimiter = "`";
980
+ isBlankingString = blankStringContents;
981
+ } else templateExpressionDepths[innermost] -= 1;
982
+ }
934
983
  index += 1;
935
984
  }
936
985
  return characters.join("");
937
986
  };
987
+ const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
988
+ const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
938
989
  //#endregion
939
990
  //#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
940
991
  const strippedContentCache = /* @__PURE__ */ new WeakMap();
941
- const getScannableContent = (file) => {
992
+ const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
993
+ const getScannableContent = (file, ignoreStringLiterals = false) => {
942
994
  if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
943
- const cachedContent = strippedContentCache.get(file);
995
+ const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
996
+ const cachedContent = cache.get(file);
944
997
  if (cachedContent !== void 0) return cachedContent;
945
- const strippedContent = stripCommentsPreservingPositions(file.content);
946
- strippedContentCache.set(file, strippedContent);
998
+ const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
999
+ cache.set(file, strippedContent);
947
1000
  return strippedContent;
948
1001
  };
949
- const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
1002
+ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
950
1003
  if (!shouldScan(file)) return [];
951
- const content = getScannableContent(file);
1004
+ const content = getScannableContent(file, ignoreStringLiterals);
952
1005
  if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
953
1006
  const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
954
1007
  if (matchedPattern === void 0) return [];
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
973
1026
  shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
974
1027
  pattern: AGENT_TOOL_DEFINITION_PATTERN,
975
1028
  requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
1029
+ ignoreStringLiterals: true,
976
1030
  message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
977
1031
  })
978
1032
  });
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
1861
1915
  });
1862
1916
  //#endregion
1863
1917
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1864
- const MESSAGE$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1918
+ const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1865
1919
  const anchorHasContent = defineRule({
1866
1920
  id: "anchor-has-content",
1867
1921
  title: "Anchor has no content",
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
1877
1931
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1878
1932
  context.report({
1879
1933
  node: opening.name,
1880
- message: MESSAGE$51
1934
+ message: MESSAGE$59
1881
1935
  });
1882
1936
  } })
1883
1937
  });
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
2271
2325
  };
2272
2326
  //#endregion
2273
2327
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2274
- const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2328
+ const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2275
2329
  const ariaActivedescendantHasTabindex = defineRule({
2276
2330
  id: "aria-activedescendant-has-tabindex",
2277
2331
  title: "aria-activedescendant missing tabindex",
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2289
2343
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2290
2344
  context.report({
2291
2345
  node: node.name,
2292
- message: MESSAGE$50
2346
+ message: MESSAGE$58
2293
2347
  });
2294
2348
  return;
2295
2349
  }
2296
2350
  if (isInteractiveElement(tag, node)) return;
2297
2351
  context.report({
2298
2352
  node: node.name,
2299
- message: MESSAGE$50
2353
+ message: MESSAGE$58
2300
2354
  });
2301
2355
  } })
2302
2356
  });
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
3085
3139
  scan: scanByPattern({
3086
3140
  shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
3087
3141
  pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
3088
- requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
3142
+ requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
3089
3143
  message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
3090
3144
  })
3091
3145
  });
@@ -3104,6 +3158,76 @@ const AUTH_FUNCTION_NAMES = new Set([
3104
3158
  "getAuth",
3105
3159
  "validateSession"
3106
3160
  ]);
3161
+ const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
3162
+ const AUTH_STANDALONE_NOUN_TOKENS = new Set([
3163
+ "signedin",
3164
+ "loggedin",
3165
+ "signin"
3166
+ ]);
3167
+ const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
3168
+ "require",
3169
+ "ensure",
3170
+ "assert",
3171
+ "verify",
3172
+ "validate",
3173
+ "check",
3174
+ "protect",
3175
+ "enforce",
3176
+ "guard",
3177
+ "gate",
3178
+ "restrict",
3179
+ "is",
3180
+ "has",
3181
+ "can",
3182
+ "must"
3183
+ ]);
3184
+ const AUTH_GETTER_VERB_TOKENS = new Set([
3185
+ "get",
3186
+ "fetch",
3187
+ "load",
3188
+ "read",
3189
+ "resolve",
3190
+ "retrieve",
3191
+ "use"
3192
+ ]);
3193
+ const AUTH_QUALIFIER_TOKENS = new Set([
3194
+ "current",
3195
+ "my",
3196
+ "own"
3197
+ ]);
3198
+ const AUTH_STRONG_NOUN_TOKENS = new Set([
3199
+ "session",
3200
+ "sessions",
3201
+ "login",
3202
+ "admin",
3203
+ "admins",
3204
+ "superadmin",
3205
+ "superuser",
3206
+ "role",
3207
+ "roles",
3208
+ "permission",
3209
+ "permissions",
3210
+ "jwt",
3211
+ "identity",
3212
+ "principal",
3213
+ "credential",
3214
+ "credentials"
3215
+ ]);
3216
+ const AUTH_WEAK_NOUN_TOKENS = new Set([
3217
+ "user",
3218
+ "users",
3219
+ "account",
3220
+ "accounts",
3221
+ "token",
3222
+ "tokens",
3223
+ "access",
3224
+ "me",
3225
+ "viewer",
3226
+ "caller",
3227
+ "subject",
3228
+ "scope",
3229
+ "scopes"
3230
+ ]);
3107
3231
  const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
3108
3232
  const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
3109
3233
  const SECRET_PATTERNS = [
@@ -4201,6 +4325,58 @@ const asyncParallel = defineRule({
4201
4325
  }
4202
4326
  });
4203
4327
  //#endregion
4328
+ //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4329
+ const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4330
+ const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4331
+ const STORAGE_GLOBALS = new Set([
4332
+ "window",
4333
+ "globalThis",
4334
+ "self"
4335
+ ]);
4336
+ const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
4337
+ const isWebStorageObject = (node) => {
4338
+ if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
4339
+ if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
4340
+ return false;
4341
+ };
4342
+ const staticMemberName = (member) => {
4343
+ if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
4344
+ if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
4345
+ return null;
4346
+ };
4347
+ const authTokenInWebStorage = defineRule({
4348
+ id: "auth-token-in-web-storage",
4349
+ title: "Auth token in web storage",
4350
+ severity: "warn",
4351
+ recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
4352
+ create: (context) => ({
4353
+ CallExpression(node) {
4354
+ const callee = node.callee;
4355
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
4356
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
4357
+ if (!isWebStorageObject(callee.object)) return;
4358
+ const keyArgument = node.arguments?.[0];
4359
+ if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
4360
+ if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4361
+ context.report({
4362
+ node,
4363
+ message: MESSAGE$57
4364
+ });
4365
+ },
4366
+ AssignmentExpression(node) {
4367
+ const target = node.left;
4368
+ if (!isNodeOfType(target, "MemberExpression")) return;
4369
+ if (!isWebStorageObject(target.object)) return;
4370
+ const propertyName = staticMemberName(target);
4371
+ if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4372
+ context.report({
4373
+ node: target,
4374
+ message: MESSAGE$57
4375
+ });
4376
+ }
4377
+ })
4378
+ });
4379
+ //#endregion
4204
4380
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
4205
4381
  const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
4206
4382
  const AUTOFILL_TOKENS = new Set([
@@ -4572,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
4572
4748
  //#endregion
4573
4749
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4574
4750
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4575
- const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4751
+ const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4576
4752
  const KEY_HANDLERS = [
4577
4753
  "onKeyUp",
4578
4754
  "onKeyDown",
@@ -4604,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
4604
4780
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4605
4781
  context.report({
4606
4782
  node: node.name,
4607
- message: MESSAGE$49
4783
+ message: MESSAGE$56
4608
4784
  });
4609
4785
  } };
4610
4786
  }
@@ -4719,7 +4895,7 @@ const isReactComponentName = (name) => {
4719
4895
  };
4720
4896
  //#endregion
4721
4897
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4722
- const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4898
+ const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4723
4899
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4724
4900
  const DEFAULT_LABELLING_PROPS = [
4725
4901
  "alt",
@@ -4880,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
4880
5056
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4881
5057
  context.report({
4882
5058
  node: opening,
4883
- message: MESSAGE$48
5059
+ message: MESSAGE$55
4884
5060
  });
4885
5061
  } };
4886
5062
  }
@@ -5306,6 +5482,38 @@ const noVagueButtonLabel = defineRule({
5306
5482
  } })
5307
5483
  });
5308
5484
  //#endregion
5485
+ //#region src/plugin/utils/has-jsx-spread-attribute.ts
5486
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5487
+ //#endregion
5488
+ //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5489
+ const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5490
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5491
+ const NAME_PROVIDING_ATTRIBUTES = [
5492
+ "aria-label",
5493
+ "aria-labelledby",
5494
+ "title"
5495
+ ];
5496
+ const dialogHasAccessibleName = defineRule({
5497
+ id: "dialog-has-accessible-name",
5498
+ title: "Dialog without accessible name",
5499
+ severity: "warn",
5500
+ recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
5501
+ create: (context) => ({ JSXOpeningElement(node) {
5502
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
5503
+ const tagName = node.name.name;
5504
+ if (tagName[0] !== tagName[0]?.toLowerCase()) return;
5505
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5506
+ const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5507
+ if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5508
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5509
+ if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5510
+ context.report({
5511
+ node: node.name,
5512
+ message: MESSAGE$54
5513
+ });
5514
+ } })
5515
+ });
5516
+ //#endregion
5309
5517
  //#region src/plugin/utils/is-es5-component.ts
5310
5518
  const PRAGMA$2 = "React";
5311
5519
  const CREATE_CLASS = "createReactClass";
@@ -5340,7 +5548,7 @@ const isEs6Component = (node) => {
5340
5548
  };
5341
5549
  //#endregion
5342
5550
  //#region src/plugin/rules/react-builtins/display-name.ts
5343
- const MESSAGE$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5551
+ const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5344
5552
  const DEFAULT_ADDITIONAL_HOCS = [
5345
5553
  "observer",
5346
5554
  "lazy",
@@ -5543,7 +5751,7 @@ const displayName = defineRule({
5543
5751
  const reportAt = (node) => {
5544
5752
  context.report({
5545
5753
  node,
5546
- message: MESSAGE$47
5754
+ message: MESSAGE$53
5547
5755
  });
5548
5756
  };
5549
5757
  return {
@@ -7691,7 +7899,7 @@ const forbidElements = defineRule({
7691
7899
  });
7692
7900
  //#endregion
7693
7901
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7694
- const MESSAGE$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7902
+ const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7695
7903
  const forwardRefUsesRef = defineRule({
7696
7904
  id: "forward-ref-uses-ref",
7697
7905
  title: "forwardRef without ref parameter",
@@ -7711,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
7711
7919
  if (isNodeOfType(onlyParam, "RestElement")) return;
7712
7920
  context.report({
7713
7921
  node: inner,
7714
- message: MESSAGE$46
7922
+ message: MESSAGE$52
7715
7923
  });
7716
7924
  } })
7717
7925
  });
@@ -7748,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7748
7956
  });
7749
7957
  //#endregion
7750
7958
  //#region src/plugin/rules/a11y/heading-has-content.ts
7751
- const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7959
+ const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7752
7960
  const DEFAULT_HEADING_TAGS = [
7753
7961
  "h1",
7754
7962
  "h2",
@@ -7781,7 +7989,7 @@ const headingHasContent = defineRule({
7781
7989
  if (isHiddenFromScreenReader(node, context.settings)) return;
7782
7990
  context.report({
7783
7991
  node,
7784
- message: MESSAGE$45
7992
+ message: MESSAGE$51
7785
7993
  });
7786
7994
  } };
7787
7995
  }
@@ -7919,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
7919
8127
  });
7920
8128
  //#endregion
7921
8129
  //#region src/plugin/rules/a11y/html-has-lang.ts
7922
- const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8130
+ const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
7923
8131
  const resolveSettings$38 = (settings) => {
7924
8132
  const reactDoctor = settings?.["react-doctor"];
7925
8133
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -7967,7 +8175,7 @@ const htmlHasLang = defineRule({
7967
8175
  if (!lang) {
7968
8176
  context.report({
7969
8177
  node: node.name,
7970
- message: MESSAGE$44
8178
+ message: MESSAGE$50
7971
8179
  });
7972
8180
  return;
7973
8181
  }
@@ -7975,13 +8183,13 @@ const htmlHasLang = defineRule({
7975
8183
  if (verdict === "missing" || verdict === "empty") {
7976
8184
  context.report({
7977
8185
  node: lang,
7978
- message: MESSAGE$44
8186
+ message: MESSAGE$50
7979
8187
  });
7980
8188
  return;
7981
8189
  }
7982
8190
  if (hasSpread && !lang) context.report({
7983
8191
  node: node.name,
7984
- message: MESSAGE$44
8192
+ message: MESSAGE$50
7985
8193
  });
7986
8194
  } };
7987
8195
  }
@@ -8195,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
8195
8403
  });
8196
8404
  //#endregion
8197
8405
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8198
- const MESSAGE$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8406
+ const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8199
8407
  const evaluateTitleValue = (value) => {
8200
8408
  if (!value) return "missing";
8201
8409
  if (isNodeOfType(value, "Literal")) {
@@ -8235,14 +8443,14 @@ const iframeHasTitle = defineRule({
8235
8443
  if (!titleAttr) {
8236
8444
  if (hasSpread || tag === "iframe") context.report({
8237
8445
  node: node.name,
8238
- message: MESSAGE$43
8446
+ message: MESSAGE$49
8239
8447
  });
8240
8448
  return;
8241
8449
  }
8242
8450
  const verdict = evaluateTitleValue(titleAttr.value);
8243
8451
  if (verdict === "missing" || verdict === "empty") context.report({
8244
8452
  node: titleAttr,
8245
- message: MESSAGE$43
8453
+ message: MESSAGE$49
8246
8454
  });
8247
8455
  } })
8248
8456
  });
@@ -8346,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
8346
8554
  });
8347
8555
  //#endregion
8348
8556
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8349
- const MESSAGE$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8557
+ const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8350
8558
  const DEFAULT_COMPONENTS = ["img"];
8351
8559
  const DEFAULT_REDUNDANT_WORDS = [
8352
8560
  "image",
@@ -8411,7 +8619,7 @@ const imgRedundantAlt = defineRule({
8411
8619
  if (!altAttribute) return;
8412
8620
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8413
8621
  node: altAttribute,
8414
- message: MESSAGE$42
8622
+ message: MESSAGE$48
8415
8623
  });
8416
8624
  } };
8417
8625
  }
@@ -10768,7 +10976,7 @@ const jsxMaxDepth = defineRule({
10768
10976
  });
10769
10977
  //#endregion
10770
10978
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10771
- const MESSAGE$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10979
+ const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10772
10980
  const LITERAL_TEXT_TAGS = new Set([
10773
10981
  "code",
10774
10982
  "pre",
@@ -10804,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
10804
11012
  if (isInsideLiteralTextTag(node)) return;
10805
11013
  context.report({
10806
11014
  node,
10807
- message: MESSAGE$41
11015
+ message: MESSAGE$47
10808
11016
  });
10809
11017
  } })
10810
11018
  });
@@ -10835,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
10835
11043
  };
10836
11044
  //#endregion
10837
11045
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10838
- const MESSAGE$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
11046
+ const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10839
11047
  const CONTEXT_MODULES$1 = [
10840
11048
  "react",
10841
11049
  "use-context-selector",
@@ -10933,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
10933
11141
  if (!isConstructedValue(innerExpression)) continue;
10934
11142
  context.report({
10935
11143
  node: attribute,
10936
- message: MESSAGE$40
11144
+ message: MESSAGE$46
10937
11145
  });
10938
11146
  }
10939
11147
  }
@@ -11019,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11019
11227
  };
11020
11228
  //#endregion
11021
11229
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11022
- const MESSAGE$39 = "This child redraws every render because the prop gets brand new JSX each time.";
11230
+ const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11023
11231
  const KNOWN_SLOT_PROP_NAMES = new Set([
11024
11232
  "icon",
11025
11233
  "Icon",
@@ -11288,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
11288
11496
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11289
11497
  context.report({
11290
11498
  node,
11291
- message: MESSAGE$39
11499
+ message: MESSAGE$45
11292
11500
  });
11293
11501
  }
11294
11502
  };
@@ -11576,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11576
11784
  ];
11577
11785
  //#endregion
11578
11786
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11579
- const MESSAGE$38 = "This child redraws every render because the prop gets a brand new array each time.";
11787
+ const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11580
11788
  const isDataArrayPropName = (propName) => {
11581
11789
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11582
11790
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11660,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
11660
11868
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11661
11869
  context.report({
11662
11870
  node,
11663
- message: MESSAGE$38
11871
+ message: MESSAGE$44
11664
11872
  });
11665
11873
  }
11666
11874
  };
@@ -11918,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
11918
12126
  ]);
11919
12127
  //#endregion
11920
12128
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
11921
- const MESSAGE$37 = "This child redraws every render because the prop gets a brand new function each time.";
12129
+ const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
11922
12130
  const isAccessorPredicateName = (propName) => {
11923
12131
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
11924
12132
  if (propName.length <= prefix.length) continue;
@@ -12124,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12124
12332
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12125
12333
  context.report({
12126
12334
  node,
12127
- message: MESSAGE$37
12335
+ message: MESSAGE$43
12128
12336
  });
12129
12337
  }
12130
12338
  };
@@ -12344,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12344
12552
  ];
12345
12553
  //#endregion
12346
12554
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12347
- const MESSAGE$36 = "This child redraws every render because the prop gets a brand new object each time.";
12555
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12348
12556
  const isConfigObjectPropName = (propName) => {
12349
12557
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12350
12558
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12432,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
12432
12640
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12433
12641
  context.report({
12434
12642
  node,
12435
- message: MESSAGE$36
12643
+ message: MESSAGE$42
12436
12644
  });
12437
12645
  }
12438
12646
  };
@@ -12440,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
12440
12648
  });
12441
12649
  //#endregion
12442
12650
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12443
- const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12651
+ const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12444
12652
  const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
12445
12653
  const resolveSettings$28 = (settings) => {
12446
12654
  const reactDoctor = settings?.["react-doctor"];
@@ -12481,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
12481
12689
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12482
12690
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12483
12691
  node: attribute,
12484
- message: MESSAGE$35
12692
+ message: MESSAGE$41
12485
12693
  });
12486
12694
  }
12487
12695
  } };
@@ -12796,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12796
13004
  });
12797
13005
  //#endregion
12798
13006
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12799
- const MESSAGE$34 = "You can't tell what props reach this element when you spread them.";
13007
+ const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12800
13008
  const resolveSettings$25 = (settings) => {
12801
13009
  const reactDoctor = settings?.["react-doctor"];
12802
13010
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12837,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
12837
13045
  }
12838
13046
  context.report({
12839
13047
  node: attribute,
12840
- message: MESSAGE$34
13048
+ message: MESSAGE$40
12841
13049
  });
12842
13050
  }
12843
13051
  } };
@@ -13065,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
13065
13273
  });
13066
13274
  //#endregion
13067
13275
  //#region src/plugin/rules/a11y/lang.ts
13068
- const MESSAGE$33 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13276
+ const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13069
13277
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13070
13278
  "aa",
13071
13279
  "ab",
@@ -13277,7 +13485,7 @@ const lang = defineRule({
13277
13485
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13278
13486
  context.report({
13279
13487
  node: langAttr,
13280
- message: MESSAGE$33
13488
+ message: MESSAGE$39
13281
13489
  });
13282
13490
  return;
13283
13491
  }
@@ -13286,7 +13494,7 @@ const lang = defineRule({
13286
13494
  if (value === null) return;
13287
13495
  if (!isValidLangTag(value)) context.report({
13288
13496
  node: langAttr,
13289
- message: MESSAGE$33
13497
+ message: MESSAGE$39
13290
13498
  });
13291
13499
  } })
13292
13500
  });
@@ -13312,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
13312
13520
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13313
13521
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13314
13522
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13523
+ ignoreStringLiterals: true,
13315
13524
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13316
13525
  })
13317
13526
  });
@@ -13330,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
13330
13539
  });
13331
13540
  //#endregion
13332
13541
  //#region src/plugin/rules/a11y/media-has-caption.ts
13333
- const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13542
+ const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13334
13543
  const DEFAULT_AUDIO = ["audio"];
13335
13544
  const DEFAULT_VIDEO = ["video"];
13336
13545
  const DEFAULT_TRACK = ["track"];
@@ -13371,7 +13580,7 @@ const mediaHasCaption = defineRule({
13371
13580
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13372
13581
  context.report({
13373
13582
  node: node.name,
13374
- message: MESSAGE$32
13583
+ message: MESSAGE$38
13375
13584
  });
13376
13585
  return;
13377
13586
  }
@@ -13388,7 +13597,7 @@ const mediaHasCaption = defineRule({
13388
13597
  return kindValue.value.toLowerCase() === "captions";
13389
13598
  })) context.report({
13390
13599
  node: node.name,
13391
- message: MESSAGE$32
13600
+ message: MESSAGE$38
13392
13601
  });
13393
13602
  } };
13394
13603
  }
@@ -15189,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
15189
15398
  });
15190
15399
  //#endregion
15191
15400
  //#region src/plugin/rules/a11y/no-access-key.ts
15192
- const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15401
+ const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15193
15402
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15194
15403
  const noAccessKey = defineRule({
15195
15404
  id: "no-access-key",
@@ -15206,7 +15415,7 @@ const noAccessKey = defineRule({
15206
15415
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15207
15416
  context.report({
15208
15417
  node: accessKey,
15209
- message: MESSAGE$31
15418
+ message: MESSAGE$37
15210
15419
  });
15211
15420
  return;
15212
15421
  }
@@ -15216,7 +15425,7 @@ const noAccessKey = defineRule({
15216
15425
  if (isUndefinedIdentifier(expression)) return;
15217
15426
  context.report({
15218
15427
  node: accessKey,
15219
- message: MESSAGE$31
15428
+ message: MESSAGE$37
15220
15429
  });
15221
15430
  }
15222
15431
  } })
@@ -15699,7 +15908,7 @@ const noAdjustStateOnPropChange = defineRule({
15699
15908
  });
15700
15909
  //#endregion
15701
15910
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15702
- const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15911
+ const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15703
15912
  const noAriaHiddenOnFocusable = defineRule({
15704
15913
  id: "no-aria-hidden-on-focusable",
15705
15914
  title: "aria-hidden on focusable element",
@@ -15726,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
15726
15935
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15727
15936
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15728
15937
  node: ariaHidden,
15729
- message: MESSAGE$30
15938
+ message: MESSAGE$36
15730
15939
  });
15731
15940
  } })
15732
15941
  });
@@ -16094,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
16094
16303
  });
16095
16304
  //#endregion
16096
16305
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16097
- const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
16306
+ const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16098
16307
  const SECOND_INDEX_METHODS = new Set([
16099
16308
  "every",
16100
16309
  "filter",
@@ -16298,7 +16507,7 @@ const noArrayIndexKey = defineRule({
16298
16507
  }
16299
16508
  context.report({
16300
16509
  node: keyAttribute,
16301
- message: MESSAGE$29
16510
+ message: MESSAGE$35
16302
16511
  });
16303
16512
  },
16304
16513
  CallExpression(node) {
@@ -16318,15 +16527,35 @@ const noArrayIndexKey = defineRule({
16318
16527
  if (propName !== "key") continue;
16319
16528
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16320
16529
  node: property,
16321
- message: MESSAGE$29
16530
+ message: MESSAGE$35
16322
16531
  });
16323
16532
  }
16324
16533
  }
16325
16534
  })
16326
16535
  });
16327
16536
  //#endregion
16537
+ //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16538
+ const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16539
+ const noAsyncEffectCallback = defineRule({
16540
+ id: "no-async-effect-callback",
16541
+ title: "Async effect callback",
16542
+ severity: "warn",
16543
+ recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
16544
+ create: (context) => ({ CallExpression(node) {
16545
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
16546
+ const callback = getEffectCallback(node);
16547
+ if (!callback) return;
16548
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
16549
+ if (!callback.async) return;
16550
+ context.report({
16551
+ node: callback,
16552
+ message: MESSAGE$34
16553
+ });
16554
+ } })
16555
+ });
16556
+ //#endregion
16328
16557
  //#region src/plugin/rules/a11y/no-autofocus.ts
16329
- const MESSAGE$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16558
+ const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16330
16559
  const resolveSettings$21 = (settings) => {
16331
16560
  const reactDoctor = settings?.["react-doctor"];
16332
16561
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16382,7 +16611,7 @@ const noAutofocus = defineRule({
16382
16611
  }
16383
16612
  context.report({
16384
16613
  node: autoFocusAttribute,
16385
- message: MESSAGE$28
16614
+ message: MESSAGE$33
16386
16615
  });
16387
16616
  } };
16388
16617
  }
@@ -16632,6 +16861,109 @@ const noBarrelImport = defineRule({
16632
16861
  }
16633
16862
  });
16634
16863
  //#endregion
16864
+ //#region src/plugin/utils/function-contains-react-render-output.ts
16865
+ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
16866
+ "FunctionDeclaration",
16867
+ "FunctionExpression",
16868
+ "ArrowFunctionExpression",
16869
+ "ClassDeclaration",
16870
+ "ClassExpression"
16871
+ ]);
16872
+ const isReactImport$1 = (symbol) => {
16873
+ let importDeclaration = symbol.declarationNode?.parent;
16874
+ while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
16875
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
16876
+ return importDeclaration.source.value === "react";
16877
+ };
16878
+ const getImportedName = (symbol) => {
16879
+ if (symbol.kind !== "import") return null;
16880
+ if (!isReactImport$1(symbol)) return null;
16881
+ return getImportedName$1(symbol.declarationNode) ?? null;
16882
+ };
16883
+ const isReactNamespaceImport = (symbol) => {
16884
+ if (symbol.kind !== "import") return false;
16885
+ if (!isReactImport$1(symbol)) return false;
16886
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
16887
+ };
16888
+ const isReactCreateElementIdentifierCall = (callee, scopes) => {
16889
+ if (!isNodeOfType(callee, "Identifier")) return false;
16890
+ const symbol = scopes.symbolFor(callee);
16891
+ return Boolean(symbol && getImportedName(symbol) === "createElement");
16892
+ };
16893
+ const isReactCreateElementMemberCall = (callee, scopes) => {
16894
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
16895
+ if (callee.computed) return false;
16896
+ if (!isNodeOfType(callee.object, "Identifier")) return false;
16897
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
16898
+ if (callee.property.name !== "createElement") return false;
16899
+ const symbol = scopes.symbolFor(callee.object);
16900
+ return Boolean(symbol && isReactNamespaceImport(symbol));
16901
+ };
16902
+ const isReactCreateElementCall = (node, scopes) => {
16903
+ if (!isNodeOfType(node, "CallExpression")) return false;
16904
+ return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
16905
+ };
16906
+ const containsRenderOutput = (node, rootNode, scopes) => {
16907
+ if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
16908
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
16909
+ if (isReactCreateElementCall(node, scopes)) return true;
16910
+ const nodeRecord = node;
16911
+ for (const key of Object.keys(nodeRecord)) {
16912
+ if (key === "parent") continue;
16913
+ const child = nodeRecord[key];
16914
+ if (Array.isArray(child)) {
16915
+ for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
16916
+ } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
16917
+ }
16918
+ return false;
16919
+ };
16920
+ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
16921
+ //#endregion
16922
+ //#region src/plugin/utils/is-component-declaration.ts
16923
+ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
16924
+ //#endregion
16925
+ //#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
16926
+ const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
16927
+ const symbolIsLocalComponent = (symbol, context) => {
16928
+ const declaration = symbol.declarationNode;
16929
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
16930
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
16931
+ return false;
16932
+ };
16933
+ const noCallComponentAsFunction = defineRule({
16934
+ id: "no-call-component-as-function",
16935
+ title: "Component called as a function",
16936
+ severity: "warn",
16937
+ tags: ["test-noise"],
16938
+ recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
16939
+ create: (context) => {
16940
+ const renderedJsxNames = /* @__PURE__ */ new Set();
16941
+ const candidateCalls = [];
16942
+ return {
16943
+ JSXOpeningElement(node) {
16944
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
16945
+ },
16946
+ CallExpression(node) {
16947
+ if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
16948
+ node,
16949
+ callee: node.callee,
16950
+ name: node.callee.name
16951
+ });
16952
+ },
16953
+ "Program:exit"() {
16954
+ for (const candidate of candidateCalls) {
16955
+ const symbol = context.scopes.symbolFor(candidate.callee);
16956
+ if (!symbol) continue;
16957
+ if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
16958
+ node: candidate.node,
16959
+ message: message(candidate.name)
16960
+ });
16961
+ }
16962
+ }
16963
+ };
16964
+ }
16965
+ });
16966
+ //#endregion
16635
16967
  //#region src/plugin/utils/is-setter-identifier.ts
16636
16968
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
16637
16969
  //#endregion
@@ -16783,7 +17115,7 @@ const noChainStateUpdates = defineRule({
16783
17115
  });
16784
17116
  //#endregion
16785
17117
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
16786
- const MESSAGE$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17118
+ const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16787
17119
  const noChildrenProp = defineRule({
16788
17120
  id: "no-children-prop",
16789
17121
  title: "Children passed as a prop",
@@ -16795,7 +17127,7 @@ const noChildrenProp = defineRule({
16795
17127
  if (node.name.name !== "children") return;
16796
17128
  context.report({
16797
17129
  node: node.name,
16798
- message: MESSAGE$27
17130
+ message: MESSAGE$32
16799
17131
  });
16800
17132
  },
16801
17133
  CallExpression(node) {
@@ -16808,7 +17140,7 @@ const noChildrenProp = defineRule({
16808
17140
  const propertyKey = property.key;
16809
17141
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
16810
17142
  node: propertyKey,
16811
- message: MESSAGE$27
17143
+ message: MESSAGE$32
16812
17144
  });
16813
17145
  }
16814
17146
  }
@@ -16816,7 +17148,7 @@ const noChildrenProp = defineRule({
16816
17148
  });
16817
17149
  //#endregion
16818
17150
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
16819
- const MESSAGE$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17151
+ const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
16820
17152
  const noCloneElement = defineRule({
16821
17153
  id: "no-clone-element",
16822
17154
  title: "cloneElement makes child props fragile",
@@ -16829,7 +17161,7 @@ const noCloneElement = defineRule({
16829
17161
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
16830
17162
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
16831
17163
  node: callee,
16832
- message: MESSAGE$26
17164
+ message: MESSAGE$31
16833
17165
  });
16834
17166
  return;
16835
17167
  }
@@ -16842,7 +17174,7 @@ const noCloneElement = defineRule({
16842
17174
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
16843
17175
  context.report({
16844
17176
  node: callee,
16845
- message: MESSAGE$26
17177
+ message: MESSAGE$31
16846
17178
  });
16847
17179
  }
16848
17180
  } })
@@ -16891,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
16891
17223
  };
16892
17224
  //#endregion
16893
17225
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
16894
- const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17226
+ const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
16895
17227
  const CONTEXT_MODULES = [
16896
17228
  "react",
16897
17229
  "use-context-selector",
@@ -16927,7 +17259,32 @@ const noCreateContextInRender = defineRule({
16927
17259
  if (!componentOrHookName) return;
16928
17260
  context.report({
16929
17261
  node,
16930
- message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
17262
+ message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17263
+ });
17264
+ } })
17265
+ });
17266
+ //#endregion
17267
+ //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17268
+ const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17269
+ const noCreateRefInFunctionComponent = defineRule({
17270
+ id: "no-create-ref-in-function-component",
17271
+ title: "createRef in function component",
17272
+ severity: "warn",
17273
+ recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
17274
+ create: (context) => ({ CallExpression(node) {
17275
+ if (!isReactFunctionCall(node, "createRef")) return;
17276
+ if (isNodeOfType(node.callee, "Identifier")) {
17277
+ const symbol = context.scopes.symbolFor(node.callee);
17278
+ if (symbol && symbol.kind !== "import") return;
17279
+ }
17280
+ const enclosingFunction = nearestEnclosingFunction(node);
17281
+ if (!enclosingFunction) return;
17282
+ const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
17283
+ if (!displayName) return;
17284
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17285
+ context.report({
17286
+ node,
17287
+ message: MESSAGE$29
16931
17288
  });
16932
17289
  } })
16933
17290
  });
@@ -17067,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
17067
17424
  });
17068
17425
  //#endregion
17069
17426
  //#region src/plugin/rules/react-builtins/no-danger.ts
17070
- const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17427
+ const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17071
17428
  const noDanger = defineRule({
17072
17429
  id: "no-danger",
17073
17430
  title: "Raw HTML injection can run unsafe markup",
@@ -17080,7 +17437,7 @@ const noDanger = defineRule({
17080
17437
  if (!propAttribute) return;
17081
17438
  context.report({
17082
17439
  node: propAttribute.name,
17083
- message: MESSAGE$24
17440
+ message: MESSAGE$28
17084
17441
  });
17085
17442
  },
17086
17443
  CallExpression(node) {
@@ -17092,7 +17449,7 @@ const noDanger = defineRule({
17092
17449
  const propertyKey = property.key;
17093
17450
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17094
17451
  node: propertyKey,
17095
- message: MESSAGE$24
17452
+ message: MESSAGE$28
17096
17453
  });
17097
17454
  }
17098
17455
  }
@@ -17100,7 +17457,7 @@ const noDanger = defineRule({
17100
17457
  });
17101
17458
  //#endregion
17102
17459
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17103
- const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17460
+ const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17104
17461
  const isLineBreak = (child) => {
17105
17462
  if (!isNodeOfType(child, "JSXText")) return false;
17106
17463
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17170,7 +17527,7 @@ const noDangerWithChildren = defineRule({
17170
17527
  if (!hasChildrenProp && !hasNestedChildren) return;
17171
17528
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17172
17529
  node: opening,
17173
- message: MESSAGE$23
17530
+ message: MESSAGE$27
17174
17531
  });
17175
17532
  },
17176
17533
  CallExpression(node) {
@@ -17182,7 +17539,7 @@ const noDangerWithChildren = defineRule({
17182
17539
  if (!propsShape.hasDangerously) return;
17183
17540
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17184
17541
  node,
17185
- message: MESSAGE$23
17542
+ message: MESSAGE$27
17186
17543
  });
17187
17544
  }
17188
17545
  })
@@ -17759,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17759
18116
  //#endregion
17760
18117
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17761
18118
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
17762
- const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18119
+ const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17763
18120
  const resolveSettings$20 = (settings) => {
17764
18121
  const reactDoctor = settings?.["react-doctor"];
17765
18122
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -17778,7 +18135,7 @@ const noDidMountSetState = defineRule({
17778
18135
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17779
18136
  context.report({
17780
18137
  node: node.callee,
17781
- message: MESSAGE$22
18138
+ message: MESSAGE$26
17782
18139
  });
17783
18140
  } };
17784
18141
  }
@@ -17786,7 +18143,7 @@ const noDidMountSetState = defineRule({
17786
18143
  //#endregion
17787
18144
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
17788
18145
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
17789
- const MESSAGE$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18146
+ const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
17790
18147
  const resolveSettings$19 = (settings) => {
17791
18148
  const reactDoctor = settings?.["react-doctor"];
17792
18149
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -17805,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
17805
18162
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17806
18163
  context.report({
17807
18164
  node: node.callee,
17808
- message: MESSAGE$21
18165
+ message: MESSAGE$25
17809
18166
  });
17810
18167
  } };
17811
18168
  }
@@ -17828,7 +18185,7 @@ const isStateMemberExpression = (node) => {
17828
18185
  };
17829
18186
  //#endregion
17830
18187
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
17831
- const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18188
+ const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
17832
18189
  const shouldIgnoreMutation = (node) => {
17833
18190
  let isConstructor = false;
17834
18191
  let isInsideCallExpression = false;
@@ -17850,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
17850
18207
  if (shouldIgnoreMutation(reportNode)) return;
17851
18208
  context.report({
17852
18209
  node: reportNode,
17853
- message: MESSAGE$20
18210
+ message: MESSAGE$24
17854
18211
  });
17855
18212
  };
17856
18213
  const noDirectMutationState = defineRule({
@@ -18060,6 +18417,26 @@ const noDocumentStartViewTransition = defineRule({
18060
18417
  } })
18061
18418
  });
18062
18419
  //#endregion
18420
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18421
+ const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18422
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18423
+ const noDocumentWrite = defineRule({
18424
+ id: "no-document-write",
18425
+ title: "document.write/writeln",
18426
+ severity: "warn",
18427
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18428
+ create: (context) => ({ CallExpression(node) {
18429
+ const callee = node.callee;
18430
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18431
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18432
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18433
+ context.report({
18434
+ node,
18435
+ message: MESSAGE$23
18436
+ });
18437
+ } })
18438
+ });
18439
+ //#endregion
18063
18440
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18064
18441
  const noDynamicImportPath = defineRule({
18065
18442
  id: "no-dynamic-import-path",
@@ -19438,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
19438
19815
  "ReactDOM",
19439
19816
  "ReactDom"
19440
19817
  ]);
19441
- const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19818
+ const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19442
19819
  const noFindDomNode = defineRule({
19443
19820
  id: "no-find-dom-node",
19444
19821
  title: "findDOMNode breaks component encapsulation",
@@ -19449,7 +19826,7 @@ const noFindDomNode = defineRule({
19449
19826
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19450
19827
  context.report({
19451
19828
  node: callee,
19452
- message: MESSAGE$19
19829
+ message: MESSAGE$22
19453
19830
  });
19454
19831
  return;
19455
19832
  }
@@ -19460,7 +19837,7 @@ const noFindDomNode = defineRule({
19460
19837
  if (callee.property.name !== "findDOMNode") return;
19461
19838
  context.report({
19462
19839
  node: callee.property,
19463
- message: MESSAGE$19
19840
+ message: MESSAGE$22
19464
19841
  });
19465
19842
  }
19466
19843
  } })
@@ -19523,64 +19900,6 @@ const noGenericHandlerNames = defineRule({
19523
19900
  } })
19524
19901
  });
19525
19902
  //#endregion
19526
- //#region src/plugin/utils/function-contains-react-render-output.ts
19527
- const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
19528
- "FunctionDeclaration",
19529
- "FunctionExpression",
19530
- "ArrowFunctionExpression",
19531
- "ClassDeclaration",
19532
- "ClassExpression"
19533
- ]);
19534
- const isReactImport$1 = (symbol) => {
19535
- let importDeclaration = symbol.declarationNode?.parent;
19536
- while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
19537
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
19538
- return importDeclaration.source.value === "react";
19539
- };
19540
- const getImportedName = (symbol) => {
19541
- if (symbol.kind !== "import") return null;
19542
- if (!isReactImport$1(symbol)) return null;
19543
- return getImportedName$1(symbol.declarationNode) ?? null;
19544
- };
19545
- const isReactNamespaceImport = (symbol) => {
19546
- if (symbol.kind !== "import") return false;
19547
- if (!isReactImport$1(symbol)) return false;
19548
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
19549
- };
19550
- const isReactCreateElementIdentifierCall = (callee, scopes) => {
19551
- if (!isNodeOfType(callee, "Identifier")) return false;
19552
- const symbol = scopes.symbolFor(callee);
19553
- return Boolean(symbol && getImportedName(symbol) === "createElement");
19554
- };
19555
- const isReactCreateElementMemberCall = (callee, scopes) => {
19556
- if (!isNodeOfType(callee, "MemberExpression")) return false;
19557
- if (callee.computed) return false;
19558
- if (!isNodeOfType(callee.object, "Identifier")) return false;
19559
- if (!isNodeOfType(callee.property, "Identifier")) return false;
19560
- if (callee.property.name !== "createElement") return false;
19561
- const symbol = scopes.symbolFor(callee.object);
19562
- return Boolean(symbol && isReactNamespaceImport(symbol));
19563
- };
19564
- const isReactCreateElementCall = (node, scopes) => {
19565
- if (!isNodeOfType(node, "CallExpression")) return false;
19566
- return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
19567
- };
19568
- const containsRenderOutput = (node, rootNode, scopes) => {
19569
- if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
19570
- if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
19571
- if (isReactCreateElementCall(node, scopes)) return true;
19572
- const nodeRecord = node;
19573
- for (const key of Object.keys(nodeRecord)) {
19574
- if (key === "parent") continue;
19575
- const child = nodeRecord[key];
19576
- if (Array.isArray(child)) {
19577
- for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
19578
- } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
19579
- }
19580
- return false;
19581
- };
19582
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
19583
- //#endregion
19584
19903
  //#region src/plugin/rules/architecture/no-giant-component.ts
19585
19904
  const noGiantComponent = defineRule({
19586
19905
  id: "no-giant-component",
@@ -19759,6 +20078,26 @@ const noGrayOnColoredBackground = defineRule({
19759
20078
  } })
19760
20079
  });
19761
20080
  //#endregion
20081
+ //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20082
+ const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20083
+ const noImgLazyWithHighFetchpriority = defineRule({
20084
+ id: "no-img-lazy-with-high-fetchpriority",
20085
+ title: "Lazy image with high fetchPriority",
20086
+ severity: "warn",
20087
+ recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
20088
+ create: (context) => ({ JSXOpeningElement(node) {
20089
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
20090
+ const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
20091
+ if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
20092
+ const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
20093
+ if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20094
+ context.report({
20095
+ node: node.name,
20096
+ message: MESSAGE$21
20097
+ });
20098
+ } })
20099
+ });
20100
+ //#endregion
19762
20101
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
19763
20102
  const noInitializeState = defineRule({
19764
20103
  id: "no-initialize-state",
@@ -19988,8 +20327,31 @@ const noIsMounted = defineRule({
19988
20327
  } })
19989
20328
  });
19990
20329
  //#endregion
20330
+ //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20331
+ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20332
+ const isJsonMethodCall = (node, method) => {
20333
+ if (!isNodeOfType(node, "CallExpression")) return false;
20334
+ const callee = node.callee;
20335
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
20336
+ };
20337
+ const noJsonParseStringifyClone = defineRule({
20338
+ id: "no-json-parse-stringify-clone",
20339
+ title: "JSON parse/stringify deep clone",
20340
+ severity: "warn",
20341
+ recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
20342
+ create: (context) => ({ CallExpression(node) {
20343
+ if (!isJsonMethodCall(node, "parse")) return;
20344
+ const firstArgument = node.arguments?.[0];
20345
+ if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20346
+ context.report({
20347
+ node,
20348
+ message: MESSAGE$20
20349
+ });
20350
+ } })
20351
+ });
20352
+ //#endregion
19991
20353
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
19992
- const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20354
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
19993
20355
  const isJsxElementTypeReference = (node) => {
19994
20356
  if (!isNodeOfType(node, "TSTypeReference")) return false;
19995
20357
  const typeName = node.typeName;
@@ -20006,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
20006
20368
  if (!typeAnnotation) return;
20007
20369
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20008
20370
  node: typeAnnotation,
20009
- message: MESSAGE$18
20371
+ message: MESSAGE$19
20010
20372
  });
20011
20373
  };
20012
20374
  const noJsxElementType = defineRule({
@@ -20310,9 +20672,6 @@ const noLongTransitionDuration = defineRule({
20310
20672
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20311
20673
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
20312
20674
  //#endregion
20313
- //#region src/plugin/utils/is-component-declaration.ts
20314
- const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
20315
- //#endregion
20316
20675
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
20317
20676
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
20318
20677
  const found = /* @__PURE__ */ new Set();
@@ -20464,7 +20823,7 @@ const noMoment = defineRule({
20464
20823
  });
20465
20824
  //#endregion
20466
20825
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20467
- const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
20826
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
20468
20827
  const resolveSettings$16 = (settings) => {
20469
20828
  const reactDoctor = settings?.["react-doctor"];
20470
20829
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -20786,7 +21145,7 @@ const noMultiComp = defineRule({
20786
21145
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
20787
21146
  for (const component of flagged.slice(1)) context.report({
20788
21147
  node: component.reportNode,
20789
- message: MESSAGE$17
21148
+ message: MESSAGE$18
20790
21149
  });
20791
21150
  } };
20792
21151
  }
@@ -20954,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
20954
21313
  };
20955
21314
  //#endregion
20956
21315
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
20957
- const MESSAGE$16 = "This reducer changes state in place, so your update is silently skipped.";
21316
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
20958
21317
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
20959
21318
  "copyWithin",
20960
21319
  "fill",
@@ -21164,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21164
21523
  reportedNodes.add(options.crossFileConsumerCallSite);
21165
21524
  context.report({
21166
21525
  node: options.crossFileConsumerCallSite,
21167
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21526
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21168
21527
  });
21169
21528
  return;
21170
21529
  }
@@ -21173,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21173
21532
  reportedNodes.add(mutation.node);
21174
21533
  context.report({
21175
21534
  node: mutation.node,
21176
- message: MESSAGE$16
21535
+ message: MESSAGE$17
21177
21536
  });
21178
21537
  }
21179
21538
  };
@@ -21445,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21445
21804
  });
21446
21805
  //#endregion
21447
21806
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21448
- const MESSAGE$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21807
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21449
21808
  const resolveSettings$14 = (settings) => {
21450
21809
  const reactDoctor = settings?.["react-doctor"];
21451
21810
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21473,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
21473
21832
  if (numeric === null) {
21474
21833
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21475
21834
  node: tabIndex,
21476
- message: MESSAGE$15
21835
+ message: MESSAGE$16
21477
21836
  });
21478
21837
  return;
21479
21838
  }
@@ -21486,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
21486
21845
  if (!roleAttribute) {
21487
21846
  context.report({
21488
21847
  node: tabIndex,
21489
- message: MESSAGE$15
21848
+ message: MESSAGE$16
21490
21849
  });
21491
21850
  return;
21492
21851
  }
@@ -21500,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
21500
21859
  }
21501
21860
  context.report({
21502
21861
  node: tabIndex,
21503
- message: MESSAGE$15
21862
+ message: MESSAGE$16
21504
21863
  });
21505
21864
  } };
21506
21865
  }
@@ -22191,7 +22550,7 @@ const noRandomKey = defineRule({
22191
22550
  });
22192
22551
  //#endregion
22193
22552
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22194
- const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22553
+ const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22195
22554
  const isChildrenIdentifier = (node, contextNode) => {
22196
22555
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22197
22556
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22217,13 +22576,13 @@ const noReactChildren = defineRule({
22217
22576
  if (isChildrenIdentifier(memberObject, node)) {
22218
22577
  context.report({
22219
22578
  node: calleeOuter,
22220
- message: MESSAGE$14
22579
+ message: MESSAGE$15
22221
22580
  });
22222
22581
  return;
22223
22582
  }
22224
22583
  if (isReactNamespaceMember(memberObject, node)) context.report({
22225
22584
  node: calleeOuter,
22226
- message: MESSAGE$14
22585
+ message: MESSAGE$15
22227
22586
  });
22228
22587
  } })
22229
22588
  });
@@ -22546,7 +22905,7 @@ const noRenderPropChildren = defineRule({
22546
22905
  });
22547
22906
  //#endregion
22548
22907
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22549
- const MESSAGE$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22908
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22550
22909
  const isReactDomRenderCall = (node) => {
22551
22910
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22552
22911
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22570,7 +22929,7 @@ const noRenderReturnValue = defineRule({
22570
22929
  if (!isUsedAsReturnValue(node.parent)) return;
22571
22930
  context.report({
22572
22931
  node: node.callee,
22573
- message: MESSAGE$13
22932
+ message: MESSAGE$14
22574
22933
  });
22575
22934
  } })
22576
22935
  });
@@ -22730,11 +23089,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
22730
23089
  return "unknown";
22731
23090
  };
22732
23091
  //#endregion
22733
- //#region src/plugin/utils/get-identifier-trailing-word.ts
22734
- const getIdentifierTrailingWord = (identifierName) => {
22735
- return identifierName.match(/[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g)?.at(-1)?.toLowerCase() ?? identifierName.toLowerCase();
23092
+ //#region src/plugin/utils/tokenize-identifier-words.ts
23093
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
23094
+ const tokenizeIdentifierWords = (identifierName) => {
23095
+ const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
23096
+ if (!words) return [];
23097
+ return words.map((word) => word.toLowerCase());
22736
23098
  };
22737
23099
  //#endregion
23100
+ //#region src/plugin/utils/get-identifier-trailing-word.ts
23101
+ const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
23102
+ //#endregion
22738
23103
  //#region src/plugin/constants/tanstack.ts
22739
23104
  const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
22740
23105
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
@@ -23262,7 +23627,7 @@ const getParentComponent = (node) => {
23262
23627
  };
23263
23628
  //#endregion
23264
23629
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23265
- const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23630
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23266
23631
  const noSetState = defineRule({
23267
23632
  id: "no-set-state",
23268
23633
  title: "Local class state forbidden",
@@ -23277,7 +23642,7 @@ const noSetState = defineRule({
23277
23642
  if (!getParentComponent(node)) return;
23278
23643
  context.report({
23279
23644
  node: node.callee,
23280
- message: MESSAGE$12
23645
+ message: MESSAGE$13
23281
23646
  });
23282
23647
  } })
23283
23648
  });
@@ -23439,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
23439
23804
  };
23440
23805
  //#endregion
23441
23806
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23442
- const MESSAGE$11 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23807
+ const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23443
23808
  const DEFAULT_HANDLERS = [
23444
23809
  "onClick",
23445
23810
  "onMouseDown",
@@ -23499,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
23499
23864
  if (!roleAttribute || !roleAttribute.value) {
23500
23865
  context.report({
23501
23866
  node: node.name,
23502
- message: MESSAGE$11
23867
+ message: MESSAGE$12
23503
23868
  });
23504
23869
  return;
23505
23870
  }
@@ -23509,19 +23874,66 @@ const noStaticElementInteractions = defineRule({
23509
23874
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23510
23875
  context.report({
23511
23876
  node: node.name,
23512
- message: MESSAGE$11
23877
+ message: MESSAGE$12
23513
23878
  });
23514
23879
  return;
23515
23880
  }
23516
23881
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23517
23882
  context.report({
23518
23883
  node: node.name,
23519
- message: MESSAGE$11
23884
+ message: MESSAGE$12
23520
23885
  });
23521
23886
  } };
23522
23887
  }
23523
23888
  });
23524
23889
  //#endregion
23890
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
23891
+ const BOOLEAN_ATTRIBUTES = new Set([
23892
+ "disabled",
23893
+ "checked",
23894
+ "readonly",
23895
+ "required",
23896
+ "selected",
23897
+ "multiple",
23898
+ "autofocus",
23899
+ "autoplay",
23900
+ "controls",
23901
+ "loop",
23902
+ "muted",
23903
+ "open",
23904
+ "reversed",
23905
+ "default",
23906
+ "novalidate",
23907
+ "formnovalidate",
23908
+ "playsinline",
23909
+ "itemscope",
23910
+ "allowfullscreen"
23911
+ ]);
23912
+ const noStringFalseOnBooleanAttribute = defineRule({
23913
+ id: "no-string-false-on-boolean-attribute",
23914
+ title: "String true/false on a boolean attribute",
23915
+ severity: "warn",
23916
+ recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
23917
+ create: (context) => ({ JSXOpeningElement(node) {
23918
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
23919
+ const firstCharacter = node.name.name.charCodeAt(0);
23920
+ if (firstCharacter < 97 || firstCharacter > 122) return;
23921
+ for (const attribute of node.attributes) {
23922
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
23923
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
23924
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
23925
+ const value = getJsxPropStringValue(attribute);
23926
+ if (value !== "false" && value !== "true") continue;
23927
+ const attributeName = attribute.name.name;
23928
+ const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
23929
+ context.report({
23930
+ node: attribute,
23931
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
23932
+ });
23933
+ }
23934
+ } })
23935
+ });
23936
+ //#endregion
23525
23937
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23526
23938
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23527
23939
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23572,6 +23984,27 @@ const noStringRefs = defineRule({
23572
23984
  }
23573
23985
  });
23574
23986
  //#endregion
23987
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
23988
+ const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
23989
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
23990
+ const noSyncXhr = defineRule({
23991
+ id: "no-sync-xhr",
23992
+ title: "Synchronous XMLHttpRequest",
23993
+ severity: "warn",
23994
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
23995
+ create: (context) => ({ CallExpression(node) {
23996
+ const callee = node.callee;
23997
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
23998
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
23999
+ const asyncArgument = node.arguments?.[2];
24000
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24001
+ context.report({
24002
+ node,
24003
+ message: MESSAGE$11
24004
+ });
24005
+ } })
24006
+ });
24007
+ //#endregion
23575
24008
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23576
24009
  const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
23577
24010
  const isInsideClassMethod = (node, customClassFactoryNames) => {
@@ -34681,6 +35114,47 @@ const serverAfterNonblocking = defineRule({
34681
35114
  }
34682
35115
  });
34683
35116
  //#endregion
35117
+ //#region src/plugin/utils/is-auth-guard-name.ts
35118
+ const SIGNED_IN_HEAD_TOKENS = new Set([
35119
+ "signed",
35120
+ "logged",
35121
+ "sign"
35122
+ ]);
35123
+ const mergeSignedInTokens = (tokens) => {
35124
+ const mergedTokens = [];
35125
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
35126
+ const currentToken = tokens[tokenIndex];
35127
+ if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
35128
+ mergedTokens.push(`${currentToken}in`);
35129
+ tokenIndex += 1;
35130
+ continue;
35131
+ }
35132
+ mergedTokens.push(currentToken);
35133
+ }
35134
+ return mergedTokens;
35135
+ };
35136
+ const isAuthGuardName = (calleeName) => {
35137
+ const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
35138
+ if (tokens.length === 0) return false;
35139
+ let hasAssertiveVerb = false;
35140
+ let hasGetterVerb = false;
35141
+ let hasQualifier = false;
35142
+ let hasStrongNoun = false;
35143
+ let hasWeakNoun = false;
35144
+ for (const token of tokens) {
35145
+ if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
35146
+ if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
35147
+ if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
35148
+ if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
35149
+ if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
35150
+ if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
35151
+ }
35152
+ if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
35153
+ if (hasGetterVerb && hasStrongNoun) return true;
35154
+ if (hasQualifier && hasWeakNoun) return true;
35155
+ return false;
35156
+ };
35157
+ //#endregion
34684
35158
  //#region src/plugin/rules/server/server-auth-actions.ts
34685
35159
  const isAsyncFunctionLikeNode = (node) => {
34686
35160
  if (!node) return false;
@@ -34723,9 +35197,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
34723
35197
  const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
34724
35198
  const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
34725
35199
  if (!calleeNode) return null;
34726
- if (isNodeOfType(calleeNode, "Identifier")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
35200
+ if (isNodeOfType(calleeNode, "Identifier")) {
35201
+ const calleeName = calleeNode.name;
35202
+ return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
35203
+ }
34727
35204
  if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
34728
35205
  const methodName = calleeNode.property.name;
35206
+ if (isAuthGuardName(methodName)) return methodName;
34729
35207
  if (!allowedFunctionNames.has(methodName)) return null;
34730
35208
  if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
34731
35209
  return methodName;
@@ -35102,13 +35580,7 @@ const serverNoMutableModuleState = defineRule({
35102
35580
  const collectDeclaredNames = (declaration) => {
35103
35581
  const names = /* @__PURE__ */ new Set();
35104
35582
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35105
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35106
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35107
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35108
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35109
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35110
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35111
- }
35583
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35112
35584
  return names;
35113
35585
  };
35114
35586
  const declarationStartsWithAwait = (declaration) => {
@@ -35118,11 +35590,15 @@ const declarationStartsWithAwait = (declaration) => {
35118
35590
  };
35119
35591
  const declarationReadsAnyName = (declaration, names) => {
35120
35592
  if (names.size === 0) return false;
35593
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35121
35594
  let didRead = false;
35122
- walkAst(declaration, (child) => {
35123
- if (didRead) return;
35124
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35125
- });
35595
+ for (const declarator of declaration.declarations ?? []) {
35596
+ if (!declarator.init) continue;
35597
+ walkAst(declarator.init, (child) => {
35598
+ if (didRead) return;
35599
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35600
+ });
35601
+ }
35126
35602
  return didRead;
35127
35603
  };
35128
35604
  const serverSequentialIndependentAwait = defineRule({
@@ -36382,7 +36858,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36382
36858
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36383
36859
  scan: scanByPattern({
36384
36860
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36385
- pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36861
+ pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36386
36862
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36387
36863
  })
36388
36864
  });
@@ -37144,6 +37620,17 @@ const reactDoctorRules = [
37144
37620
  category: "Performance"
37145
37621
  }
37146
37622
  },
37623
+ {
37624
+ key: "react-doctor/auth-token-in-web-storage",
37625
+ id: "auth-token-in-web-storage",
37626
+ source: "react-doctor",
37627
+ originallyExternal: false,
37628
+ rule: {
37629
+ ...authTokenInWebStorage,
37630
+ framework: "global",
37631
+ category: "Security"
37632
+ }
37633
+ },
37147
37634
  {
37148
37635
  key: "react-doctor/autocomplete-valid",
37149
37636
  id: "autocomplete-valid",
@@ -37360,6 +37847,18 @@ const reactDoctorRules = [
37360
37847
  requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
37361
37848
  }
37362
37849
  },
37850
+ {
37851
+ key: "react-doctor/dialog-has-accessible-name",
37852
+ id: "dialog-has-accessible-name",
37853
+ source: "react-doctor",
37854
+ originallyExternal: false,
37855
+ rule: {
37856
+ ...dialogHasAccessibleName,
37857
+ framework: "global",
37858
+ category: "Accessibility",
37859
+ requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
37860
+ }
37861
+ },
37363
37862
  {
37364
37863
  key: "react-doctor/display-name",
37365
37864
  id: "display-name",
@@ -38519,6 +39018,18 @@ const reactDoctorRules = [
38519
39018
  requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
38520
39019
  }
38521
39020
  },
39021
+ {
39022
+ key: "react-doctor/no-async-effect-callback",
39023
+ id: "no-async-effect-callback",
39024
+ source: "react-doctor",
39025
+ originallyExternal: false,
39026
+ rule: {
39027
+ ...noAsyncEffectCallback,
39028
+ framework: "global",
39029
+ category: "Bugs",
39030
+ requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
39031
+ }
39032
+ },
38522
39033
  {
38523
39034
  key: "react-doctor/no-autofocus",
38524
39035
  id: "no-autofocus",
@@ -38542,6 +39053,18 @@ const reactDoctorRules = [
38542
39053
  category: "Performance"
38543
39054
  }
38544
39055
  },
39056
+ {
39057
+ key: "react-doctor/no-call-component-as-function",
39058
+ id: "no-call-component-as-function",
39059
+ source: "react-doctor",
39060
+ originallyExternal: false,
39061
+ rule: {
39062
+ ...noCallComponentAsFunction,
39063
+ framework: "global",
39064
+ category: "Bugs",
39065
+ requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
39066
+ }
39067
+ },
38545
39068
  {
38546
39069
  key: "react-doctor/no-cascading-set-state",
38547
39070
  id: "no-cascading-set-state",
@@ -38602,6 +39125,18 @@ const reactDoctorRules = [
38602
39125
  requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
38603
39126
  }
38604
39127
  },
39128
+ {
39129
+ key: "react-doctor/no-create-ref-in-function-component",
39130
+ id: "no-create-ref-in-function-component",
39131
+ source: "react-doctor",
39132
+ originallyExternal: false,
39133
+ rule: {
39134
+ ...noCreateRefInFunctionComponent,
39135
+ framework: "global",
39136
+ category: "Bugs",
39137
+ requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
39138
+ }
39139
+ },
38605
39140
  {
38606
39141
  key: "react-doctor/no-create-store-in-render",
38607
39142
  id: "no-create-store-in-render",
@@ -38779,6 +39314,17 @@ const reactDoctorRules = [
38779
39314
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
38780
39315
  }
38781
39316
  },
39317
+ {
39318
+ key: "react-doctor/no-document-write",
39319
+ id: "no-document-write",
39320
+ source: "react-doctor",
39321
+ originallyExternal: false,
39322
+ rule: {
39323
+ ...noDocumentWrite,
39324
+ framework: "global",
39325
+ category: "Performance"
39326
+ }
39327
+ },
38782
39328
  {
38783
39329
  key: "react-doctor/no-dynamic-import-path",
38784
39330
  id: "no-dynamic-import-path",
@@ -38976,6 +39522,18 @@ const reactDoctorRules = [
38976
39522
  category: "Accessibility"
38977
39523
  }
38978
39524
  },
39525
+ {
39526
+ key: "react-doctor/no-img-lazy-with-high-fetchpriority",
39527
+ id: "no-img-lazy-with-high-fetchpriority",
39528
+ source: "react-doctor",
39529
+ originallyExternal: false,
39530
+ rule: {
39531
+ ...noImgLazyWithHighFetchpriority,
39532
+ framework: "global",
39533
+ category: "Performance",
39534
+ requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
39535
+ }
39536
+ },
38979
39537
  {
38980
39538
  key: "react-doctor/no-initialize-state",
38981
39539
  id: "no-initialize-state",
@@ -39046,6 +39604,17 @@ const reactDoctorRules = [
39046
39604
  requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
39047
39605
  }
39048
39606
  },
39607
+ {
39608
+ key: "react-doctor/no-json-parse-stringify-clone",
39609
+ id: "no-json-parse-stringify-clone",
39610
+ source: "react-doctor",
39611
+ originallyExternal: false,
39612
+ rule: {
39613
+ ...noJsonParseStringifyClone,
39614
+ framework: "global",
39615
+ category: "Performance"
39616
+ }
39617
+ },
39049
39618
  {
39050
39619
  key: "react-doctor/no-jsx-element-type",
39051
39620
  id: "no-jsx-element-type",
@@ -39565,6 +40134,18 @@ const reactDoctorRules = [
39565
40134
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39566
40135
  }
39567
40136
  },
40137
+ {
40138
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40139
+ id: "no-string-false-on-boolean-attribute",
40140
+ source: "react-doctor",
40141
+ originallyExternal: false,
40142
+ rule: {
40143
+ ...noStringFalseOnBooleanAttribute,
40144
+ framework: "global",
40145
+ category: "Bugs",
40146
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40147
+ }
40148
+ },
39568
40149
  {
39569
40150
  key: "react-doctor/no-string-refs",
39570
40151
  id: "no-string-refs",
@@ -39577,6 +40158,17 @@ const reactDoctorRules = [
39577
40158
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39578
40159
  }
39579
40160
  },
40161
+ {
40162
+ key: "react-doctor/no-sync-xhr",
40163
+ id: "no-sync-xhr",
40164
+ source: "react-doctor",
40165
+ originallyExternal: false,
40166
+ rule: {
40167
+ ...noSyncXhr,
40168
+ framework: "global",
40169
+ category: "Performance"
40170
+ }
40171
+ },
39580
40172
  {
39581
40173
  key: "react-doctor/no-this-in-sfc",
39582
40174
  id: "no-this-in-sfc",