oxlint-plugin-react-doctor 0.5.6-dev.b08ca1c → 0.5.6-dev.b8170f8

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 +126 -0
  2. package/dist/index.js +352 -150
  3. package/package.json +1 -1
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$57 = "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$57
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$56 = "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$56
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$56
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
  });
@@ -4272,7 +4326,7 @@ const asyncParallel = defineRule({
4272
4326
  });
4273
4327
  //#endregion
4274
4328
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4275
- const MESSAGE$55 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
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.";
4276
4330
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4277
4331
  const STORAGE_GLOBALS = new Set([
4278
4332
  "window",
@@ -4306,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
4306
4360
  if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4307
4361
  context.report({
4308
4362
  node,
4309
- message: MESSAGE$55
4363
+ message: MESSAGE$57
4310
4364
  });
4311
4365
  },
4312
4366
  AssignmentExpression(node) {
@@ -4317,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
4317
4371
  if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4318
4372
  context.report({
4319
4373
  node: target,
4320
- message: MESSAGE$55
4374
+ message: MESSAGE$57
4321
4375
  });
4322
4376
  }
4323
4377
  })
@@ -4694,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
4694
4748
  //#endregion
4695
4749
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4696
4750
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4697
- const MESSAGE$54 = "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`.";
4698
4752
  const KEY_HANDLERS = [
4699
4753
  "onKeyUp",
4700
4754
  "onKeyDown",
@@ -4726,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
4726
4780
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4727
4781
  context.report({
4728
4782
  node: node.name,
4729
- message: MESSAGE$54
4783
+ message: MESSAGE$56
4730
4784
  });
4731
4785
  } };
4732
4786
  }
@@ -4841,7 +4895,7 @@ const isReactComponentName = (name) => {
4841
4895
  };
4842
4896
  //#endregion
4843
4897
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4844
- const MESSAGE$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
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`.";
4845
4899
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4846
4900
  const DEFAULT_LABELLING_PROPS = [
4847
4901
  "alt",
@@ -5002,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
5002
5056
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
5003
5057
  context.report({
5004
5058
  node: opening,
5005
- message: MESSAGE$53
5059
+ message: MESSAGE$55
5006
5060
  });
5007
5061
  } };
5008
5062
  }
@@ -5432,7 +5486,7 @@ const noVagueButtonLabel = defineRule({
5432
5486
  const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
5487
  //#endregion
5434
5488
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5435
- const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
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.";
5436
5490
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5437
5491
  const NAME_PROVIDING_ATTRIBUTES = [
5438
5492
  "aria-label",
@@ -5455,7 +5509,7 @@ const dialogHasAccessibleName = defineRule({
5455
5509
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5456
5510
  context.report({
5457
5511
  node: node.name,
5458
- message: MESSAGE$52
5512
+ message: MESSAGE$54
5459
5513
  });
5460
5514
  } })
5461
5515
  });
@@ -5494,7 +5548,7 @@ const isEs6Component = (node) => {
5494
5548
  };
5495
5549
  //#endregion
5496
5550
  //#region src/plugin/rules/react-builtins/display-name.ts
5497
- const MESSAGE$51 = "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`.";
5498
5552
  const DEFAULT_ADDITIONAL_HOCS = [
5499
5553
  "observer",
5500
5554
  "lazy",
@@ -5697,7 +5751,7 @@ const displayName = defineRule({
5697
5751
  const reportAt = (node) => {
5698
5752
  context.report({
5699
5753
  node,
5700
- message: MESSAGE$51
5754
+ message: MESSAGE$53
5701
5755
  });
5702
5756
  };
5703
5757
  return {
@@ -7845,7 +7899,7 @@ const forbidElements = defineRule({
7845
7899
  });
7846
7900
  //#endregion
7847
7901
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7848
- const MESSAGE$50 = "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`.";
7849
7903
  const forwardRefUsesRef = defineRule({
7850
7904
  id: "forward-ref-uses-ref",
7851
7905
  title: "forwardRef without ref parameter",
@@ -7865,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
7865
7919
  if (isNodeOfType(onlyParam, "RestElement")) return;
7866
7920
  context.report({
7867
7921
  node: inner,
7868
- message: MESSAGE$50
7922
+ message: MESSAGE$52
7869
7923
  });
7870
7924
  } })
7871
7925
  });
@@ -7902,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7902
7956
  });
7903
7957
  //#endregion
7904
7958
  //#region src/plugin/rules/a11y/heading-has-content.ts
7905
- const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
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`.";
7906
7960
  const DEFAULT_HEADING_TAGS = [
7907
7961
  "h1",
7908
7962
  "h2",
@@ -7935,7 +7989,7 @@ const headingHasContent = defineRule({
7935
7989
  if (isHiddenFromScreenReader(node, context.settings)) return;
7936
7990
  context.report({
7937
7991
  node,
7938
- message: MESSAGE$49
7992
+ message: MESSAGE$51
7939
7993
  });
7940
7994
  } };
7941
7995
  }
@@ -8073,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
8073
8127
  });
8074
8128
  //#endregion
8075
8129
  //#region src/plugin/rules/a11y/html-has-lang.ts
8076
- const MESSAGE$48 = "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`.";
8077
8131
  const resolveSettings$38 = (settings) => {
8078
8132
  const reactDoctor = settings?.["react-doctor"];
8079
8133
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8121,7 +8175,7 @@ const htmlHasLang = defineRule({
8121
8175
  if (!lang) {
8122
8176
  context.report({
8123
8177
  node: node.name,
8124
- message: MESSAGE$48
8178
+ message: MESSAGE$50
8125
8179
  });
8126
8180
  return;
8127
8181
  }
@@ -8129,13 +8183,13 @@ const htmlHasLang = defineRule({
8129
8183
  if (verdict === "missing" || verdict === "empty") {
8130
8184
  context.report({
8131
8185
  node: lang,
8132
- message: MESSAGE$48
8186
+ message: MESSAGE$50
8133
8187
  });
8134
8188
  return;
8135
8189
  }
8136
8190
  if (hasSpread && !lang) context.report({
8137
8191
  node: node.name,
8138
- message: MESSAGE$48
8192
+ message: MESSAGE$50
8139
8193
  });
8140
8194
  } };
8141
8195
  }
@@ -8349,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
8349
8403
  });
