oxlint-plugin-react-doctor 0.5.8-dev.80e3093 → 0.5.8-dev.8232e96

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 (2) hide show
  1. package/dist/index.js +288 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1160,6 +1160,61 @@ const isFirebaseRulesPath = (relativePath) => /(?:^|\/)(?:firestore\.rules|stora
1160
1160
  //#endregion
1161
1161
  //#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
1162
1162
  const WHITESPACE_PATTERN = /\s/;
1163
+ const IDENTIFIER_CHARACTER_PATTERN = /[\p{ID_Continue}$]/u;
1164
+ const REGEX_PRECEDING_KEYWORDS = new Set([
1165
+ "return",
1166
+ "typeof",
1167
+ "case",
1168
+ "in",
1169
+ "of",
1170
+ "new",
1171
+ "delete",
1172
+ "void",
1173
+ "instanceof",
1174
+ "do",
1175
+ "else",
1176
+ "yield",
1177
+ "await",
1178
+ "throw"
1179
+ ]);
1180
+ const isRegexLiteralStart = (characters, slashIndex) => {
1181
+ let cursor = slashIndex - 1;
1182
+ while (cursor >= 0 && WHITESPACE_PATTERN.test(characters[cursor])) cursor -= 1;
1183
+ if (cursor < 0) return true;
1184
+ const previousCharacter = characters[cursor];
1185
+ const characterBefore = cursor > 0 ? characters[cursor - 1] : "";
1186
+ if (IDENTIFIER_CHARACTER_PATTERN.test(previousCharacter)) {
1187
+ let wordStartIndex = cursor;
1188
+ while (wordStartIndex > 0 && IDENTIFIER_CHARACTER_PATTERN.test(characters[wordStartIndex - 1])) wordStartIndex -= 1;
1189
+ if (wordStartIndex > 0 && characters[wordStartIndex - 1] === ".") return false;
1190
+ return REGEX_PRECEDING_KEYWORDS.has(characters.slice(wordStartIndex, cursor + 1).join(""));
1191
+ }
1192
+ if (previousCharacter === "<") return false;
1193
+ if (previousCharacter === ">") return characterBefore === "=";
1194
+ if (previousCharacter === "+" || previousCharacter === "-") return characterBefore !== previousCharacter;
1195
+ if (previousCharacter === "!") return !(IDENTIFIER_CHARACTER_PATTERN.test(characterBefore) || characterBefore === ")" || characterBefore === "]");
1196
+ return !(previousCharacter === ")" || previousCharacter === "]" || previousCharacter === "}" || previousCharacter === "\"" || previousCharacter === "'" || previousCharacter === "`");
1197
+ };
1198
+ const findRegexLiteralEnd = (content, slashIndex) => {
1199
+ let cursor = slashIndex + 1;
1200
+ let isInsideCharacterClass = false;
1201
+ while (cursor < content.length) {
1202
+ const character = content[cursor];
1203
+ if (character === "\\") {
1204
+ cursor += 2;
1205
+ continue;
1206
+ }
1207
+ if (character === "\n") return null;
1208
+ if (character === "[") isInsideCharacterClass = true;
1209
+ else if (character === "]") isInsideCharacterClass = false;
1210
+ else if (character === "/" && !isInsideCharacterClass) {
1211
+ if (content[cursor + 1] === "/") return null;
1212
+ return cursor + 1;
1213
+ }
1214
+ cursor += 1;
1215
+ }
1216
+ return null;
1217
+ };
1163
1218
  const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
1164
1219
  for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
1165
1220
  const character = content[cursor];
@@ -1193,6 +1248,11 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1193
1248
  index += 2;
1194
1249
  continue;
1195
1250
  }
1251
+ if (character === "\n" && stringDelimiter !== "`") {
1252
+ stringDelimiter = null;
1253
+ index += 1;
1254
+ continue;
1255
+ }
1196
1256
  if (character === stringDelimiter) {
1197
1257
  stringDelimiter = null;
1198
1258
  index += 1;
@@ -1240,6 +1300,14 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
1240
1300
  }
1241
1301
  continue;
1242
1302
  }
1303
+ if (character === "/") {
1304
+ const regexEndIndex = isRegexLiteralStart(characters, index) ? findRegexLiteralEnd(content, index) : null;
1305
+ if (regexEndIndex !== null) {
1306
+ if (blankStringContents) for (let interiorIndex = index + 1; interiorIndex < regexEndIndex - 1; interiorIndex += 1) blankUnlessNewline(interiorIndex);
1307
+ index = regexEndIndex;
1308
+ continue;
1309
+ }
1310
+ }
1243
1311
  if (templateExpressionDepths.length > 0) {
1244
1312
  const innermost = templateExpressionDepths.length - 1;
1245
1313
  if (character === "{") templateExpressionDepths[innermost] += 1;
@@ -2018,7 +2086,7 @@ const anchorAmbiguousText = defineRule({
2018
2086
  });
2019
2087
  //#endregion
2020
2088
  //#region src/plugin/rules/a11y/anchor-has-content.ts
2021
- const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
2089
+ const MESSAGE$58 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
2022
2090
  const anchorHasContent = defineRule({
2023
2091
  id: "anchor-has-content",
2024
2092
  title: "Anchor has no content",
@@ -2041,7 +2109,7 @@ const anchorHasContent = defineRule({
2041
2109
  ]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
2042
2110
  context.report({
2043
2111
  node: opening.name,
2044
- message: MESSAGE$59
2112
+ message: MESSAGE$58
2045
2113
  });
2046
2114
  } };
2047
2115
  }
@@ -2427,7 +2495,7 @@ const parseJsxValue = (value) => {
2427
2495
  };
2428
2496
  //#endregion
2429
2497
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2430
- const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2498
+ const MESSAGE$57 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2431
2499
  const ariaActivedescendantHasTabindex = defineRule({
2432
2500
  id: "aria-activedescendant-has-tabindex",
2433
2501
  title: "aria-activedescendant missing tabindex",
@@ -2445,14 +2513,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2445
2513
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2446
2514
  context.report({
2447
2515
  node: node.name,
2448
- message: MESSAGE$58
2516
+ message: MESSAGE$57
2449
2517
  });
2450
2518
  return;
2451
2519
  }
2452
2520
  if (isInteractiveElement(tag, node)) return;
2453
2521
  context.report({
2454
2522
  node: node.name,
2455
- message: MESSAGE$58
2523
+ message: MESSAGE$57
2456
2524
  });
2457
2525
  } })
