oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.f45cb29

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 +294 -0
  2. package/dist/index.js +436 -140
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1861,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
1861
1861
  });
1862
1862
  //#endregion
1863
1863
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1864
- const MESSAGE$51 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
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`.";
1865
1865
  const anchorHasContent = defineRule({
1866
1866
  id: "anchor-has-content",
1867
1867
  title: "Anchor has no content",
@@ -1877,7 +1877,7 @@ const anchorHasContent = defineRule({
1877
1877
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1878
1878
  context.report({
1879
1879
  node: opening.name,
1880
- message: MESSAGE$51
1880
+ message: MESSAGE$57
1881
1881
  });
1882
1882
  } })
1883
1883
  });
@@ -2271,7 +2271,7 @@ const parseJsxValue = (value) => {
2271
2271
  };
2272
2272
  //#endregion
2273
2273
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2274
- const MESSAGE$50 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2274
+ const MESSAGE$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2275
2275
  const ariaActivedescendantHasTabindex = defineRule({
2276
2276
  id: "aria-activedescendant-has-tabindex",
2277
2277
  title: "aria-activedescendant missing tabindex",
@@ -2289,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2289
2289
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2290
2290
  context.report({
2291
2291
  node: node.name,
2292
- message: MESSAGE$50
2292
+ message: MESSAGE$56
2293
2293
  });
2294
2294
  return;
2295
2295
  }
2296
2296
  if (isInteractiveElement(tag, node)) return;
2297
2297
  context.report({
2298
2298
  node: node.name,
2299
- message: MESSAGE$50
2299
+ message: MESSAGE$56
2300
2300
  });
2301
2301
  } })
2302
2302
  });
@@ -4201,6 +4201,58 @@ const asyncParallel = defineRule({
4201
4201
  }
4202
4202
  });
4203
4203
  //#endregion
4204
+ //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4205
+ 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.";
4206
+ const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4207
+ const STORAGE_GLOBALS = new Set([
4208
+ "window",
4209
+ "globalThis",
4210
+ "self"
4211
+ ]);
4212
+ const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
4213
+ const isWebStorageObject = (node) => {
4214
+ if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
4215
+ if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
4216
+ return false;
4217
+ };
4218
+ const staticMemberName = (member) => {
4219
+ if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
4220
+ if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
4221
+ return null;
4222
+ };
4223
+ const authTokenInWebStorage = defineRule({
4224
+ id: "auth-token-in-web-storage",
4225
+ title: "Auth token in web storage",
4226
+ severity: "warn",
4227
+ recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
4228
+ create: (context) => ({
4229
+ CallExpression(node) {
4230
+ const callee = node.callee;
4231
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
4232
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
4233
+ if (!isWebStorageObject(callee.object)) return;
4234
+ const keyArgument = node.arguments?.[0];
4235
+ if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
4236
+ if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4237
+ context.report({
4238
+ node,
4239
+ message: MESSAGE$55
4240
+ });
4241
+ },
4242
+ AssignmentExpression(node) {
4243
+ const target = node.left;
4244
+ if (!isNodeOfType(target, "MemberExpression")) return;
4245
+ if (!isWebStorageObject(target.object)) return;
4246
+ const propertyName = staticMemberName(target);
4247
+ if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4248
+ context.report({
4249
+ node: target,
4250
+ message: MESSAGE$55
4251
+ });
4252
+ }
4253
+ })
4254
+ });
4255
+ //#endregion
4204
4256
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
4205
4257
  const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
4206
4258
  const AUTOFILL_TOKENS = new Set([
@@ -4572,7 +4624,7 @@ const isPureEventBlockerHandler = (attribute) => {
4572
4624
  //#endregion
4573
4625
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4574
4626
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4575
- const MESSAGE$49 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4627
+ const MESSAGE$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4576
4628
  const KEY_HANDLERS = [
4577
4629
  "onKeyUp",
4578
4630
  "onKeyDown",
@@ -4604,7 +4656,7 @@ const clickEventsHaveKeyEvents = defineRule({
4604
4656
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4605
4657
  context.report({
4606
4658
  node: node.name,
4607
- message: MESSAGE$49
4659
+ message: MESSAGE$54
4608
4660
  });
4609
4661
  } };
4610
4662
  }
@@ -4719,7 +4771,7 @@ const isReactComponentName = (name) => {
4719
4771
  };
4720
4772
  //#endregion
4721
4773
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4722
- const MESSAGE$48 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4774
+ 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`.";
4723
4775
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4724
4776
  const DEFAULT_LABELLING_PROPS = [
4725
4777
  "alt",
@@ -4880,7 +4932,7 @@ const controlHasAssociatedLabel = defineRule({
4880
4932
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4881
4933
  context.report({
4882
4934
  node: opening,
4883
- message: MESSAGE$48
4935
+ message: MESSAGE$53
4884
4936
  });
4885
4937
  } };
4886
4938
  }
@@ -5306,6 +5358,38 @@ const noVagueButtonLabel = defineRule({
5306
5358
  } })
5307
5359
  });
5308
5360
  //#endregion
5361
+ //#region src/plugin/utils/has-jsx-spread-attribute.ts
5362
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5363
+ //#endregion
5364
+ //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5365
+ 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.";
5366
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5367
+ const NAME_PROVIDING_ATTRIBUTES = [
5368
+ "aria-label",
5369
+ "aria-labelledby",
5370
+ "title"
5371
+ ];
5372
+ const dialogHasAccessibleName = defineRule({
5373
+ id: "dialog-has-accessible-name",
5374
+ title: "Dialog without accessible name",
5375
+ severity: "warn",
5376
+ recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
5377
+ create: (context) => ({ JSXOpeningElement(node) {
5378
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
5379
+ const tagName = node.name.name;
5380
+ if (tagName[0] !== tagName[0]?.toLowerCase()) return;
5381
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5382
+ const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5383
+ if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5384
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5385
+ if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5386
+ context.report({
5387
+ node: node.name,
5388
+ message: MESSAGE$52
5389
+ });
5390
+ } })
5391
+ });
5392
+ //#endregion
5309
5393
  //#region src/plugin/utils/is-es5-component.ts