8350
8404
  //#endregion
8351
8405
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8352
- const MESSAGE$47 = "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.";
8353
8407
  const evaluateTitleValue = (value) => {
8354
8408
  if (!value) return "missing";
8355
8409
  if (isNodeOfType(value, "Literal")) {
@@ -8389,14 +8443,14 @@ const iframeHasTitle = defineRule({
8389
8443
  if (!titleAttr) {
8390
8444
  if (hasSpread || tag === "iframe") context.report({
8391
8445
  node: node.name,
8392
- message: MESSAGE$47
8446
+ message: MESSAGE$49
8393
8447
  });
8394
8448
  return;
8395
8449
  }
8396
8450
  const verdict = evaluateTitleValue(titleAttr.value);
8397
8451
  if (verdict === "missing" || verdict === "empty") context.report({
8398
8452
  node: titleAttr,
8399
- message: MESSAGE$47
8453
+ message: MESSAGE$49
8400
8454
  });
8401
8455
  } })
8402
8456
  });
@@ -8500,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
8500
8554
  });
8501
8555
  //#endregion
8502
8556
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8503
- const MESSAGE$46 = "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.";
8504
8558
  const DEFAULT_COMPONENTS = ["img"];
8505
8559
  const DEFAULT_REDUNDANT_WORDS = [
8506
8560
  "image",
@@ -8565,7 +8619,7 @@ const imgRedundantAlt = defineRule({
8565
8619
  if (!altAttribute) return;
8566
8620
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8567
8621
  node: altAttribute,
8568
- message: MESSAGE$46
8622
+ message: MESSAGE$48
8569
8623
  });
8570
8624
  } };
8571
8625
  }
@@ -10630,6 +10684,24 @@ const hasJsxKeyAttribute = (openingElement) => {
10630
10684
  return false;
10631
10685
  };
10632
10686
  //#endregion
10687
+ //#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
10688
+ const ascendThroughJsxValueWrappers = (node) => {
10689
+ let current = node;
10690
+ while (current.parent) {
10691
+ const parent = current.parent;
10692
+ if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
10693
+ current = parent;
10694
+ }
10695
+ return current;
10696
+ };
10697
+ const isNonChildrenJsxAttributeValue = (node) => {
10698
+ const container = ascendThroughJsxValueWrappers(node).parent;
10699
+ if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
10700
+ const attribute = container.parent;
10701
+ if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
10702
+ return getJsxAttributeName(attribute.name) !== "children";
10703
+ };
10704
+ //#endregion
10633
10705
  //#region src/plugin/rules/react-builtins/jsx-key.ts
10634
10706
  const ITERATOR_METHOD_NAMES = new Set([
10635
10707
  "map",
@@ -10668,6 +10740,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
10668
10740
  const arrayParent = parent.parent;
10669
10741
  if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
10670
10742
  if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
10743
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10671
10744
  return { kind: "array" };
10672
10745
  } else if (isNodeOfType(parent, "CallExpression")) {
10673
10746
  const callee = parent.callee;
@@ -10680,10 +10753,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
10680
10753
  if (!targetArg) return null;
10681
10754
  let walker = current;
10682
10755
  while (walker && walker !== parent) {
10683
- if (walker === targetArg) return {
10684
- kind: "iterator",
10685
- callExpression: parent
10686
- };
10756
+ if (walker === targetArg) {
10757
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10758
+ return {
10759
+ kind: "iterator",
10760
+ callExpression: parent
10761
+ };
10762
+ }
10687
10763
  walker = walker.parent ?? null;
10688
10764
  }
10689
10765
  return null;
@@ -10922,7 +10998,7 @@ const jsxMaxDepth = defineRule({
10922
10998
  });
10923
10999
  //#endregion
10924
11000
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10925
- const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
11001
+ const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10926
11002
  const LITERAL_TEXT_TAGS = new Set([
10927
11003
  "code",
10928
11004
  "pre",
@@ -10958,7 +11034,7 @@ const jsxNoCommentTextnodes = defineRule({
10958
11034
  if (isInsideLiteralTextTag(node)) return;
10959
11035
  context.report({
10960
11036
  node,
10961
- message: MESSAGE$45
11037
+ message: MESSAGE$47
10962
11038
  });
10963
11039
  } })
10964
11040
  });
@@ -10989,7 +11065,7 @@ const isInsideFunctionScope = (node) => {
10989
11065
  };
10990
11066
  //#endregion
10991
11067
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10992
- const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
11068
+ const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10993
11069
  const CONTEXT_MODULES$1 = [
10994
11070
  "react",
10995
11071
  "use-context-selector",
@@ -11087,7 +11163,7 @@ const jsxNoConstructedContextValues = defineRule({
11087
11163
  if (!isConstructedValue(innerExpression)) continue;
11088
11164
  context.report({
11089
11165
  node: attribute,
11090
- message: MESSAGE$44
11166
+ message: MESSAGE$46
11091
11167
  });
11092
11168
  }
11093
11169
  }
@@ -11173,7 +11249,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11173
11249
  };
11174
11250
  //#endregion
11175
11251
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11176
- const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
11252
+ const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11177
11253
  const KNOWN_SLOT_PROP_NAMES = new Set([
11178
11254
  "icon",
11179
11255
  "Icon",
@@ -11442,7 +11518,7 @@ const jsxNoJsxAsProp = defineRule({
11442
11518
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11443
11519
  context.report({
11444
11520
  node,
11445
- message: MESSAGE$43
11521
+ message: MESSAGE$45
11446
11522
  });
11447
11523
  }
11448
11524
  };
@@ -11730,7 +11806,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11730
11806
  ];
11731
11807
  //#endregion
11732
11808
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11733
- const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
11809
+ const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11734
11810
  const isDataArrayPropName = (propName) => {
11735
11811
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11736
11812
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11814,7 +11890,7 @@ const jsxNoNewArrayAsProp = defineRule({
11814
11890
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11815
11891
  context.report({
11816
11892
  node,
11817
- message: MESSAGE$42
11893
+ message: MESSAGE$44
11818
11894
  });
11819
11895
  }
11820
11896
  };
@@ -12072,7 +12148,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12072
12148
  ]);
12073
12149
  //#endregion
12074
12150
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
12075
- const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
12151
+ const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
12076
12152
  const isAccessorPredicateName = (propName) => {
12077
12153
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12078
12154
  if (propName.length <= prefix.length) continue;
@@ -12278,7 +12354,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12278
12354
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12279
12355
  context.report({
12280
12356
  node,
12281
- message: MESSAGE$41
12357
+ message: MESSAGE$43
12282
12358
  });
12283
12359
  }
12284
12360
  };