2458
2526
  });
@@ -4557,7 +4625,7 @@ const asyncParallel = defineRule({
4557
4625
  });
4558
4626
  //#endregion
4559
4627
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4560
- 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.";
4628
+ const MESSAGE$56 = "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.";
4561
4629
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4562
4630
  const STORAGE_GLOBALS = new Set([
4563
4631
  "window",
@@ -4598,7 +4666,7 @@ const authTokenInWebStorage = defineRule({
4598
4666
  if (!isAuthCredentialKey(keyArgument.value)) return;
4599
4667
  context.report({
4600
4668
  node,
4601
- message: MESSAGE$57
4669
+ message: MESSAGE$56
4602
4670
  });
4603
4671
  },
4604
4672
  AssignmentExpression(node) {
@@ -4609,7 +4677,7 @@ const authTokenInWebStorage = defineRule({
4609
4677
  if (!propertyName || !isAuthCredentialKey(propertyName)) return;
4610
4678
  context.report({
4611
4679
  node: target,
4612
- message: MESSAGE$57
4680
+ message: MESSAGE$56
4613
4681
  });
4614
4682
  }
4615
4683
  }))
@@ -5099,7 +5167,7 @@ const isPureEventBlockerHandler = (attribute) => {
5099
5167
  };
5100
5168
  //#endregion
5101
5169
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
5102
- const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5170
+ const MESSAGE$55 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5103
5171
  const KEY_HANDLERS = [
5104
5172
  "onKeyUp",
5105
5173
  "onKeyDown",
@@ -5127,7 +5195,7 @@ const clickEventsHaveKeyEvents = defineRule({
5127
5195
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
5128
5196
  context.report({
5129
5197
  node: node.name,
5130
- message: MESSAGE$56
5198
+ message: MESSAGE$55
5131
5199
  });
5132
5200
  } };
5133
5201
  }
@@ -5344,7 +5412,7 @@ const getClassNameLiteral = (classAttribute) => {
5344
5412
  };
5345
5413
  //#endregion
5346
5414
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
5347
- 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`.";
5415
+ const MESSAGE$54 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
5348
5416
  const DISPLAY_NONE_CLASS_TOKEN = "hidden";
5349
5417
  const isDisplayNoneClassToken = (token) => token.toLowerCase() === DISPLAY_NONE_CLASS_TOKEN;
5350
5418
  const collectStaticTemplateClassTokens = (templateLiteral) => {
@@ -5539,7 +5607,7 @@ const controlHasAssociatedLabel = defineRule({
5539
5607
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
5540
5608
  context.report({
5541
5609
  node: opening,
5542
- message: MESSAGE$55
5610
+ message: MESSAGE$54
5543
5611
  });
5544
5612
  } };
5545
5613
  }
@@ -5562,7 +5630,7 @@ const corsCookieTrustRisk = defineRule({
5562
5630
  const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
5563
5631
  const HTML_VALUE_START_PATTERN = /(?:__html\s*:|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
5564
5632
  const HTML_TAINT_PATTERN = /searchParams|query|params|request|req\.|response\.|result\.|data\.|await|fetch|props\.|children|content|html|body|text|message|\blocation\b|document\.cookie|\breferrer\b|\blocalStorage\b|\bsessionStorage\b|URLSearchParams|window\.name/i;
5565
- const STRING_LITERAL_VALUE_PATTERN = /^(?:["'][^"']*["']|`[^`$]*`)\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
5633
+ const STRING_LITERAL_VALUE_PATTERN = /^(?:"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`[^`$]*`)\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
5566
5634
  const MODULE_CONSTANT_VALUE_PATTERN = /^[A-Z][A-Z0-9_]*\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
5567
5635
  const DOM_CONTENT_SOURCE_VALUE_PATTERN = /^[\w$]+(?:\??\.[\w$]+)*\??\.(?:inner|outer)HTML\b/;
5568
5636
  const SANITIZER_PATTERN = /\b(?:DOMPurify|sanitize\w*|purify|(?:escape|encode)[A-Za-z]*(?:Html|HTML|Entit\w*)|insane|xss)\b|(?<!un)safe|(?<!un)saniti[sz]/i;
@@ -5994,7 +6062,7 @@ const noVagueButtonLabel = defineRule({
5994
6062
  });
5995
6063
  //#endregion
5996
6064
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5997
- 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.";
6065
+ const MESSAGE$53 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5998
6066
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5999
6067
  const NAME_PROVIDING_ATTRIBUTES = [
6000
6068
  "aria-label",
@@ -6017,7 +6085,7 @@ const dialogHasAccessibleName = defineRule({
6017
6085
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
6018
6086
  context.report({
6019
6087
  node: node.name,
6020
- message: MESSAGE$54
6088
+ message: MESSAGE$53
6021
6089
  });
6022
6090
  } })
6023
6091
  });
@@ -6056,7 +6124,7 @@ const isEs6Component = (node) => {
6056
6124
  };
6057
6125
  //#endregion
6058
6126
  //#region src/plugin/rules/react-builtins/display-name.ts
6059
- const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
6127
+ const MESSAGE$52 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
6060
6128
  const DEFAULT_ADDITIONAL_HOCS = [
6061
6129
  "observer",
6062
6130
  "lazy",
@@ -6259,7 +6327,7 @@ const displayName = defineRule({
6259
6327
  const reportAt = (node) => {
6260
6328
  context.report({
6261
6329
  node,
6262
- message: MESSAGE$53
6330
+ message: MESSAGE$52
6263
6331
  });
6264
6332
  };
6265
6333
  return {
@@ -8423,7 +8491,7 @@ const forbidElements = defineRule({
8423
8491
  });
8424
8492
  //#endregion
8425
8493
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
8426
- const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
8494
+ const MESSAGE$51 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
8427
8495
  const forwardRefUsesRef = defineRule({
8428
8496
  id: "forward-ref-uses-ref",
8429
8497
  title: "forwardRef without ref parameter",
@@ -8443,7 +8511,7 @@ const forwardRefUsesRef = defineRule({
8443
8511
  if (isNodeOfType(onlyParam, "RestElement")) return;
8444
8512
  context.report({
8445
8513
  node: inner,
8446
- message: MESSAGE$52
8514
+ message: MESSAGE$51
8447
8515
  });
8448
8516
  } })
8449
8517
  });
@@ -8480,7 +8548,7 @@ const gitProviderUrlInjectionRisk = defineRule({
8480
8548
  });
8481
8549
  //#endregion
8482
8550
  //#region src/plugin/rules/a11y/heading-has-content.ts
8483
- 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`.";
8551
+ const MESSAGE$50 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
8484
8552
  const DEFAULT_HEADING_TAGS = [
8485
8553
  "h1",
8486
8554
  "h2",
@@ -8514,7 +8582,7 @@ const headingHasContent = defineRule({
8514
8582
  for (const attribute of ["aria-label", "aria-labelledby"]) if (hasJsxPropIgnoreCase(node.attributes, attribute)) return;
8515
8583
  context.report({
8516
8584
  node,
8517
- message: MESSAGE$51
8585
+ message: MESSAGE$50
8518
8586
  });
8519
8587
  } };
8520
8588
  }
@@ -8652,7 +8720,7 @@ const hooksNoNanInDeps = defineRule({
8652
8720
  });
8653
8721
  //#endregion
8654
8722
  //#region src/plugin/rules/a11y/html-has-lang.ts
8655
- const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8723
+ const MESSAGE$49 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8656
8724
  const resolveSettings$38 = (settings) => {
8657
8725
  const reactDoctor = settings?.["react-doctor"];
8658
8726
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8699,13 +8767,13 @@ const htmlHasLang = defineRule({
8699
8767
  if (!lang) {
8700
8768
  context.report({
8701
8769
  node: node.name,
8702
- message: MESSAGE$50
8770
+ message: MESSAGE$49
8703
8771
  });
8704
8772
  return;
8705
8773
  }
8706
8774
  if (evaluateLang(lang.value) === "empty") context.report({
8707
8775
  node: lang,
8708
- message: MESSAGE$50
8776
+ message: MESSAGE$49
8709
8777
  });
8710
8778
  } };
8711
8779
  }
@@ -8925,7 +8993,7 @@ const htmlNoNestedInteractive = defineRule({
8925
8993
  });
8926
8994
  //#endregion
8927
8995
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8928
- const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8996
+ const MESSAGE$48 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8929
8997
  const evaluateTitleValue = (value) => {
8930
8998
  if (!value) return "missing";
8931
8999
  if (isNodeOfType(value, "Literal")) {
@@ -8965,14 +9033,14 @@ const iframeHasTitle = defineRule({
8965
9033
  if (!titleAttr) {
8966
9034
  if (hasSpread || tag === "iframe") context.report({
8967
9035
  node: node.name,
8968
- message: MESSAGE$49
9036
+ message: MESSAGE$48
8969
9037
  });
8970
9038
  return;
8971
9039
  }
8972
9040
  const verdict = evaluateTitleValue(titleAttr.value);
8973
9041
  if (verdict === "missing" || verdict === "empty") context.report({
8974
9042
  node: titleAttr,
8975
- message: MESSAGE$49
9043
+ message: MESSAGE$48
8976
9044
  });
8977
9045
  } })
8978
9046
  });
@@ -9087,7 +9155,7 @@ const iframeMissingSandbox = defineRule({
9087
9155
  });
9088
9156
  //#endregion
9089
9157
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
9090
- const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
9158
+ const MESSAGE$47 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
9091
9159
  const DEFAULT_COMPONENTS = ["img"];
9092
9160
  const DEFAULT_REDUNDANT_WORDS = [
9093
9161
  "image",
@@ -9152,7 +9220,7 @@ const imgRedundantAlt = defineRule({
9152
9220
  if (!altAttribute) return;
9153
9221
  if (altValueRedundant(altAttribute, settings.words)) context.report({
9154
9222
  node: altAttribute,
9155
- message: MESSAGE$48
9223
+ message: MESSAGE$47
9156
9224
  });
9157
9225
  } };
9158
9226
  }
@@ -9191,7 +9259,7 @@ const DEPRECATED_CIPHER_API_PATTERN = /(?<!cipher\.)\bcreate(?:Cipher|Decipher)\
9191
9259
  const WEAK_CIPHER_ALGORITHM_PATTERN = /\bcreate(?:Cipher|Decipher)iv\s*\(\s*["'](?:des|des3|des-?ede3?|rc4|rc2|bf|blowfish)\b/i;
9192
9260
  const WEAK_CIPHER_NAME_PATTERN = /\b(?:DES|RC4|Blowfish)\b/;
9193
9261
  const CIPHER_CONTEXT_PATTERN = /\b(?:cipher|decipher|encrypt|decrypt|crypto)\b/i;
9194
- const UNSAFE_SIGNATURE_COMPARISON_PATTERN = /[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*(?:\([^)]*\))?|[A-Za-z_$][\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?/i;
9262
+ const UNSAFE_SIGNATURE_COMPARISON_PATTERN = /[A-Za-z_$][\w$.]{0,100}signature[\w$]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*(?:\([^)]*\))?|[A-Za-z_$][\w$.]{0,100}(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]{0,100}signature[\w$]*(?:\([^)]*\))?/i;
9195
9263
  const ENUM_MEMBER_COMPARAND_PATTERN = /(?:===?|!==?)\s*[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))|^[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))[\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)/;
9196
9264
  const SIGNATURE_METADATA_IDENTIFIER_PATTERN = /signature(?:Method|Type|Status|Algorithm|Kind|Mode|Version)\b/i;
9197
9265
  const BOOLEAN_COMPARAND_PATTERN = /(?:===?|!==?)\s*(?:true|false|null|undefined)\b/;
@@ -9231,17 +9299,18 @@ const insecureCryptoRisk = defineRule({
9231
9299
  if (!isProductionSourcePath(file.relativePath)) return [];
9232
9300
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9233
9301
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9234
- let matchIndex = findMatchIndexNearContext(file.content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9235
- if (matchIndex < 0) matchIndex = file.content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
9236
- if (matchIndex < 0) matchIndex = file.content.search(DEPRECATED_CIPHER_API_PATTERN);
9237
- if (matchIndex < 0 && CIPHER_CONTEXT_PATTERN.test(file.content)) matchIndex = file.content.search(WEAK_CIPHER_NAME_PATTERN);
9238
- if (matchIndex < 0 && !TIMING_SAFE_COMPARISON_PATTERN.test(file.content) && !CLIENT_COMPONENT_FILE_PATTERN.test(file.relativePath)) {
9239
- const comparisonMatch = UNSAFE_SIGNATURE_COMPARISON_PATTERN.exec(file.content);
9302
+ const content = getScannableContent(file);
9303
+ let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9304
+ if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
9305
+ if (matchIndex < 0) matchIndex = content.search(DEPRECATED_CIPHER_API_PATTERN);
9306
+ if (matchIndex < 0 && CIPHER_CONTEXT_PATTERN.test(content)) matchIndex = content.search(WEAK_CIPHER_NAME_PATTERN);
9307
+ if (matchIndex < 0 && !TIMING_SAFE_COMPARISON_PATTERN.test(content) && !CLIENT_COMPONENT_FILE_PATTERN.test(file.relativePath)) {
9308
+ const comparisonMatch = UNSAFE_SIGNATURE_COMPARISON_PATTERN.exec(content);
9240
9309
  if (comparisonMatch !== null && !ENUM_MEMBER_COMPARAND_PATTERN.test(comparisonMatch[0]) && !SIGNATURE_METADATA_IDENTIFIER_PATTERN.test(comparisonMatch[0]) && !BOOLEAN_COMPARAND_PATTERN.test(comparisonMatch[0])) matchIndex = comparisonMatch.index;
9241
9310
  }
9242
- if (matchIndex < 0) matchIndex = findRandomCallIndexWithSameLineContext(file.content, MATH_RANDOM_CALL_PATTERN, SECURITY_RANDOM_CONTEXT_PATTERN, UI_NONCE_CONTEXT_PATTERN);
9311
+ if (matchIndex < 0) matchIndex = findRandomCallIndexWithSameLineContext(content, MATH_RANDOM_CALL_PATTERN, SECURITY_RANDOM_CONTEXT_PATTERN, UI_NONCE_CONTEXT_PATTERN);
9243
9312
  if (matchIndex < 0) return [];
9244
- const location = getLocationAtIndex(file.content, matchIndex);
9313
+ const location = getLocationAtIndex(content, matchIndex);
9245
9314
  return [{
9246
9315
  message: "Code uses weak hashes, deprecated ciphers, timing-unsafe comparisons, or Math.random in a security-shaped context.",
9247
9316
  line: location.line,
@@ -12131,7 +12200,7 @@ const jsxMaxDepth = defineRule({
12131
12200
  });
12132
12201
  //#endregion
12133
12202
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
12134
- const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
12203
+ const MESSAGE$46 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
12135
12204
  const LITERAL_TEXT_TAGS = new Set([
12136
12205
  "code",
12137
12206
  "pre",
@@ -12179,7 +12248,7 @@ const jsxNoCommentTextnodes = defineRule({
12179
12248
  if (isInsideLiteralTextTag(node)) return;
12180
12249
  context.report({
12181
12250
  node,
12182
- message: MESSAGE$47
12251
+ message: MESSAGE$46
12183
12252
  });
12184
12253
  } })
12185
12254
  });
@@ -12210,7 +12279,7 @@ const isInsideFunctionScope = (node) => {
12210
12279
  };
12211
12280
  //#endregion
12212
12281
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
12213
- const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
12282
+ const MESSAGE$45 = "Every reader of this context redraws on each render because you build its `value` inline.";
12214
12283
  const CONTEXT_MODULES$1 = [
12215
12284
  "react",
12216
12285
  "use-context-selector",
@@ -12308,7 +12377,7 @@ const jsxNoConstructedContextValues = defineRule({
12308
12377
  if (!isConstructedValue(innerExpression)) continue;
12309
12378
  context.report({
12310
12379
  node: attribute,
12311
- message: MESSAGE$46
12380
+ message: MESSAGE$45
12312
12381
  });
12313
12382
  }
12314
12383
  }
@@ -12392,7 +12461,8 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
12392
12461
  };
12393
12462
  //#endregion
12394
12463
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
12395
- const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
12464
+ const MESSAGE_MEMOISED = "This child redraws every render because the prop gets brand new JSX each time.";
12465
+ const MESSAGE_UNKNOWN = "If this child is memoized, it still redraws every render because the prop gets brand new JSX each time.";
12396
12466
  const KNOWN_SLOT_PROP_NAMES = new Set([
12397
12467
  "icon",
12398
12468
  "Icon",
@@ -12655,7 +12725,8 @@ const jsxNoJsxAsProp = defineRule({
12655
12725
  if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
12656
12726
  const parentJsxOpening = node.parent;
12657
12727
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
12658
- if (memoStatusForJsxOpeningName(memoRegistry, openingName) === "not-memoised") return;
12728
+ const memoStatus = memoStatusForJsxOpeningName(memoRegistry, openingName);
12729
+ if (memoStatus === "not-memoised") return;
12659
12730
  if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
12660
12731
  if (!isInsideFunctionScope(node)) return;
12661
12732
  const value = node.value;
@@ -12666,7 +12737,7 @@ const jsxNoJsxAsProp = defineRule({
12666
12737
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
12667
12738
  context.report({
12668
12739
  node,
12669
- message: MESSAGE$45
12740
+ message: memoStatus === "memoised" ? MESSAGE_MEMOISED : MESSAGE_UNKNOWN
12670
12741
  });
12671
12742
  }
12672
12743
  };
@@ -17946,19 +18017,108 @@ const isArrayFromLengthObjectCall = (node) => {
17946
18017
  }
17947
18018
  return false;
17948
18019
  };
17949
- const isInsideStaticPlaceholderMap = (node) => {
18020
+ const TYPE_RESOLUTION_DEPTH_LIMIT = 4;
18021
+ const isStringKeywordAnnotation = (typeAnnotation) => Boolean(typeAnnotation && isNodeOfType(typeAnnotation, "TSTypeAnnotation") && isNodeOfType(typeAnnotation.typeAnnotation, "TSStringKeyword"));
18022
+ const findSameFileTypeDeclaration = (referenceNode, typeName) => {
18023
+ const programRoot = findProgramRoot(referenceNode);
18024
+ if (!programRoot || !isNodeOfType(programRoot, "Program")) return null;
18025
+ for (const statement of programRoot.body) {
18026
+ const declaration = isNodeOfType(statement, "ExportNamedDeclaration") ? statement.declaration : statement;
18027
+ if (!declaration) continue;
18028
+ if ((isNodeOfType(declaration, "TSInterfaceDeclaration") || isNodeOfType(declaration, "TSTypeAliasDeclaration")) && isNodeOfType(declaration.id, "Identifier") && declaration.id.name === typeName) return declaration;
18029
+ }
18030
+ return null;
18031
+ };
18032
+ const typeDeclaresStringProperty = (typeNode, propertyName, referenceNode, depth) => {
18033
+ if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return false;
18034
+ let members = null;
18035
+ if (isNodeOfType(typeNode, "TSTypeLiteral")) members = typeNode.members;
18036
+ else if (isNodeOfType(typeNode, "TSInterfaceDeclaration")) members = typeNode.body.body;
18037
+ if (members) {
18038
+ for (const member of members) {
18039
+ if (!isNodeOfType(member, "TSPropertySignature")) continue;
18040
+ if (!isNodeOfType(member.key, "Identifier") || member.key.name !== propertyName) continue;
18041
+ return isStringKeywordAnnotation(member.typeAnnotation);
18042
+ }
18043
+ return false;
18044
+ }
18045
+ if (isNodeOfType(typeNode, "TSTypeAliasDeclaration")) return typeDeclaresStringProperty(typeNode.typeAnnotation, propertyName, referenceNode, depth + 1);
18046
+ if (isNodeOfType(typeNode, "TSTypeReference") && isNodeOfType(typeNode.typeName, "Identifier")) {
18047
+ const declaration = findSameFileTypeDeclaration(referenceNode, typeNode.typeName.name);
18048
+ if (!declaration) return false;
18049
+ return typeDeclaresStringProperty(declaration, propertyName, referenceNode, depth + 1);
18050
+ }
18051
+ return false;
18052
+ };
18053
+ const isDestructuredFromStringTypedPattern = (bindingIdentifier) => {
18054
+ const property = bindingIdentifier.parent;
18055
+ if (!property || !isNodeOfType(property, "Property")) return false;
18056
+ if (!isNodeOfType(property.key, "Identifier")) return false;
18057
+ const pattern = property.parent;
18058
+ if (!pattern || !isNodeOfType(pattern, "ObjectPattern")) return false;
18059
+ const typeAnnotation = pattern.typeAnnotation;
18060
+ if (!typeAnnotation || !isNodeOfType(typeAnnotation, "TSTypeAnnotation")) return false;
18061
+ return typeDeclaresStringProperty(typeAnnotation.typeAnnotation, property.key.name, bindingIdentifier, 0);
18062
+ };
18063
+ const isProvablyStringValued = (expression, depth) => {
18064
+ if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return false;
18065
+ const node = stripParenExpression(expression);
18066
+ if (isNodeOfType(node, "Literal")) return typeof node.value === "string";
18067
+ if (isNodeOfType(node, "TemplateLiteral")) return true;
18068
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "String") return true;
18069
+ if (isNodeOfType(node, "Identifier")) {
18070
+ const binding = findVariableInitializer(node, node.name);
18071
+ if (!binding) return false;
18072
+ if (binding.initializer && isProvablyStringValued(binding.initializer, depth + 1)) return true;
18073
+ if (isNodeOfType(binding.bindingIdentifier, "Identifier") && isStringKeywordAnnotation(binding.bindingIdentifier.typeAnnotation)) return true;
18074
+ return isDestructuredFromStringTypedPattern(binding.bindingIdentifier);
18075
+ }
18076
+ return false;
18077
+ };
18078
+ const hasProvablyStringFirstArgument = (callNode) => {
18079
+ if (!isNodeOfType(callNode, "CallExpression")) return false;
18080
+ const source = callNode.arguments?.[0];
18081
+ return Boolean(source && isProvablyStringValued(source, 0));
18082
+ };
18083
+ /**
18084
+ * `[...str]`, `str.split(...)`, and `Array.from(str)` slice ONE string
18085
+ * into positional fragments (characters, lines, tokens). Fragment
18086
+ * position IS the entry's stable identity — nothing reorders, filters,
18087
+ * or carries per-item state — so an index key is correct there.
18088
+ * `.split(...)` needs no string proof (only strings have `.split`);
18089
+ * the spread / `Array.from` forms do, because both are equally common
18090
+ * on arrays.
18091
+ */
18092
+ const isStringDerivedReceiver = (receiver, depth = 0) => {
18093
+ const node = stripParenExpression(receiver);
18094
+ if (isNodeOfType(node, "Identifier")) {
18095
+ if (depth >= TYPE_RESOLUTION_DEPTH_LIMIT) return false;
18096
+ const binding = findVariableInitializer(node, node.name);
18097
+ if (!binding?.initializer) return false;
18098
+ return isStringDerivedReceiver(binding.initializer, depth + 1);
18099
+ }
18100
+ if (isNodeOfType(node, "ArrayExpression") && node.elements?.length === 1) {
18101
+ const only = node.elements[0];
18102
+ if (only && isNodeOfType(only, "SpreadElement")) return isProvablyStringValued(only.argument, 0);
18103
+ }
18104
+ if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "split") return true;
18105
+ return isArrayFromCall(node) && hasProvablyStringFirstArgument(node);
18106
+ };
18107
+ const isInsideIteratorMapMatching = (node, matchesMethodReceiver, matchesArrayFromCall) => {
17950
18108
  let current = node;
17951
18109
  while (current.parent) {
17952
18110
  const parent = current.parent;
17953
18111
  if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
17954
18112
  const callee = parent.callee;
17955
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
17956
- if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
18113
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return matchesMethodReceiver(callee.object);
18114
+ if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return matchesArrayFromCall(parent);
17957
18115
  }
17958
18116
  current = parent;
17959
18117
  }
17960
18118
  return false;
17961
18119
  };
18120
+ const isInsideStringDerivedMap = (node) => isInsideIteratorMapMatching(node, isStringDerivedReceiver, hasProvablyStringFirstArgument);
18121
+ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node, isStaticPlaceholderReceiver, isArrayFromLengthObjectCall);
17962
18122
  /**
17963
18123
  * Walk up from a JSXAttribute node looking for the enclosing iterator
17964
18124
  * callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
@@ -18014,6 +18174,7 @@ const noArrayIndexAsKey = defineRule({
18014
18174
  const indexName = extractIndexName(node.value.expression);
18015
18175
  if (!indexName) return;
18016
18176
  if (isInsideStaticPlaceholderMap(node)) return;
18177
+ if (isInsideStringDerivedMap(node)) return;
18017
18178
  if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
18018
18179
  const openingElement = node.parent;
18019
18180
  if (openingElement && isNodeOfType(openingElement, "JSXOpeningElement")) {
@@ -19851,6 +20012,25 @@ const countSetterCallSites = (ref) => {
19851
20012
  }
19852
20013
  return count;
19853
20014
  };
20015
+ const collectUpdaterParameterReads = (analysis, updaterFn) => {
20016
+ const reads = [];
20017
+ for (const ref of getDownstreamRefs(analysis, updaterFn.body)) if (ref.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updaterFn)) reads.push(ref.identifier);
20018
+ return reads;
20019
+ };
20020
+ const readsParameterOnlyViaObjectSpread = (parameterReads) => parameterReads.every((read) => {
20021
+ const spread = read.parent;
20022
+ if (!spread || !isNodeOfType(spread, "SpreadElement")) return false;
20023
+ const container = spread.parent;
20024
+ return Boolean(container && isNodeOfType(container, "ObjectExpression"));
20025
+ });
20026
+ const isAccumulatingFunctionalUpdater = (analysis, callExpr) => {
20027
+ if (!isNodeOfType(callExpr, "CallExpression")) return false;
20028
+ const firstArgument = callExpr.arguments?.[0];
20029
+ if (!firstArgument || !isFunctionLike$1(firstArgument)) return false;
20030
+ const parameterReads = collectUpdaterParameterReads(analysis, firstArgument);
20031
+ if (parameterReads.length === 0) return false;
20032
+ return !readsParameterOnlyViaObjectSpread(parameterReads);
20033
+ };
19854
20034
  const getStateNameForUseStateDecl = (useStateNode) => {
19855
20035
  if (!useStateNode || !isNodeOfType(useStateNode, "VariableDeclarator")) return null;
19856
20036
  if (!isNodeOfType(useStateNode.id, "ArrayPattern")) return null;
@@ -19885,6 +20065,7 @@ const noDerivedState = defineRule({
19885
20065
  const depsUpstreamRefs = depsRefs.flatMap((depRef) => getUpstreamRefs(analysis, depRef));
19886
20066
  if (isInitialOnlySetterCall(callExpr)) continue;
19887
20067
  if (isControlledPropMirror(node, callExpr)) continue;
20068
+ if (isAccumulatingFunctionalUpdater(analysis, callExpr)) continue;
19888
20069
  const isSomeArgsInternal = argsUpstreamRefs.some((argRef) => isState(analysis, argRef) || isProp(analysis, argRef));
19889
20070
  const isValueAlwaysInSync = argsUpstreamRefs.length > 0 && argsUpstreamRefs.every((argRef) => depsUpstreamRefs.some((depRef) => argRef.resolved === depRef.resolved)) && countSetterCallSites(ref) === 1;
19890
20071
  if (isSomeArgsInternal) context.report({
@@ -30874,35 +31055,67 @@ const preferUseSyncExternalStore = defineRule({
30874
31055
  });
30875
31056
  //#endregion
30876
31057
  //#region src/plugin/rules/state-and-effects/prefer-use-reducer.ts
31058
+ const getSetterNameFromStatement = (statement, setterNames) => {
31059
+ let expression = null;
31060
+ if (isNodeOfType(statement, "ExpressionStatement")) expression = statement.expression;
31061
+ else if (isNodeOfType(statement, "ReturnStatement")) expression = statement.argument;
31062
+ if (!expression) return null;
31063
+ const call = stripParenExpression(expression);
31064
+ if (!isNodeOfType(call, "CallExpression")) return null;
31065
+ if (!isNodeOfType(call.callee, "Identifier")) return null;
31066
+ return setterNames.has(call.callee.name) ? call.callee.name : null;
31067
+ };
31068
+ const findLargestCoUpdatedSetterGroup = (componentBody, setterNames) => {
31069
+ let largestGroupSize = 0;
31070
+ const visit = (node, isInsideNestedFunction) => {
31071
+ if (isNodeOfType(node, "BlockStatement") && isInsideNestedFunction) {
31072
+ const groupSetterNames = /* @__PURE__ */ new Set();
31073
+ for (const statement of node.body ?? []) {
31074
+ const setterName = getSetterNameFromStatement(statement, setterNames);
31075
+ if (setterName) groupSetterNames.add(setterName);
31076
+ }
31077
+ largestGroupSize = Math.max(largestGroupSize, groupSetterNames.size);
31078
+ }
31079
+ const enteringNestedFunction = isInsideNestedFunction || isFunctionLike$1(node);
31080
+ const record = node;
31081
+ for (const key of Object.keys(record)) {
31082
+ if (key === "parent" || key === "type") continue;
31083
+ const child = record[key];
31084
+ if (Array.isArray(child)) {
31085
+ for (const item of child) if (isAstNode(item)) visit(item, enteringNestedFunction);
31086
+ } else if (isAstNode(child)) visit(child, enteringNestedFunction);
31087
+ }
31088
+ };
31089
+ visit(componentBody, false);
31090
+ return largestGroupSize;
31091
+ };
30877
31092
  const preferUseReducer = defineRule({
30878
31093
  id: "prefer-useReducer",
30879
31094
  title: "Many related useState calls",
30880
31095
  tags: ["test-noise"],
30881
31096
  severity: "warn",
30882
- recommendation: "Group related state in `useReducer` so one logical update does not fan out into separate renders.",
31097
+ recommendation: "Group state that always changes together into `useReducer` so one dispatched action describes the whole transition instead of a list of setter calls.",
30883
31098
  create: (context) => {
30884
- const reportExcessiveUseState = (body, componentName) => {
31099
+ const reportCoUpdatedUseState = (body, componentName) => {
30885
31100
  if (!isNodeOfType(body, "BlockStatement")) return;
30886
- let useStateCount = 0;
30887
- for (const statement of body.body ?? []) {
30888
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
30889
- for (const declarator of statement.declarations ?? []) if (declarator.init && isHookCall$1(declarator.init, "useState")) useStateCount++;
30890
- }
30891
- if (useStateCount >= 5) context.report({
31101
+ const setterNames = new Set(collectUseStateBindings(body).map((binding) => binding.setterName));
31102
+ if (setterNames.size < 5) return;
31103
+ const coUpdatedCount = findLargestCoUpdatedSetterGroup(body, setterNames);
31104
+ if (coUpdatedCount >= 5) context.report({
30892
31105
  node: body,
30893
- message: `${useStateCount} useState calls in "${componentName}" can each trigger a separate render.`
31106
+ message: `"${componentName}" updates ${coUpdatedCount} separate useState values in one place state that changes together is easier to keep consistent as a single useReducer action.`
30894
31107
  });
30895
31108
  };
30896
31109
  return {
30897
31110
  FunctionDeclaration(node) {
30898
31111
  if (!node.id?.name || !isUppercaseName(node.id.name)) return;
30899
- reportExcessiveUseState(node.body, node.id.name);
31112
+ reportCoUpdatedUseState(node.body, node.id.name);
30900
31113
  },
30901
31114
  VariableDeclarator(node) {
30902
31115
  if (!isComponentAssignment(node)) return;
30903
31116
  if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
30904
31117
  if (!isNodeOfType(node.id, "Identifier")) return;
30905
- reportExcessiveUseState(node.init.body, node.id.name);
31118
+ reportCoUpdatedUseState(node.init.body, node.id.name);
30906
31119
  }
30907
31120
  };
30908
31121
  }
@@ -34169,6 +34382,17 @@ const collectReturnedJsxRoots = (functionNode) => {
34169
34382
  return roots;
34170
34383
  };
34171
34384
  const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
34385
+ const fragmentChildrenOrNull = (child) => {
34386
+ if (isNodeOfType(child, "JSXFragment")) return child.children ?? [];
34387
+ if (isNodeOfType(child, "JSXElement") && isJsxFragmentElement(child.openingElement)) return child.children ?? [];
34388
+ return null;
34389
+ };
34390
+ const isChildrenForwardingJsxChildThroughFragments = (child, bindings) => {
34391
+ if (isChildrenForwardingJsxChild(child, bindings)) return true;
34392
+ const fragmentChildren = fragmentChildrenOrNull(child);
34393
+ if (fragmentChildren === null) return false;
34394
+ return fragmentChildren.some((innerChild) => isChildrenForwardingJsxChildThroughFragments(innerChild, bindings));
34395
+ };
34172
34396
  const isChildrenForwardingAttribute = (attribute, bindings) => {
34173
34397
  if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
34174
34398
  return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
@@ -34180,7 +34404,7 @@ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElemen
34180
34404
  if (!isNodeOfType(node, "JSXElement")) return void 0;
34181
34405
  const elementName = resolveJsxElementName(node.openingElement);
34182
34406
  if (!elementName || !isTextHandlingElement(elementName)) return;
34183
- didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
34407
+ didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChildThroughFragments(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
34184
34408
  });
34185
34409
  return didForwardIntoText;
34186
34410
  };
@@ -34198,7 +34422,7 @@ const jsxRootForwardsChildren = (jsxRoot, bindings, isTextHandlingElement, count
34198
34422
  return;
34199
34423
  }
34200
34424
  }
34201
- if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings))) didForward = true;
34425
+ if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChildThroughFragments(child, bindings))) didForward = true;
34202
34426
  });
34203
34427
  return didForward;
34204
34428
  };
@@ -34276,14 +34500,13 @@ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandl
34276
34500
  if (kind === "text") wrappers.add(componentName);
34277
34501
  else if (kind === "nonText") nonTextWrappers.add(componentName);
34278
34502
  };
34279
- const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
34280
34503
  const collectTextWrapperComponents = (programNode, isTextHandlingRoot, isNonTextHostRoot) => {
34281
34504
  const wrappers = /* @__PURE__ */ new Set();
34282
34505
  const nonTextWrappers = /* @__PURE__ */ new Set();
34283
34506
  const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
34284
34507
  const isNonTextHostElement = (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName);
34285
34508
  const recordDeclaration = (componentName, definitionNode) => recordWrapperFromDeclaration(componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers);
34286
- for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
34509
+ while (true) {
34287
34510
  const wrappersSizeBeforePass = wrappers.size;
34288
34511
  const nonTextSizeBeforePass = nonTextWrappers.size;
34289
34512
  walkAst(programNode, (node) => {
@@ -38965,7 +39188,7 @@ const isAuthGuardName = (calleeName) => {
38965
39188
  //#endregion
38966
39189
  //#region src/plugin/utils/is-non-privileged-server-action.ts
38967
39190
  const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
38968
- const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name);
39191
+ const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name) && getImportSourceForName(node, node.callee.name) !== null;
38969
39192
  const unwrapExpression = (node) => {
38970
39193
  let current = node;
38971
39194
  while (current) {
@@ -38990,6 +39213,7 @@ const isDataExposingValue = (node) => {
38990
39213
  const isLiteralOnlyExpression = (node) => {
38991
39214
  if (!node) return false;
38992
39215
  if (isNodeOfType(node, "Literal")) return true;
39216
+ if (isNodeOfType(node, "Identifier")) return node.name === "undefined";
38993
39217
  if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every(isLiteralOnlyExpression);
38994
39218
  if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression(node.argument);
38995
39219
  if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => element === null || !isNodeOfType(element, "SpreadElement") && isLiteralOnlyExpression(element));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.8-dev.80e3093",
3
+ "version": "0.5.8-dev.8232e96",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",