5310
5394
  const PRAGMA$2 = "React";
5311
5395
  const CREATE_CLASS = "createReactClass";
@@ -5340,7 +5424,7 @@ const isEs6Component = (node) => {
5340
5424
  };
5341
5425
  //#endregion
5342
5426
  //#region src/plugin/rules/react-builtins/display-name.ts
5343
- const MESSAGE$47 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5427
+ const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5344
5428
  const DEFAULT_ADDITIONAL_HOCS = [
5345
5429
  "observer",
5346
5430
  "lazy",
@@ -5543,7 +5627,7 @@ const displayName = defineRule({
5543
5627
  const reportAt = (node) => {
5544
5628
  context.report({
5545
5629
  node,
5546
- message: MESSAGE$47
5630
+ message: MESSAGE$51
5547
5631
  });
5548
5632
  };
5549
5633
  return {
@@ -7691,7 +7775,7 @@ const forbidElements = defineRule({
7691
7775
  });
7692
7776
  //#endregion
7693
7777
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7694
- const MESSAGE$46 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7778
+ const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7695
7779
  const forwardRefUsesRef = defineRule({
7696
7780
  id: "forward-ref-uses-ref",
7697
7781
  title: "forwardRef without ref parameter",
@@ -7711,7 +7795,7 @@ const forwardRefUsesRef = defineRule({
7711
7795
  if (isNodeOfType(onlyParam, "RestElement")) return;
7712
7796
  context.report({
7713
7797
  node: inner,
7714
- message: MESSAGE$46
7798
+ message: MESSAGE$50
7715
7799
  });
7716
7800
  } })
7717
7801
  });
@@ -7748,7 +7832,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7748
7832
  });
7749
7833
  //#endregion
7750
7834
  //#region src/plugin/rules/a11y/heading-has-content.ts
7751
- const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7835
+ 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`.";
7752
7836
  const DEFAULT_HEADING_TAGS = [
7753
7837
  "h1",
7754
7838
  "h2",
@@ -7781,7 +7865,7 @@ const headingHasContent = defineRule({
7781
7865
  if (isHiddenFromScreenReader(node, context.settings)) return;
7782
7866
  context.report({
7783
7867
  node,
7784
- message: MESSAGE$45
7868
+ message: MESSAGE$49
7785
7869
  });
7786
7870
  } };
7787
7871
  }
@@ -7919,7 +8003,7 @@ const hooksNoNanInDeps = defineRule({
7919
8003
  });
7920
8004
  //#endregion
7921
8005
  //#region src/plugin/rules/a11y/html-has-lang.ts
7922
- const MESSAGE$44 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8006
+ const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
7923
8007
  const resolveSettings$38 = (settings) => {
7924
8008
  const reactDoctor = settings?.["react-doctor"];
7925
8009
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -7967,7 +8051,7 @@ const htmlHasLang = defineRule({
7967
8051
  if (!lang) {
7968
8052
  context.report({
7969
8053
  node: node.name,
7970
- message: MESSAGE$44
8054
+ message: MESSAGE$48
7971
8055
  });
7972
8056
  return;
7973
8057
  }
@@ -7975,13 +8059,13 @@ const htmlHasLang = defineRule({
7975
8059
  if (verdict === "missing" || verdict === "empty") {
7976
8060
  context.report({
7977
8061
  node: lang,
7978
- message: MESSAGE$44
8062
+ message: MESSAGE$48
7979
8063
  });
7980
8064
  return;
7981
8065
  }
7982
8066
  if (hasSpread && !lang) context.report({
7983
8067
  node: node.name,
7984
- message: MESSAGE$44
8068
+ message: MESSAGE$48
7985
8069
  });
7986
8070
  } };
7987
8071
  }
@@ -8195,7 +8279,7 @@ const htmlNoNestedInteractive = defineRule({
8195
8279
  });
8196
8280
  //#endregion
8197
8281
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8198
- const MESSAGE$43 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8282
+ const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8199
8283
  const evaluateTitleValue = (value) => {
8200
8284
  if (!value) return "missing";
8201
8285
  if (isNodeOfType(value, "Literal")) {
@@ -8235,14 +8319,14 @@ const iframeHasTitle = defineRule({
8235
8319
  if (!titleAttr) {
8236
8320
  if (hasSpread || tag === "iframe") context.report({
8237
8321
  node: node.name,
8238
- message: MESSAGE$43
8322
+ message: MESSAGE$47
8239
8323
  });
8240
8324
  return;
8241
8325
  }
8242
8326
  const verdict = evaluateTitleValue(titleAttr.value);
8243
8327
  if (verdict === "missing" || verdict === "empty") context.report({
8244
8328
  node: titleAttr,
8245
- message: MESSAGE$43
8329
+ message: MESSAGE$47
8246
8330
  });
8247
8331
  } })
8248
8332
  });
@@ -8346,7 +8430,7 @@ const iframeMissingSandbox = defineRule({
8346
8430
  });
8347
8431
  //#endregion
8348
8432
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8349
- const MESSAGE$42 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8433
+ const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8350
8434
  const DEFAULT_COMPONENTS = ["img"];
8351
8435
  const DEFAULT_REDUNDANT_WORDS = [
8352
8436
  "image",
@@ -8411,7 +8495,7 @@ const imgRedundantAlt = defineRule({
8411
8495
  if (!altAttribute) return;
8412
8496
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8413
8497
  node: altAttribute,
8414
- message: MESSAGE$42
8498
+ message: MESSAGE$46
8415
8499
  });
8416
8500
  } };
8417
8501
  }
@@ -10768,7 +10852,7 @@ const jsxMaxDepth = defineRule({
10768
10852
  });
10769
10853
  //#endregion
10770
10854
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10771
- const MESSAGE$41 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10855
+ const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10772
10856
  const LITERAL_TEXT_TAGS = new Set([
10773
10857
  "code",
10774
10858
  "pre",
@@ -10804,7 +10888,7 @@ const jsxNoCommentTextnodes = defineRule({
10804
10888
  if (isInsideLiteralTextTag(node)) return;
10805
10889
  context.report({
10806
10890
  node,
10807
- message: MESSAGE$41
10891
+ message: MESSAGE$45
10808
10892
  });
10809
10893
  } })
10810
10894
  });
@@ -10835,7 +10919,7 @@ const isInsideFunctionScope = (node) => {
10835
10919
  };
10836
10920
  //#endregion
10837
10921
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10838
- const MESSAGE$40 = "Every reader of this context redraws on each render because you build its `value` inline.";
10922
+ const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
10839
10923
  const CONTEXT_MODULES$1 = [
10840
10924
  "react",
10841
10925
  "use-context-selector",
@@ -10933,7 +11017,7 @@ const jsxNoConstructedContextValues = defineRule({
10933
11017
  if (!isConstructedValue(innerExpression)) continue;
10934
11018
  context.report({
10935
11019
  node: attribute,
10936
- message: MESSAGE$40
11020
+ message: MESSAGE$44
10937
11021
  });
10938
11022
  }
10939
11023
  }
@@ -11019,7 +11103,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11019
11103
  };
11020
11104
  //#endregion
11021
11105
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11022
- const MESSAGE$39 = "This child redraws every render because the prop gets brand new JSX each time.";
11106
+ const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
11023
11107
  const KNOWN_SLOT_PROP_NAMES = new Set([
11024
11108
  "icon",
11025
11109
  "Icon",
@@ -11288,7 +11372,7 @@ const jsxNoJsxAsProp = defineRule({
11288
11372
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11289
11373
  context.report({
11290
11374
  node,
11291
- message: MESSAGE$39
11375
+ message: MESSAGE$43
11292
11376
  });
11293
11377
  }
11294
11378
  };
@@ -11576,7 +11660,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11576
11660
  ];
11577
11661
  //#endregion
11578
11662
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11579
- const MESSAGE$38 = "This child redraws every render because the prop gets a brand new array each time.";
11663
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
11580
11664
  const isDataArrayPropName = (propName) => {
11581
11665
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11582
11666
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11660,7 +11744,7 @@ const jsxNoNewArrayAsProp = defineRule({
11660
11744
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11661
11745
  context.report({
11662
11746
  node,
11663
- message: MESSAGE$38
11747
+ message: MESSAGE$42
11664
11748
  });
11665
11749
  }
11666
11750
  };
@@ -11918,7 +12002,7 @@ const SAFE_RECEIVER_NAMES = new Set([
11918
12002
  ]);
11919
12003
  //#endregion
11920
12004
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
11921
- const MESSAGE$37 = "This child redraws every render because the prop gets a brand new function each time.";
12005
+ const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
11922
12006
  const isAccessorPredicateName = (propName) => {
11923
12007
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
11924
12008
  if (propName.length <= prefix.length) continue;
@@ -12124,7 +12208,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12124
12208
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12125
12209
  context.report({
12126
12210
  node,
12127
- message: MESSAGE$37
12211
+ message: MESSAGE$41
12128
12212
  });
12129
12213
  }
12130
12214
  };
@@ -12344,7 +12428,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12344
12428
  ];
12345
12429
  //#endregion
12346
12430
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12347
- const MESSAGE$36 = "This child redraws every render because the prop gets a brand new object each time.";
12431
+ const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
12348
12432
  const isConfigObjectPropName = (propName) => {
12349
12433
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12350
12434
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12432,7 +12516,7 @@ const jsxNoNewObjectAsProp = defineRule({
12432
12516
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12433
12517
  context.report({
12434
12518
  node,
12435
- message: MESSAGE$36
12519
+ message: MESSAGE$40
12436
12520
  });
12437
12521
  }
12438
12522
  };
@@ -12440,7 +12524,7 @@ const jsxNoNewObjectAsProp = defineRule({
12440
12524
  });
12441
12525
  //#endregion
12442
12526
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12443
- const MESSAGE$35 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12527
+ const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12444
12528
  const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
12445
12529
  const resolveSettings$28 = (settings) => {
12446
12530
  const reactDoctor = settings?.["react-doctor"];
@@ -12481,7 +12565,7 @@ const jsxNoScriptUrl = defineRule({
12481
12565
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12482
12566
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12483
12567
  node: attribute,
12484
- message: MESSAGE$35
12568
+ message: MESSAGE$39
12485
12569
  });
12486
12570
  }
12487
12571
  } };
@@ -12796,7 +12880,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12796
12880
  });
12797
12881
  //#endregion
12798
12882
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12799
- const MESSAGE$34 = "You can't tell what props reach this element when you spread them.";
12883
+ const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
12800
12884
  const resolveSettings$25 = (settings) => {
12801
12885
  const reactDoctor = settings?.["react-doctor"];
12802
12886
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12837,7 +12921,7 @@ const jsxPropsNoSpreading = defineRule({
12837
12921
  }
12838
12922
  context.report({
12839
12923
  node: attribute,
12840
- message: MESSAGE$34
12924
+ message: MESSAGE$38
12841
12925
  });
12842
12926
  }
12843
12927
  } };
@@ -13065,7 +13149,7 @@ const labelHasAssociatedControl = defineRule({
13065
13149
  });
13066
13150
  //#endregion
13067
13151
  //#region src/plugin/rules/a11y/lang.ts
13068
- const MESSAGE$33 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13152
+ 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`.";
13069
13153
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13070
13154
  "aa",
13071
13155
  "ab",
@@ -13277,7 +13361,7 @@ const lang = defineRule({
13277
13361
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13278
13362
  context.report({
13279
13363
  node: langAttr,
13280
- message: MESSAGE$33
13364
+ message: MESSAGE$37
13281
13365
  });
13282
13366
  return;
13283
13367
  }
@@ -13286,7 +13370,7 @@ const lang = defineRule({
13286
13370
  if (value === null) return;
13287
13371
  if (!isValidLangTag(value)) context.report({
13288
13372
  node: langAttr,
13289
- message: MESSAGE$33
13373
+ message: MESSAGE$37
13290
13374
  });
13291
13375
  } })
13292
13376
  });