@@ -12498,7 +12574,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12498
12574
  ];
12499
12575
  //#endregion
12500
12576
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12501
- const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
12577
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12502
12578
  const isConfigObjectPropName = (propName) => {
12503
12579
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12504
12580
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12586,7 +12662,7 @@ const jsxNoNewObjectAsProp = defineRule({
12586
12662
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12587
12663
  context.report({
12588
12664
  node,
12589
- message: MESSAGE$40
12665
+ message: MESSAGE$42
12590
12666
  });
12591
12667
  }
12592
12668
  };
@@ -12594,7 +12670,7 @@ const jsxNoNewObjectAsProp = defineRule({
12594
12670
  });
12595
12671
  //#endregion
12596
12672
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12597
- const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12673
+ const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12598
12674
  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;
12599
12675
  const resolveSettings$28 = (settings) => {
12600
12676
  const reactDoctor = settings?.["react-doctor"];
@@ -12635,7 +12711,7 @@ const jsxNoScriptUrl = defineRule({
12635
12711
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12636
12712
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12637
12713
  node: attribute,
12638
- message: MESSAGE$39
12714
+ message: MESSAGE$41
12639
12715
  });
12640
12716
  }
12641
12717
  } };
@@ -12950,7 +13026,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12950
13026
  });
12951
13027
  //#endregion
12952
13028
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12953
- const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
13029
+ const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12954
13030
  const resolveSettings$25 = (settings) => {
12955
13031
  const reactDoctor = settings?.["react-doctor"];
12956
13032
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12991,7 +13067,7 @@ const jsxPropsNoSpreading = defineRule({
12991
13067
  }
12992
13068
  context.report({
12993
13069
  node: attribute,
12994
- message: MESSAGE$38
13070
+ message: MESSAGE$40
12995
13071
  });
12996
13072
  }
12997
13073
  } };
@@ -13219,7 +13295,7 @@ const labelHasAssociatedControl = defineRule({
13219
13295
  });
13220
13296
  //#endregion
13221
13297
  //#region src/plugin/rules/a11y/lang.ts