@@ -13330,7 +13414,7 @@ const mdxSsrExecutionRisk = defineRule({
13330
13414
  });
13331
13415
  //#endregion
13332
13416
  //#region src/plugin/rules/a11y/media-has-caption.ts
13333
- const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13417
+ const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13334
13418
  const DEFAULT_AUDIO = ["audio"];
13335
13419
  const DEFAULT_VIDEO = ["video"];
13336
13420
  const DEFAULT_TRACK = ["track"];
@@ -13371,7 +13455,7 @@ const mediaHasCaption = defineRule({
13371
13455
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13372
13456
  context.report({
13373
13457
  node: node.name,
13374
- message: MESSAGE$32
13458
+ message: MESSAGE$36
13375
13459
  });
13376
13460
  return;
13377
13461
  }
@@ -13388,7 +13472,7 @@ const mediaHasCaption = defineRule({
13388
13472
  return kindValue.value.toLowerCase() === "captions";
13389
13473
  })) context.report({
13390
13474
  node: node.name,
13391
- message: MESSAGE$32
13475
+ message: MESSAGE$36
13392
13476
  });
13393
13477
  } };
13394
13478
  }
@@ -15189,7 +15273,7 @@ const nextjsNoVercelOgImport = defineRule({
15189
15273
  });
15190
15274
  //#endregion