13222
- const MESSAGE$37 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13298
+ 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`.";
13223
13299
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13224
13300
  "aa",
13225
13301
  "ab",
@@ -13431,7 +13507,7 @@ const lang = defineRule({
13431
13507
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13432
13508
  context.report({
13433
13509
  node: langAttr,
13434
- message: MESSAGE$37
13510
+ message: MESSAGE$39
13435
13511
  });
13436
13512
  return;
13437
13513
  }
@@ -13440,7 +13516,7 @@ const lang = defineRule({
13440
13516
  if (value === null) return;
13441
13517
  if (!isValidLangTag(value)) context.report({
13442
13518
  node: langAttr,
13443
- message: MESSAGE$37
13519
+ message: MESSAGE$39
13444
13520
  });
13445
13521
  } })
13446
13522
  });
@@ -13466,6 +13542,7 @@ const mcpToolCapabilityRisk = defineRule({
13466
13542
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13467
13543
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13468
13544
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13545
+ ignoreStringLiterals: true,
13469
13546
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13470
13547
  })
13471
13548
  });
@@ -13484,7 +13561,7 @@ const mdxSsrExecutionRisk = defineRule({
13484
13561
  });
13485
13562
  //#endregion
13486
13563
  //#region src/plugin/rules/a11y/media-has-caption.ts
13487
- const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13564
+ const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13488
13565
  const DEFAULT_AUDIO = ["audio"];
13489
13566
  const DEFAULT_VIDEO = ["video"];
13490
13567
  const DEFAULT_TRACK = ["track"];
@@ -13525,7 +13602,7 @@ const mediaHasCaption = defineRule({
13525
13602
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13526
13603
  context.report({
13527
13604
  node: node.name,
13528
- message: MESSAGE$36
13605
+ message: MESSAGE$38
13529
13606
  });
13530
13607
  return;
13531
13608
  }
@@ -13542,7 +13619,7 @@ const mediaHasCaption = defineRule({
13542
13619
  return kindValue.value.toLowerCase() === "captions";
13543
13620
  })) context.report({
13544
13621
  node: node.name,
13545
- message: MESSAGE$36
13622
+ message: MESSAGE$38
13546
13623
  });
13547
13624
  } };
13548
13625
  }
@@ -15343,7 +15420,7 @@ const nextjsNoVercelOgImport = defineRule({
15343
15420
  });
15344
15421
  //#endregion
15345
15422
  //#region src/plugin/rules/a11y/no-access-key.ts
15346
- const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15423
+ const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15347
15424
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15348
15425
  const noAccessKey = defineRule({
15349
15426
  id: "no-access-key",
@@ -15360,7 +15437,7 @@ const noAccessKey = defineRule({
15360
15437
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15361
15438
  context.report({
15362
15439
  node: accessKey,
15363
- message: MESSAGE$35
15440
+ message: MESSAGE$37
15364
15441
  });
15365
15442
  return;
15366
15443
  }
@@ -15370,7 +15447,7 @@ const noAccessKey = defineRule({
15370
15447
  if (isUndefinedIdentifier(expression)) return;
15371
15448
  context.report({
15372
15449
  node: accessKey,
15373
- message: MESSAGE$35
15450
+ message: MESSAGE$37
15374
15451
  });
15375
15452
  }
15376
15453
  } })
@@ -15853,7 +15930,7 @@ const noAdjustStateOnPropChange = defineRule({
15853
15930
  });
15854
15931
  //#endregion
15855
15932
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15856
- const MESSAGE$34 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15933
+ 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.";
15857
15934
  const noAriaHiddenOnFocusable = defineRule({
15858
15935
  id: "no-aria-hidden-on-focusable",
15859
15936
  title: "aria-hidden on focusable element",
@@ -15880,7 +15957,7 @@ const noAriaHiddenOnFocusable = defineRule({
15880
15957
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15881
15958
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15882
15959
  node: ariaHidden,
15883
- message: MESSAGE$34
15960
+ message: MESSAGE$36
15884
15961
  });
15885
15962
  } })
15886
15963
  });
@@ -16248,7 +16325,7 @@ const noArrayIndexAsKey = defineRule({
16248
16325
  });
16249
16326
  //#endregion
16250
16327
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16251
- const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
16328
+ const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16252
16329
  const SECOND_INDEX_METHODS = new Set([
16253
16330
  "every",
16254
16331
  "filter",
@@ -16452,7 +16529,7 @@ const noArrayIndexKey = defineRule({
16452
16529
  }
16453
16530
  context.report({
16454
16531
  node: keyAttribute,
16455
- message: MESSAGE$33
16532
+ message: MESSAGE$35
16456
16533
  });
16457
16534
  },
16458
16535
  CallExpression(node) {
@@ -16472,7 +16549,7 @@ const noArrayIndexKey = defineRule({
16472
16549
  if (propName !== "key") continue;
16473
16550
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16474
16551
  node: property,
16475
- message: MESSAGE$33
16552
+ message: MESSAGE$35
16476
16553
  });
16477
16554
  }
16478
16555
  }
@@ -16480,7 +16557,7 @@ const noArrayIndexKey = defineRule({
16480
16557
  });
16481
16558
  //#endregion
16482
16559
  //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16483
- const MESSAGE$32 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16560
+ 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.";
16484
16561
  const noAsyncEffectCallback = defineRule({
16485
16562
  id: "no-async-effect-callback",
16486
16563
  title: "Async effect callback",
@@ -16494,13 +16571,13 @@ const noAsyncEffectCallback = defineRule({
16494
16571
  if (!callback.async) return;
16495
16572
  context.report({
16496
16573
  node: callback,
16497
- message: MESSAGE$32
16574
+ message: MESSAGE$34
16498
16575
  });
16499
16576
  } })
16500
16577
  });
16501
16578
  //#endregion
16502
16579
  //#region src/plugin/rules/a11y/no-autofocus.ts
16503
- const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16580
+ 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.";
16504
16581
  const resolveSettings$21 = (settings) => {
16505
16582
  const reactDoctor = settings?.["react-doctor"];
16506
16583
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16556,7 +16633,7 @@ const noAutofocus = defineRule({
16556
16633
  }
16557
16634
  context.report({
16558
16635
  node: autoFocusAttribute,
16559
- message: MESSAGE$31
16636
+ message: MESSAGE$33
16560
16637
  });
16561
16638
  } };
16562
16639
  }
@@ -17060,7 +17137,7 @@ const noChainStateUpdates = defineRule({
17060
17137
  });
17061
17138
  //#endregion
17062
17139
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
17063
- const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17140
+ const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17064
17141
  const noChildrenProp = defineRule({
17065
17142
  id: "no-children-prop",
17066
17143
  title: "Children passed as a prop",
@@ -17072,7 +17149,7 @@ const noChildrenProp = defineRule({
17072
17149
  if (node.name.name !== "children") return;
17073
17150
  context.report({
17074
17151
  node: node.name,
17075
- message: MESSAGE$30
17152
+ message: MESSAGE$32
17076
17153
  });
17077
17154
  },
17078
17155
  CallExpression(node) {
@@ -17085,7 +17162,7 @@ const noChildrenProp = defineRule({
17085
17162
  const propertyKey = property.key;
17086
17163
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17087
17164
  node: propertyKey,
17088
- message: MESSAGE$30
17165
+ message: MESSAGE$32
17089
17166
  });
17090
17167
  }
17091
17168
  }
@@ -17093,7 +17170,7 @@ const noChildrenProp = defineRule({
17093
17170
  });
17094
17171
  //#endregion
17095
17172
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
17096
- const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17173
+ const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17097
17174
  const noCloneElement = defineRule({
17098
17175
  id: "no-clone-element",
17099
17176
  title: "cloneElement makes child props fragile",
@@ -17106,7 +17183,7 @@ const noCloneElement = defineRule({
17106
17183
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17107
17184
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17108
17185
  node: callee,
17109
- message: MESSAGE$29
17186
+ message: MESSAGE$31
17110
17187
  });
17111
17188
  return;
17112
17189
  }
@@ -17119,7 +17196,7 @@ const noCloneElement = defineRule({
17119
17196
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17120
17197
  context.report({
17121
17198
  node: callee,
17122
- message: MESSAGE$29
17199
+ message: MESSAGE$31
17123
17200
  });
17124
17201
  }
17125
17202
  } })
@@ -17168,7 +17245,7 @@ const enclosingComponentOrHookName = (node) => {
17168
17245
  };
17169
17246
  //#endregion
17170
17247
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
17171
- const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17248
+ const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17172
17249
  const CONTEXT_MODULES = [
17173
17250
  "react",
17174
17251
  "use-context-selector",
@@ -17204,13 +17281,13 @@ const noCreateContextInRender = defineRule({
17204
17281
  if (!componentOrHookName) return;
17205
17282
  context.report({
17206
17283
  node,
17207
- message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
17284
+ message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17208
17285
  });
17209
17286
  } })
17210
17287
  });
17211
17288
  //#endregion
17212
17289
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17213
- const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17290
+ 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.";
17214
17291
  const noCreateRefInFunctionComponent = defineRule({
17215
17292
  id: "no-create-ref-in-function-component",
17216
17293
  title: "createRef in function component",
@@ -17229,7 +17306,7 @@ const noCreateRefInFunctionComponent = defineRule({
17229
17306
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17230
17307
  context.report({
17231
17308
  node,
17232
- message: MESSAGE$27
17309
+ message: MESSAGE$29
17233
17310
  });
17234
17311
  } })
17235
17312
  });
@@ -17369,7 +17446,7 @@ const noCreateStoreInRender = defineRule({
17369
17446
  });
17370
17447
  //#endregion
17371
17448
  //#region src/plugin/rules/react-builtins/no-danger.ts
17372
- const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17449
+ const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17373
17450
  const noDanger = defineRule({
17374
17451
  id: "no-danger",
17375
17452
  title: "Raw HTML injection can run unsafe markup",
@@ -17382,7 +17459,7 @@ const noDanger = defineRule({
17382
17459
  if (!propAttribute) return;
17383
17460
  context.report({
17384
17461
  node: propAttribute.name,
17385
- message: MESSAGE$26
17462
+ message: MESSAGE$28
17386
17463
  });
17387
17464
  },
17388
17465
  CallExpression(node) {
@@ -17394,7 +17471,7 @@ const noDanger = defineRule({
17394
17471
  const propertyKey = property.key;
17395
17472
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17396
17473
  node: propertyKey,
17397
- message: MESSAGE$26
17474
+ message: MESSAGE$28
17398
17475
  });
17399
17476
  }
17400
17477
  }
@@ -17402,7 +17479,7 @@ const noDanger = defineRule({
17402
17479
  });
17403
17480
  //#endregion
17404
17481
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17405
- const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17482
+ const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17406
17483
  const isLineBreak = (child) => {
17407
17484
  if (!isNodeOfType(child, "JSXText")) return false;
17408
17485
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17472,7 +17549,7 @@ const noDangerWithChildren = defineRule({
17472
17549
  if (!hasChildrenProp && !hasNestedChildren) return;
17473
17550
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17474
17551
  node: opening,
17475
- message: MESSAGE$25
17552
+ message: MESSAGE$27
17476
17553
  });
17477
17554
  },
17478
17555
  CallExpression(node) {
@@ -17484,7 +17561,7 @@ const noDangerWithChildren = defineRule({
17484
17561
  if (!propsShape.hasDangerously) return;
17485
17562
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17486
17563
  node,
17487
- message: MESSAGE$25
17564
+ message: MESSAGE$27
17488
17565
  });
17489
17566
  }
17490
17567
  })
@@ -18061,7 +18138,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
18061
18138
  //#endregion
18062
18139
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
18063
18140
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
18064
- const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18141
+ const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18065
18142
  const resolveSettings$20 = (settings) => {
18066
18143
  const reactDoctor = settings?.["react-doctor"];
18067
18144
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18080,7 +18157,7 @@ const noDidMountSetState = defineRule({
18080
18157
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18081
18158
  context.report({
18082
18159
  node: node.callee,
18083
- message: MESSAGE$24
18160
+ message: MESSAGE$26
18084
18161
  });
18085
18162
  } };
18086
18163
  }
@@ -18088,7 +18165,7 @@ const noDidMountSetState = defineRule({
18088
18165
  //#endregion
18089
18166
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18090
18167
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
18091
- const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18168
+ const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18092
18169
  const resolveSettings$19 = (settings) => {
18093
18170
  const reactDoctor = settings?.["react-doctor"];
18094
18171
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18107,7 +18184,7 @@ const noDidUpdateSetState = defineRule({
18107
18184
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18108
18185
  context.report({
18109
18186
  node: node.callee,
18110
- message: MESSAGE$23
18187
+ message: MESSAGE$25
18111
18188
  });
18112
18189
  } };
18113
18190
  }
@@ -18130,7 +18207,7 @@ const isStateMemberExpression = (node) => {
18130
18207
  };
18131
18208
  //#endregion
18132
18209
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
18133
- const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18210
+ const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18134
18211
  const shouldIgnoreMutation = (node) => {
18135
18212
  let isConstructor = false;
18136
18213
  let isInsideCallExpression = false;
@@ -18152,7 +18229,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18152
18229
  if (shouldIgnoreMutation(reportNode)) return;
18153
18230
  context.report({
18154
18231
  node: reportNode,
18155
- message: MESSAGE$22
18232
+ message: MESSAGE$24
18156
18233
  });
18157
18234
  };
18158
18235
  const noDirectMutationState = defineRule({
@@ -18362,6 +18439,26 @@ const noDocumentStartViewTransition = defineRule({
18362
18439
  } })
18363
18440
  });
18364
18441
  //#endregion
18442
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18443
+ 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.";
18444
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18445
+ const noDocumentWrite = defineRule({
18446
+ id: "no-document-write",
18447
+ title: "document.write/writeln",
18448
+ severity: "warn",
18449
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18450
+ create: (context) => ({ CallExpression(node) {
18451
+ const callee = node.callee;
18452
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18453
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18454
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18455
+ context.report({
18456
+ node,
18457
+ message: MESSAGE$23
18458
+ });
18459
+ } })
18460
+ });
18461
+ //#endregion
18365
18462
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18366
18463
  const noDynamicImportPath = defineRule({
18367
18464
  id: "no-dynamic-import-path",
@@ -19740,7 +19837,7 @@ const ALLOWED_NAMESPACES = new Set([
19740
19837
  "ReactDOM",
19741
19838
  "ReactDom"
19742
19839
  ]);
19743
- const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19840
+ const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19744
19841
  const noFindDomNode = defineRule({
19745
19842
  id: "no-find-dom-node",
19746
19843
  title: "findDOMNode breaks component encapsulation",
@@ -19751,7 +19848,7 @@ const noFindDomNode = defineRule({
19751
19848
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19752
19849
  context.report({
19753
19850
  node: callee,
19754
- message: MESSAGE$21
19851
+ message: MESSAGE$22
19755
19852
  });
19756
19853
  return;
19757
19854
  }
@@ -19762,7 +19859,7 @@ const noFindDomNode = defineRule({
19762
19859
  if (callee.property.name !== "findDOMNode") return;
19763
19860
  context.report({
19764
19861
  node: callee.property,
19765
- message: MESSAGE$21
19862
+ message: MESSAGE$22
19766
19863
  });
19767
19864
  }
19768
19865
  } })
@@ -20004,7 +20101,7 @@ const noGrayOnColoredBackground = defineRule({
20004
20101
  });
20005
20102
  //#endregion
20006
20103
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20007
- const MESSAGE$20 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20104
+ 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.";
20008
20105
  const noImgLazyWithHighFetchpriority = defineRule({
20009
20106
  id: "no-img-lazy-with-high-fetchpriority",
20010
20107
  title: "Lazy image with high fetchPriority",
@@ -20018,7 +20115,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
20018
20115
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20019
20116
  context.report({
20020
20117
  node: node.name,
20021
- message: MESSAGE$20
20118
+ message: MESSAGE$21
20022
20119
  });
20023
20120
  } })
20024
20121
  });
@@ -20115,15 +20212,20 @@ const noInlineExhaustiveStyle = defineRule({
20115
20212
  severity: "warn",
20116
20213
  tags: ["test-noise", "react-jsx-only"],
20117
20214
  recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
20118
- create: (context) => ({ JSXAttribute(node) {
20119
- const expression = getInlineStyleExpression(node);
20120
- if (!expression) return;
20121
- const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20122
- if (propertyCount >= 8) context.report({
20123
- node: expression,
20124
- message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20125
- });
20126
- } })
20215
+ create: (context) => {
20216
+ if (isGeneratedImageRenderContext(context)) return {};
20217
+ return { JSXAttribute(node) {
20218
+ const expression = getInlineStyleExpression(node);
20219
+ if (!expression) return;
20220
+ const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20221
+ if (propertyCount < 8) return;
20222
+ if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
20223
+ context.report({
20224
+ node: expression,
20225
+ message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20226
+ });
20227
+ } };
20228
+ }
20127
20229
  });
20128
20230
  //#endregion
20129
20231
  //#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
@@ -20253,7 +20355,7 @@ const noIsMounted = defineRule({
20253
20355
  });
20254
20356
  //#endregion
20255
20357
  //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20256
- const MESSAGE$19 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20358
+ 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)`.";
20257
20359
  const isJsonMethodCall = (node, method) => {
20258
20360
  if (!isNodeOfType(node, "CallExpression")) return false;
20259
20361
  const callee = node.callee;
@@ -20270,13 +20372,13 @@ const noJsonParseStringifyClone = defineRule({
20270
20372
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20271
20373
  context.report({
20272
20374
  node,
20273
- message: MESSAGE$19
20375
+ message: MESSAGE$20
20274
20376
  });
20275
20377
  } })
20276
20378
  });
20277
20379
  //#endregion
20278
20380
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
20279
- const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20381
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20280
20382
  const isJsxElementTypeReference = (node) => {
20281
20383
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20282
20384
  const typeName = node.typeName;
@@ -20293,7 +20395,7 @@ const checkReturnType = (context, returnType) => {
20293
20395
  if (!typeAnnotation) return;
20294
20396
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20295
20397
  node: typeAnnotation,
20296
- message: MESSAGE$18
20398
+ message: MESSAGE$19
20297
20399
  });
20298
20400
  };
20299
20401
  const noJsxElementType = defineRule({
@@ -20748,7 +20850,7 @@ const noMoment = defineRule({
20748
20850
  });
20749
20851
  //#endregion
20750
20852
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20751
- const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
20853
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
20752
20854
  const resolveSettings$16 = (settings) => {
20753
20855
  const reactDoctor = settings?.["react-doctor"];
20754
20856
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21070,7 +21172,7 @@ const noMultiComp = defineRule({
21070
21172
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21071
21173
  for (const component of flagged.slice(1)) context.report({
21072
21174
  node: component.reportNode,
21073
- message: MESSAGE$17
21175
+ message: MESSAGE$18
21074
21176
  });
21075
21177
  } };
21076
21178
  }
@@ -21238,7 +21340,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21238
21340
  };
21239
21341
  //#endregion
21240
21342
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
21241
- const MESSAGE$16 = "This reducer changes state in place, so your update is silently skipped.";
21343
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
21242
21344
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21243
21345
  "copyWithin",
21244
21346
  "fill",
@@ -21448,7 +21550,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21448
21550
  reportedNodes.add(options.crossFileConsumerCallSite);
21449
21551
  context.report({
21450
21552
  node: options.crossFileConsumerCallSite,
21451
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21553
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21452
21554
  });
21453
21555
  return;
21454
21556
  }