15191
15275
  //#region src/plugin/rules/a11y/no-access-key.ts
15192
- const MESSAGE$31 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15276
+ const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15193
15277
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15194
15278
  const noAccessKey = defineRule({
15195
15279
  id: "no-access-key",
@@ -15206,7 +15290,7 @@ const noAccessKey = defineRule({
15206
15290
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15207
15291
  context.report({
15208
15292
  node: accessKey,
15209
- message: MESSAGE$31
15293
+ message: MESSAGE$35
15210
15294
  });
15211
15295
  return;
15212
15296
  }
@@ -15216,7 +15300,7 @@ const noAccessKey = defineRule({
15216
15300
  if (isUndefinedIdentifier(expression)) return;
15217
15301
  context.report({
15218
15302
  node: accessKey,
15219
- message: MESSAGE$31
15303
+ message: MESSAGE$35
15220
15304
  });
15221
15305
  }
15222
15306
  } })
@@ -15699,7 +15783,7 @@ const noAdjustStateOnPropChange = defineRule({
15699
15783
  });
15700
15784
  //#endregion
15701
15785
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15702
- const MESSAGE$30 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15786
+ 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.";
15703
15787
  const noAriaHiddenOnFocusable = defineRule({
15704
15788
  id: "no-aria-hidden-on-focusable",
15705
15789
  title: "aria-hidden on focusable element",
@@ -15726,7 +15810,7 @@ const noAriaHiddenOnFocusable = defineRule({
15726
15810
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15727
15811
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15728
15812
  node: ariaHidden,
15729
- message: MESSAGE$30
15813
+ message: MESSAGE$34
15730
15814
  });
15731
15815
  } })
15732
15816
  });
@@ -16094,7 +16178,7 @@ const noArrayIndexAsKey = defineRule({
16094
16178
  });
16095
16179
  //#endregion
16096
16180
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16097
- const MESSAGE$29 = "Your users can see & submit the wrong data when this list reorders.";
16181
+ const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
16098
16182
  const SECOND_INDEX_METHODS = new Set([
16099
16183
  "every",
16100
16184
  "filter",
@@ -16298,7 +16382,7 @@ const noArrayIndexKey = defineRule({
16298
16382
  }
16299
16383
  context.report({
16300
16384
  node: keyAttribute,
16301
- message: MESSAGE$29
16385
+ message: MESSAGE$33
16302
16386
  });
16303
16387
  },
16304
16388
  CallExpression(node) {
@@ -16318,15 +16402,35 @@ const noArrayIndexKey = defineRule({
16318
16402
  if (propName !== "key") continue;
16319
16403
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16320
16404
  node: property,
16321
- message: MESSAGE$29
16405
+ message: MESSAGE$33
16322
16406
  });
16323
16407
  }
16324
16408
  }
16325
16409
  })
16326
16410
  });
16327
16411
  //#endregion
16412
+ //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16413
+ 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.";
16414
+ const noAsyncEffectCallback = defineRule({
16415
+ id: "no-async-effect-callback",
16416
+ title: "Async effect callback",
16417
+ severity: "warn",
16418
+ recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
16419
+ create: (context) => ({ CallExpression(node) {
16420
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
16421
+ const callback = getEffectCallback(node);
16422
+ if (!callback) return;
16423
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
16424
+ if (!callback.async) return;
16425
+ context.report({
16426
+ node: callback,
16427
+ message: MESSAGE$32
16428
+ });
16429
+ } })
16430
+ });
16431
+ //#endregion
16328
16432
  //#region src/plugin/rules/a11y/no-autofocus.ts
16329
- const MESSAGE$28 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16433
+ 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.";
16330
16434
  const resolveSettings$21 = (settings) => {
16331
16435
  const reactDoctor = settings?.["react-doctor"];
16332
16436
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16382,7 +16486,7 @@ const noAutofocus = defineRule({
16382
16486
  }
16383
16487
  context.report({
16384
16488
  node: autoFocusAttribute,
16385
- message: MESSAGE$28
16489
+ message: MESSAGE$31
16386
16490
  });
16387
16491
  } };