@@ -21457,7 +21559,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21457
21559
  reportedNodes.add(mutation.node);
21458
21560
  context.report({
21459
21561
  node: mutation.node,
21460
- message: MESSAGE$16
21562
+ message: MESSAGE$17
21461
21563
  });
21462
21564
  }
21463
21565
  };
@@ -21729,7 +21831,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21729
21831
  });
21730
21832
  //#endregion
21731
21833
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21732
- const MESSAGE$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21834
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21733
21835
  const resolveSettings$14 = (settings) => {
21734
21836
  const reactDoctor = settings?.["react-doctor"];
21735
21837
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21757,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
21757
21859
  if (numeric === null) {
21758
21860
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21759
21861
  node: tabIndex,
21760
- message: MESSAGE$15
21862
+ message: MESSAGE$16
21761
21863
  });
21762
21864
  return;
21763
21865
  }
@@ -21770,7 +21872,7 @@ const noNoninteractiveTabindex = defineRule({
21770
21872
  if (!roleAttribute) {
21771
21873
  context.report({
21772
21874
  node: tabIndex,
21773
- message: MESSAGE$15
21875
+ message: MESSAGE$16
21774
21876
  });
21775
21877
  return;
21776
21878
  }
@@ -21784,7 +21886,7 @@ const noNoninteractiveTabindex = defineRule({
21784
21886
  }
21785
21887
  context.report({
21786
21888
  node: tabIndex,
21787
- message: MESSAGE$15
21889
+ message: MESSAGE$16
21788
21890
  });
21789
21891
  } };
21790
21892
  }