16388
16492
  }
@@ -16632,6 +16736,109 @@ const noBarrelImport = defineRule({
16632
16736
  }
16633
16737
  });
16634
16738
  //#endregion
16739
+ //#region src/plugin/utils/function-contains-react-render-output.ts
16740
+ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
16741
+ "FunctionDeclaration",
16742
+ "FunctionExpression",
16743
+ "ArrowFunctionExpression",
16744
+ "ClassDeclaration",
16745
+ "ClassExpression"
16746
+ ]);
16747
+ const isReactImport$1 = (symbol) => {
16748
+ let importDeclaration = symbol.declarationNode?.parent;
16749
+ while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
16750
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
16751
+ return importDeclaration.source.value === "react";
16752
+ };
16753
+ const getImportedName = (symbol) => {
16754
+ if (symbol.kind !== "import") return null;
16755
+ if (!isReactImport$1(symbol)) return null;
16756
+ return getImportedName$1(symbol.declarationNode) ?? null;
16757
+ };
16758
+ const isReactNamespaceImport = (symbol) => {
16759
+ if (symbol.kind !== "import") return false;
16760
+ if (!isReactImport$1(symbol)) return false;
16761
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
16762
+ };
16763
+ const isReactCreateElementIdentifierCall = (callee, scopes) => {
16764
+ if (!isNodeOfType(callee, "Identifier")) return false;
16765
+ const symbol = scopes.symbolFor(callee);
16766
+ return Boolean(symbol && getImportedName(symbol) === "createElement");
16767
+ };
16768
+ const isReactCreateElementMemberCall = (callee, scopes) => {
16769
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
16770
+ if (callee.computed) return false;
16771
+ if (!isNodeOfType(callee.object, "Identifier")) return false;
16772
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
16773
+ if (callee.property.name !== "createElement") return false;
16774
+ const symbol = scopes.symbolFor(callee.object);
16775
+ return Boolean(symbol && isReactNamespaceImport(symbol));
16776
+ };
16777
+ const isReactCreateElementCall = (node, scopes) => {
16778
+ if (!isNodeOfType(node, "CallExpression")) return false;
16779
+ return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
16780
+ };
16781
+ const containsRenderOutput = (node, rootNode, scopes) => {
16782
+ if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
16783
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
16784
+ if (isReactCreateElementCall(node, scopes)) return true;
16785
+ const nodeRecord = node;
16786
+ for (const key of Object.keys(nodeRecord)) {
16787
+ if (key === "parent") continue;
16788
+ const child = nodeRecord[key];
16789
+ if (Array.isArray(child)) {
16790
+ for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
16791
+ } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
16792
+ }
16793
+ return false;
16794
+ };
16795
+ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
16796
+ //#endregion
16797
+ //#region src/plugin/utils/is-component-declaration.ts
16798
+ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
16799
+ //#endregion
16800
+ //#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
16801
+ const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
16802
+ const symbolIsLocalComponent = (symbol, context) => {
16803
+ const declaration = symbol.declarationNode;
16804
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
16805
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
16806
+ return false;
16807
+ };
16808
+ const noCallComponentAsFunction = defineRule({
16809
+ id: "no-call-component-as-function",
16810
+ title: "Component called as a function",
16811
+ severity: "warn",
16812
+ tags: ["test-noise"],
16813
+ recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
16814
+ create: (context) => {
16815
+ const renderedJsxNames = /* @__PURE__ */ new Set();
16816
+ const candidateCalls = [];
16817
+ return {
16818
+ JSXOpeningElement(node) {
16819
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
16820
+ },
16821
+ CallExpression(node) {
16822
+ if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
16823
+ node,
16824
+ callee: node.callee,
16825
+ name: node.callee.name
16826
+ });
16827
+ },
16828
+ "Program:exit"() {
16829
+ for (const candidate of candidateCalls) {
16830
+ const symbol = context.scopes.symbolFor(candidate.callee);
16831
+ if (!symbol) continue;
16832
+ if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
16833
+ node: candidate.node,
16834
+ message: message(candidate.name)
16835
+ });
16836
+ }
16837
+ }
16838
+ };
16839
+ }
16840
+ });
16841
+ //#endregion
16635
16842
  //#region src/plugin/utils/is-setter-identifier.ts
16636
16843
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
16637
16844
  //#endregion
@@ -16783,7 +16990,7 @@ const noChainStateUpdates = defineRule({
16783
16990
  });
16784
16991
  //#endregion
16785
16992
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
16786
- const MESSAGE$27 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16993
+ const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16787
16994
  const noChildrenProp = defineRule({
16788
16995
  id: "no-children-prop",
16789
16996
  title: "Children passed as a prop",
@@ -16795,7 +17002,7 @@ const noChildrenProp = defineRule({
16795
17002
  if (node.name.name !== "children") return;
16796
17003
  context.report({
16797
17004
  node: node.name,
16798
- message: MESSAGE$27
17005
+ message: MESSAGE$30
16799
17006
  });
16800
17007
  },
16801
17008
  CallExpression(node) {
@@ -16808,7 +17015,7 @@ const noChildrenProp = defineRule({
16808
17015
  const propertyKey = property.key;
16809
17016
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
16810
17017
  node: propertyKey,
16811
- message: MESSAGE$27
17018
+ message: MESSAGE$30
16812
17019
  });
16813
17020
  }
16814
17021
  }
@@ -16816,7 +17023,7 @@ const noChildrenProp = defineRule({
16816
17023
  });
16817
17024
  //#endregion
16818
17025
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
16819
- const MESSAGE$26 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17026
+ const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
16820
17027
  const noCloneElement = defineRule({
16821
17028
  id: "no-clone-element",
16822
17029
  title: "cloneElement makes child props fragile",
@@ -16829,7 +17036,7 @@ const noCloneElement = defineRule({
16829
17036
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
16830
17037
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
16831
17038
  node: callee,
16832
- message: MESSAGE$26
17039
+ message: MESSAGE$29
16833
17040
  });
16834
17041
  return;
16835
17042
  }
@@ -16842,7 +17049,7 @@ const noCloneElement = defineRule({
16842
17049
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
16843
17050
  context.report({
16844
17051
  node: callee,
16845
- message: MESSAGE$26
17052
+ message: MESSAGE$29
16846
17053
  });
16847
17054
  }
16848
17055
  } })
@@ -16891,7 +17098,7 @@ const enclosingComponentOrHookName = (node) => {
16891
17098
  };
16892
17099
  //#endregion
16893
17100
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
16894
- const MESSAGE$25 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17101
+ const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
16895
17102
  const CONTEXT_MODULES = [
16896
17103
  "react",
16897
17104
  "use-context-selector",
@@ -16927,7 +17134,32 @@ const noCreateContextInRender = defineRule({
16927
17134
  if (!componentOrHookName) return;
16928
17135
  context.report({
16929
17136
  node,
16930
- message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
17137
+ message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
17138
+ });
17139
+ } })
17140
+ });
17141
+ //#endregion
17142
+ //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17143
+ 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.";
17144
+ const noCreateRefInFunctionComponent = defineRule({
17145
+ id: "no-create-ref-in-function-component",
17146
+ title: "createRef in function component",
17147
+ severity: "warn",
17148
+ recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
17149
+ create: (context) => ({ CallExpression(node) {
17150
+ if (!isReactFunctionCall(node, "createRef")) return;
17151
+ if (isNodeOfType(node.callee, "Identifier")) {
17152
+ const symbol = context.scopes.symbolFor(node.callee);
17153
+ if (symbol && symbol.kind !== "import") return;
17154
+ }
17155
+ const enclosingFunction = nearestEnclosingFunction(node);
17156
+ if (!enclosingFunction) return;
17157
+ const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
17158
+ if (!displayName) return;
17159
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17160
+ context.report({
17161
+ node,
17162
+ message: MESSAGE$27
16931
17163
  });
16932
17164
  } })
16933
17165
  });
@@ -17067,7 +17299,7 @@ const noCreateStoreInRender = defineRule({
17067
17299
  });
17068
17300
  //#endregion
17069
17301
  //#region src/plugin/rules/react-builtins/no-danger.ts
17070
- const MESSAGE$24 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17302
+ const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17071
17303
  const noDanger = defineRule({
17072
17304
  id: "no-danger",
17073
17305
  title: "Raw HTML injection can run unsafe markup",
@@ -17080,7 +17312,7 @@ const noDanger = defineRule({
17080
17312
  if (!propAttribute) return;
17081
17313
  context.report({
17082
17314
  node: propAttribute.name,
17083
- message: MESSAGE$24
17315
+ message: MESSAGE$26
17084
17316
  });
17085
17317
  },
17086
17318
  CallExpression(node) {
@@ -17092,7 +17324,7 @@ const noDanger = defineRule({
17092
17324
  const propertyKey = property.key;
17093
17325
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17094
17326
  node: propertyKey,
17095
- message: MESSAGE$24
17327
+ message: MESSAGE$26
17096
17328
  });
17097
17329
  }
17098
17330
  }
@@ -17100,7 +17332,7 @@ const noDanger = defineRule({
17100
17332
  });
17101
17333
  //#endregion
17102
17334
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17103
- const MESSAGE$23 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17335
+ const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17104
17336
  const isLineBreak = (child) => {
17105
17337
  if (!isNodeOfType(child, "JSXText")) return false;
17106
17338
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17170,7 +17402,7 @@ const noDangerWithChildren = defineRule({
17170
17402
  if (!hasChildrenProp && !hasNestedChildren) return;
17171
17403
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17172
17404
  node: opening,
17173
- message: MESSAGE$23
17405
+ message: MESSAGE$25
17174
17406
  });
17175
17407
  },
17176
17408
  CallExpression(node) {
@@ -17182,7 +17414,7 @@ const noDangerWithChildren = defineRule({
17182
17414
  if (!propsShape.hasDangerously) return;
17183
17415
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17184
17416
  node,
17185
- message: MESSAGE$23
17417
+ message: MESSAGE$25
17186
17418
  });
17187
17419
  }
17188
17420
  })
@@ -17759,7 +17991,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17759
17991
  //#endregion
17760
17992
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17761
17993
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
17762
- const MESSAGE$22 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17994
+ const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17763
17995
  const resolveSettings$20 = (settings) => {
17764
17996
  const reactDoctor = settings?.["react-doctor"];
17765
17997
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -17778,7 +18010,7 @@ const noDidMountSetState = defineRule({
17778
18010
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17779
18011
  context.report({
17780
18012
  node: node.callee,
17781
- message: MESSAGE$22
18013
+ message: MESSAGE$24
17782
18014
  });
17783
18015
  } };
17784
18016
  }