@@ -22475,7 +22577,7 @@ const noRandomKey = defineRule({
22475
22577
  });
22476
22578
  //#endregion
22477
22579
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22478
- const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22580
+ const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22479
22581
  const isChildrenIdentifier = (node, contextNode) => {
22480
22582
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22481
22583
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22501,13 +22603,13 @@ const noReactChildren = defineRule({
22501
22603
  if (isChildrenIdentifier(memberObject, node)) {
22502
22604
  context.report({
22503
22605
  node: calleeOuter,
22504
- message: MESSAGE$14
22606
+ message: MESSAGE$15
22505
22607
  });
22506
22608
  return;
22507
22609
  }
22508
22610
  if (isReactNamespaceMember(memberObject, node)) context.report({
22509
22611
  node: calleeOuter,
22510
- message: MESSAGE$14
22612
+ message: MESSAGE$15
22511
22613
  });
22512
22614
  } })
22513
22615
  });
@@ -22830,7 +22932,7 @@ const noRenderPropChildren = defineRule({
22830
22932
  });
22831
22933
  //#endregion
22832
22934
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22833
- const MESSAGE$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22935
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22834
22936
  const isReactDomRenderCall = (node) => {
22835
22937
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22836
22938
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22854,7 +22956,7 @@ const noRenderReturnValue = defineRule({
22854
22956
  if (!isUsedAsReturnValue(node.parent)) return;
22855
22957
  context.report({
22856
22958
  node: node.callee,
22857
- message: MESSAGE$13
22959
+ message: MESSAGE$14
22858
22960
  });
22859
22961
  } })
22860
22962
  });
@@ -23552,7 +23654,7 @@ const getParentComponent = (node) => {
23552
23654
  };
23553
23655
  //#endregion
23554
23656
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23555
- const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23657
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23556
23658
  const noSetState = defineRule({
23557
23659
  id: "no-set-state",
23558
23660
  title: "Local class state forbidden",
@@ -23567,7 +23669,7 @@ const noSetState = defineRule({
23567
23669
  if (!getParentComponent(node)) return;
23568
23670
  context.report({
23569
23671
  node: node.callee,
23570
- message: MESSAGE$12
23672
+ message: MESSAGE$13
23571
23673
  });
23572
23674
  } })
23573
23675
  });
@@ -23729,7 +23831,7 @@ const isAbstractRole = (openingElement, settings) => {
23729
23831
  };
23730
23832
  //#endregion
23731
23833
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23732
- 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.";
23834
+ 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.";
23733
23835
  const DEFAULT_HANDLERS = [
23734
23836
  "onClick",
23735
23837
  "onMouseDown",
@@ -23789,7 +23891,7 @@ const noStaticElementInteractions = defineRule({
23789
23891
  if (!roleAttribute || !roleAttribute.value) {
23790
23892
  context.report({
23791
23893
  node: node.name,
23792
- message: MESSAGE$11
23894
+ message: MESSAGE$12
23793
23895
  });
23794
23896
  return;
23795
23897
  }
@@ -23799,19 +23901,66 @@ const noStaticElementInteractions = defineRule({
23799
23901
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23800
23902
  context.report({
23801
23903
  node: node.name,
23802
- message: MESSAGE$11
23904
+ message: MESSAGE$12
23803
23905
  });
23804
23906
  return;
23805
23907
  }
23806
23908
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23807
23909
  context.report({
23808
23910
  node: node.name,
23809
- message: MESSAGE$11
23911
+ message: MESSAGE$12
23810
23912
  });
23811
23913
  } };
23812
23914
  }
23813
23915
  });
23814
23916
  //#endregion