@@ -17786,7 +18018,7 @@ const noDidMountSetState = defineRule({
17786
18018
  //#endregion
17787
18019
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
17788
18020
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
17789
- const MESSAGE$21 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18021
+ const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
17790
18022
  const resolveSettings$19 = (settings) => {
17791
18023
  const reactDoctor = settings?.["react-doctor"];
17792
18024
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -17805,7 +18037,7 @@ const noDidUpdateSetState = defineRule({
17805
18037
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17806
18038
  context.report({
17807
18039
  node: node.callee,
17808
- message: MESSAGE$21
18040
+ message: MESSAGE$23
17809
18041
  });
17810
18042
  } };
17811
18043
  }
@@ -17828,7 +18060,7 @@ const isStateMemberExpression = (node) => {
17828
18060
  };
17829
18061
  //#endregion
17830
18062
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
17831
- const MESSAGE$20 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18063
+ const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
17832
18064
  const shouldIgnoreMutation = (node) => {
17833
18065
  let isConstructor = false;
17834
18066
  let isInsideCallExpression = false;
@@ -17850,7 +18082,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
17850
18082
  if (shouldIgnoreMutation(reportNode)) return;
17851
18083
  context.report({
17852
18084
  node: reportNode,
17853
- message: MESSAGE$20
18085
+ message: MESSAGE$22
17854
18086
  });
17855
18087
  };
17856
18088
  const noDirectMutationState = defineRule({
@@ -19438,7 +19670,7 @@ const ALLOWED_NAMESPACES = new Set([
19438
19670
  "ReactDOM",
19439
19671
  "ReactDom"
19440
19672
  ]);
19441
- const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19673
+ const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19442
19674
  const noFindDomNode = defineRule({
19443
19675
  id: "no-find-dom-node",
19444
19676
  title: "findDOMNode breaks component encapsulation",
@@ -19449,7 +19681,7 @@ const noFindDomNode = defineRule({
19449
19681
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19450
19682
  context.report({
19451
19683
  node: callee,
19452
- message: MESSAGE$19
19684
+ message: MESSAGE$21
19453
19685
  });
19454
19686
  return;
19455
19687
  }
@@ -19460,7 +19692,7 @@ const noFindDomNode = defineRule({
19460
19692
  if (callee.property.name !== "findDOMNode") return;
19461
19693
  context.report({
19462
19694
  node: callee.property,
19463
- message: MESSAGE$19
19695
+ message: MESSAGE$21
19464
19696
  });
19465
19697
  }
19466
19698
  } })
@@ -19523,64 +19755,6 @@ const noGenericHandlerNames = defineRule({
19523
19755
  } })
19524
19756
  });
19525
19757
  //#endregion
19526
- //#region src/plugin/utils/function-contains-react-render-output.ts
19527
- const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
19528
- "FunctionDeclaration",
19529
- "FunctionExpression",
19530
- "ArrowFunctionExpression",
19531
- "ClassDeclaration",
19532
- "ClassExpression"
19533
- ]);
19534
- const isReactImport$1 = (symbol) => {
19535
- let importDeclaration = symbol.declarationNode?.parent;
19536
- while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
19537
- if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
19538
- return importDeclaration.source.value === "react";
19539
- };
19540
- const getImportedName = (symbol) => {
19541
- if (symbol.kind !== "import") return null;
19542
- if (!isReactImport$1(symbol)) return null;
19543
- return getImportedName$1(symbol.declarationNode) ?? null;
19544
- };
19545
- const isReactNamespaceImport = (symbol) => {
19546
- if (symbol.kind !== "import") return false;
19547
- if (!isReactImport$1(symbol)) return false;
19548
- return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
19549
- };
19550
- const isReactCreateElementIdentifierCall = (callee, scopes) => {
19551
- if (!isNodeOfType(callee, "Identifier")) return false;
19552
- const symbol = scopes.symbolFor(callee);
19553
- return Boolean(symbol && getImportedName(symbol) === "createElement");
19554
- };
19555
- const isReactCreateElementMemberCall = (callee, scopes) => {
19556
- if (!isNodeOfType(callee, "MemberExpression")) return false;
19557
- if (callee.computed) return false;
19558
- if (!isNodeOfType(callee.object, "Identifier")) return false;
19559
- if (!isNodeOfType(callee.property, "Identifier")) return false;
19560
- if (callee.property.name !== "createElement") return false;
19561
- const symbol = scopes.symbolFor(callee.object);
19562
- return Boolean(symbol && isReactNamespaceImport(symbol));
19563
- };
19564
- const isReactCreateElementCall = (node, scopes) => {
19565
- if (!isNodeOfType(node, "CallExpression")) return false;
19566
- return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
19567
- };
19568
- const containsRenderOutput = (node, rootNode, scopes) => {
19569
- if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
19570
- if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
19571
- if (isReactCreateElementCall(node, scopes)) return true;
19572
- const nodeRecord = node;
19573
- for (const key of Object.keys(nodeRecord)) {
19574
- if (key === "parent") continue;
19575
- const child = nodeRecord[key];
19576
- if (Array.isArray(child)) {
19577
- for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
19578
- } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
19579
- }
19580
- return false;
19581
- };
19582
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
19583
- //#endregion
19584
19758
  //#region src/plugin/rules/architecture/no-giant-component.ts
19585
19759
  const noGiantComponent = defineRule({
19586
19760
  id: "no-giant-component",
@@ -19759,6 +19933,26 @@ const noGrayOnColoredBackground = defineRule({
19759
19933
  } })
19760
19934
  });
19761
19935
  //#endregion
19936
+ //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
19937
+ 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.";
19938
+ const noImgLazyWithHighFetchpriority = defineRule({
19939
+ id: "no-img-lazy-with-high-fetchpriority",
19940
+ title: "Lazy image with high fetchPriority",
19941
+ severity: "warn",
19942
+ recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
19943
+ create: (context) => ({ JSXOpeningElement(node) {
19944
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
19945
+ const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
19946
+ if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
19947
+ const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
19948
+ if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
19949
+ context.report({
19950
+ node: node.name,
19951
+ message: MESSAGE$20
19952
+ });
19953
+ } })
19954
+ });
19955
+ //#endregion
19762
19956
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
19763
19957
  const noInitializeState = defineRule({
19764
19958
  id: "no-initialize-state",
@@ -19988,6 +20182,29 @@ const noIsMounted = defineRule({
19988
20182
  } })
19989
20183
  });
19990
20184
  //#endregion
20185
+ //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20186
+ 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)`.";
20187
+ const isJsonMethodCall = (node, method) => {
20188
+ if (!isNodeOfType(node, "CallExpression")) return false;
20189
+ const callee = node.callee;
20190
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
20191
+ };
20192
+ const noJsonParseStringifyClone = defineRule({
20193
+ id: "no-json-parse-stringify-clone",
20194
+ title: "JSON parse/stringify deep clone",
20195
+ severity: "warn",
20196
+ recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
20197
+ create: (context) => ({ CallExpression(node) {
20198
+ if (!isJsonMethodCall(node, "parse")) return;
20199
+ const firstArgument = node.arguments?.[0];
20200
+ if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20201
+ context.report({
20202
+ node,
20203
+ message: MESSAGE$19
20204
+ });
20205
+ } })
20206
+ });
20207
+ //#endregion
19991
20208
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
19992
20209
  const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
19993
20210
  const isJsxElementTypeReference = (node) => {
@@ -20310,9 +20527,6 @@ const noLongTransitionDuration = defineRule({
20310
20527
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20311
20528
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
20312
20529
  //#endregion
20313
- //#region src/plugin/utils/is-component-declaration.ts
20314
- const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
20315
- //#endregion
20316
20530
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
20317
20531
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
20318
20532
  const found = /* @__PURE__ */ new Set();
@@ -37144,6 +37358,17 @@ const reactDoctorRules = [
37144
37358
  category: "Performance"
37145
37359
  }
37146
37360
  },
37361
+ {
37362
+ key: "react-doctor/auth-token-in-web-storage",
37363
+ id: "auth-token-in-web-storage",
37364
+ source: "react-doctor",
37365
+ originallyExternal: false,
37366
+ rule: {
37367
+ ...authTokenInWebStorage,
37368
+ framework: "global",
37369
+ category: "Security"
37370
+ }
37371
+ },
37147
37372
  {
37148
37373
  key: "react-doctor/autocomplete-valid",
37149
37374
  id: "autocomplete-valid",
@@ -37360,6 +37585,18 @@ const reactDoctorRules = [
37360
37585
  requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
37361
37586
  }
37362
37587
  },
37588
+ {
37589
+ key: "react-doctor/dialog-has-accessible-name",
37590
+ id: "dialog-has-accessible-name",
37591
+ source: "react-doctor",
37592
+ originallyExternal: false,
37593
+ rule: {
37594
+ ...dialogHasAccessibleName,
37595
+ framework: "global",
37596
+ category: "Accessibility",
37597
+ requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
37598
+ }
37599
+ },
37363
37600
  {
37364
37601
  key: "react-doctor/display-name",
37365
37602
  id: "display-name",
@@ -38519,6 +38756,18 @@ const reactDoctorRules = [
38519
38756
  requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
38520
38757
  }
38521
38758
  },
38759
+ {
38760
+ key: "react-doctor/no-async-effect-callback",
38761
+ id: "no-async-effect-callback",
38762
+ source: "react-doctor",
38763
+ originallyExternal: false,
38764
+ rule: {
38765
+ ...noAsyncEffectCallback,
38766
+ framework: "global",
38767
+ category: "Bugs",
38768
+ requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
38769
+ }
38770
+ },
38522
38771
  {
38523
38772
  key: "react-doctor/no-autofocus",
38524
38773
  id: "no-autofocus",
@@ -38542,6 +38791,18 @@ const reactDoctorRules = [
38542
38791
  category: "Performance"
38543
38792
  }
38544
38793
  },
38794
+ {
38795
+ key: "react-doctor/no-call-component-as-function",
38796
+ id: "no-call-component-as-function",
38797
+ source: "react-doctor",
38798
+ originallyExternal: false,
38799
+ rule: {
38800
+ ...noCallComponentAsFunction,
38801
+ framework: "global",
38802
+ category: "Bugs",
38803
+ requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
38804
+ }
38805
+ },
38545
38806
  {
38546
38807
  key: "react-doctor/no-cascading-set-state",
38547
38808
  id: "no-cascading-set-state",
@@ -38602,6 +38863,18 @@ const reactDoctorRules = [
38602
38863
  requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
38603
38864
  }
38604
38865
  },
38866
+ {
38867
+ key: "react-doctor/no-create-ref-in-function-component",
38868
+ id: "no-create-ref-in-function-component",
38869
+ source: "react-doctor",
38870
+ originallyExternal: false,
38871
+ rule: {
38872
+ ...noCreateRefInFunctionComponent,
38873
+ framework: "global",
38874
+ category: "Bugs",
38875
+ requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
38876
+ }
38877
+ },
38605
38878
  {
38606
38879
  key: "react-doctor/no-create-store-in-render",
38607
38880
  id: "no-create-store-in-render",
@@ -38976,6 +39249,18 @@ const reactDoctorRules = [
38976
39249
  category: "Accessibility"
38977
39250
  }
38978
39251
  },
39252
+ {
39253
+ key: "react-doctor/no-img-lazy-with-high-fetchpriority",
39254
+ id: "no-img-lazy-with-high-fetchpriority",
39255
+ source: "react-doctor",
39256
+ originallyExternal: false,
39257
+ rule: {
39258
+ ...noImgLazyWithHighFetchpriority,
39259
+ framework: "global",
39260
+ category: "Performance",
39261
+ requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
39262
+ }
39263
+ },
38979
39264
  {
38980
39265
  key: "react-doctor/no-initialize-state",
38981
39266
  id: "no-initialize-state",
@@ -39046,6 +39331,17 @@ const reactDoctorRules = [
39046
39331
  requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
39047
39332
  }
39048
39333
  },
39334
+ {
39335
+ key: "react-doctor/no-json-parse-stringify-clone",
39336
+ id: "no-json-parse-stringify-clone",
39337
+ source: "react-doctor",
39338
+ originallyExternal: false,
39339
+ rule: {
39340
+ ...noJsonParseStringifyClone,
39341
+ framework: "global",
39342
+ category: "Performance"
39343
+ }
39344
+ },
39049
39345
  {
39050
39346
  key: "react-doctor/no-jsx-element-type",
39051
39347
  id: "no-jsx-element-type",