23917
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
23918
+ const BOOLEAN_ATTRIBUTES = new Set([
23919
+ "disabled",
23920
+ "checked",
23921
+ "readonly",
23922
+ "required",
23923
+ "selected",
23924
+ "multiple",
23925
+ "autofocus",
23926
+ "autoplay",
23927
+ "controls",
23928
+ "loop",
23929
+ "muted",
23930
+ "open",
23931
+ "reversed",
23932
+ "default",
23933
+ "novalidate",
23934
+ "formnovalidate",
23935
+ "playsinline",
23936
+ "itemscope",
23937
+ "allowfullscreen"
23938
+ ]);
23939
+ const noStringFalseOnBooleanAttribute = defineRule({
23940
+ id: "no-string-false-on-boolean-attribute",
23941
+ title: "String true/false on a boolean attribute",
23942
+ severity: "warn",
23943
+ 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.",
23944
+ create: (context) => ({ JSXOpeningElement(node) {
23945
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
23946
+ const firstCharacter = node.name.name.charCodeAt(0);
23947
+ if (firstCharacter < 97 || firstCharacter > 122) return;
23948
+ for (const attribute of node.attributes) {
23949
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
23950
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
23951
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
23952
+ const value = getJsxPropStringValue(attribute);
23953
+ if (value !== "false" && value !== "true") continue;
23954
+ const attributeName = attribute.name.name;
23955
+ 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}\``;
23956
+ context.report({
23957
+ node: attribute,
23958
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
23959
+ });
23960
+ }
23961
+ } })
23962
+ });
23963
+ //#endregion
23815
23964
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23816
23965
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23817
23966
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23862,6 +24011,27 @@ const noStringRefs = defineRule({
23862
24011
  }
23863
24012
  });
23864
24013
  //#endregion
24014
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
24015
+ 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)`).";
24016
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24017
+ const noSyncXhr = defineRule({
24018
+ id: "no-sync-xhr",
24019
+ title: "Synchronous XMLHttpRequest",
24020
+ severity: "warn",
24021
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
24022
+ create: (context) => ({ CallExpression(node) {
24023
+ const callee = node.callee;
24024
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
24025
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
24026
+ const asyncArgument = node.arguments?.[2];
24027
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24028
+ context.report({
24029
+ node,
24030
+ message: MESSAGE$11
24031
+ });
24032
+ } })
24033
+ });
24034
+ //#endregion
23865
24035
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23866
24036
  const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
23867
24037
  const isInsideClassMethod = (node, customClassFactoryNames) => {
@@ -35437,13 +35607,7 @@ const serverNoMutableModuleState = defineRule({
35437
35607
  const collectDeclaredNames = (declaration) => {
35438
35608
  const names = /* @__PURE__ */ new Set();
35439
35609
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35440
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35441
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35442
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35443
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35444
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35445
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35446
- }
35610
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35447
35611
  return names;
35448
35612
  };
35449
35613
  const declarationStartsWithAwait = (declaration) => {
@@ -35453,11 +35617,15 @@ const declarationStartsWithAwait = (declaration) => {
35453
35617
  };
35454
35618
  const declarationReadsAnyName = (declaration, names) => {
35455
35619
  if (names.size === 0) return false;
35620
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35456
35621
  let didRead = false;
35457
- walkAst(declaration, (child) => {
35458
- if (didRead) return;
35459
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35460
- });
35622
+ for (const declarator of declaration.declarations ?? []) {
35623
+ if (!declarator.init) continue;
35624
+ walkAst(declarator.init, (child) => {
35625
+ if (didRead) return;
35626
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35627
+ });
35628
+ }
35461
35629
  return didRead;
35462
35630
  };
35463
35631
  const serverSequentialIndependentAwait = defineRule({
@@ -36717,7 +36885,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36717
36885
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36718
36886
  scan: scanByPattern({
36719
36887
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36720
- 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,
36888
+ 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,
36721
36889
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36722
36890
  })
36723
36891
  });
@@ -39173,6 +39341,17 @@ const reactDoctorRules = [
39173
39341
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
39174
39342
  }
39175
39343
  },
39344
+ {
39345
+ key: "react-doctor/no-document-write",
39346
+ id: "no-document-write",
39347
+ source: "react-doctor",
39348
+ originallyExternal: false,
39349
+ rule: {
39350
+ ...noDocumentWrite,
39351
+ framework: "global",
39352
+ category: "Performance"
39353
+ }
39354
+ },
39176
39355
  {
39177
39356
  key: "react-doctor/no-dynamic-import-path",
39178
39357
  id: "no-dynamic-import-path",
@@ -39982,6 +40161,18 @@ const reactDoctorRules = [
39982
40161
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39983
40162
  }
39984
40163
  },
40164
+ {
40165
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40166
+ id: "no-string-false-on-boolean-attribute",
40167
+ source: "react-doctor",
40168
+ originallyExternal: false,
40169
+ rule: {
40170
+ ...noStringFalseOnBooleanAttribute,
40171
+ framework: "global",
40172
+ category: "Bugs",
40173
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40174
+ }
40175
+ },
39985
40176
  {
39986
40177
  key: "react-doctor/no-string-refs",
39987
40178
  id: "no-string-refs",
@@ -39994,6 +40185,17 @@ const reactDoctorRules = [
39994
40185
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39995
40186
  }
39996
40187
  },
40188
+ {
40189
+ key: "react-doctor/no-sync-xhr",
40190
+ id: "no-sync-xhr",
40191
+ source: "react-doctor",
40192
+ originallyExternal: false,
40193
+ rule: {
40194
+ ...noSyncXhr,
40195
+ framework: "global",
40196
+ category: "Performance"
40197
+ }
40198
+ },
39997
40199
  {
39998
40200
  key: "react-doctor/no-this-in-sfc",
39999
40201
  id: "no-this-in-sfc",