oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.451beeb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +420 -0
  2. package/dist/index.js +705 -166
  3. package/package.json +2 -2
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$59 = "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$59
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$58 = "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$58
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$58
2300
2300
  });
2301
2301
  } })
2302
2302
  });
@@ -3104,6 +3104,76 @@ const AUTH_FUNCTION_NAMES = new Set([
3104
3104
  "getAuth",
3105
3105
  "validateSession"
3106
3106
  ]);
3107
+ const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
3108
+ const AUTH_STANDALONE_NOUN_TOKENS = new Set([
3109
+ "signedin",
3110
+ "loggedin",
3111
+ "signin"
3112
+ ]);
3113
+ const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
3114
+ "require",
3115
+ "ensure",
3116
+ "assert",
3117
+ "verify",
3118
+ "validate",
3119
+ "check",
3120
+ "protect",
3121
+ "enforce",
3122
+ "guard",
3123
+ "gate",
3124
+ "restrict",
3125
+ "is",
3126
+ "has",
3127
+ "can",
3128
+ "must"
3129
+ ]);
3130
+ const AUTH_GETTER_VERB_TOKENS = new Set([
3131
+ "get",
3132
+ "fetch",
3133
+ "load",
3134
+ "read",
3135
+ "resolve",
3136
+ "retrieve",
3137
+ "use"
3138
+ ]);
3139
+ const AUTH_QUALIFIER_TOKENS = new Set([
3140
+ "current",
3141
+ "my",
3142
+ "own"
3143
+ ]);
3144
+ const AUTH_STRONG_NOUN_TOKENS = new Set([
3145
+ "session",
3146
+ "sessions",
3147
+ "login",
3148
+ "admin",
3149
+ "admins",
3150
+ "superadmin",
3151
+ "superuser",
3152
+ "role",
3153
+ "roles",
3154
+ "permission",
3155
+ "permissions",
3156
+ "jwt",
3157
+ "identity",
3158
+ "principal",
3159
+ "credential",
3160
+ "credentials"
3161
+ ]);
3162
+ const AUTH_WEAK_NOUN_TOKENS = new Set([
3163
+ "user",
3164
+ "users",
3165
+ "account",
3166
+ "accounts",
3167
+ "token",
3168
+ "tokens",
3169
+ "access",
3170
+ "me",
3171
+ "viewer",
3172
+ "caller",
3173
+ "subject",
3174
+ "scope",
3175
+ "scopes"
3176
+ ]);
3107
3177
  const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
3108
3178
  const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
3109
3179
  const SECRET_PATTERNS = [
@@ -4201,6 +4271,58 @@ const asyncParallel = defineRule({
4201
4271
  }
4202
4272
  });
4203
4273
  //#endregion
4274
+ //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4275
+ const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4276
+ const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4277
+ const STORAGE_GLOBALS = new Set([
4278
+ "window",
4279
+ "globalThis",
4280
+ "self"
4281
+ ]);
4282
+ const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
4283
+ const isWebStorageObject = (node) => {
4284
+ if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
4285
+ if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
4286
+ return false;
4287
+ };
4288
+ const staticMemberName = (member) => {
4289
+ if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
4290
+ if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
4291
+ return null;
4292
+ };
4293
+ const authTokenInWebStorage = defineRule({
4294
+ id: "auth-token-in-web-storage",
4295
+ title: "Auth token in web storage",
4296
+ severity: "warn",
4297
+ recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
4298
+ create: (context) => ({
4299
+ CallExpression(node) {
4300
+ const callee = node.callee;
4301
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
4302
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
4303
+ if (!isWebStorageObject(callee.object)) return;
4304
+ const keyArgument = node.arguments?.[0];
4305
+ if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
4306
+ if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4307
+ context.report({
4308
+ node,
4309
+ message: MESSAGE$57
4310
+ });
4311
+ },
4312
+ AssignmentExpression(node) {
4313
+ const target = node.left;
4314
+ if (!isNodeOfType(target, "MemberExpression")) return;
4315
+ if (!isWebStorageObject(target.object)) return;
4316
+ const propertyName = staticMemberName(target);
4317
+ if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4318
+ context.report({
4319
+ node: target,
4320
+ message: MESSAGE$57
4321
+ });
4322
+ }
4323
+ })
4324
+ });
4325
+ //#endregion
4204
4326
  //#region src/plugin/rules/a11y/autocomplete-valid.ts
4205
4327
  const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
4206
4328
  const AUTOFILL_TOKENS = new Set([
@@ -4572,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
4572
4694
  //#endregion
4573
4695
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4574
4696
  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`.";
4697
+ const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4576
4698
  const KEY_HANDLERS = [
4577
4699
  "onKeyUp",
4578
4700
  "onKeyDown",
@@ -4604,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
4604
4726
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4605
4727
  context.report({
4606
4728
  node: node.name,
4607
- message: MESSAGE$49
4729
+ message: MESSAGE$56
4608
4730
  });
4609
4731
  } };
4610
4732
  }
@@ -4719,7 +4841,7 @@ const isReactComponentName = (name) => {
4719
4841
  };
4720
4842
  //#endregion
4721
4843
  //#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`.";
4844
+ const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4723
4845
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4724
4846
  const DEFAULT_LABELLING_PROPS = [
4725
4847
  "alt",
@@ -4880,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
4880
5002
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4881
5003
  context.report({
4882
5004
  node: opening,
4883
- message: MESSAGE$48
5005
+ message: MESSAGE$55
4884
5006
  });
4885
5007
  } };
4886
5008
  }
@@ -5306,6 +5428,38 @@ const noVagueButtonLabel = defineRule({
5306
5428
  } })
5307
5429
  });
5308
5430
  //#endregion
5431
+ //#region src/plugin/utils/has-jsx-spread-attribute.ts
5432
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
+ //#endregion
5434
+ //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5435
+ const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5436
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5437
+ const NAME_PROVIDING_ATTRIBUTES = [
5438
+ "aria-label",
5439
+ "aria-labelledby",
5440
+ "title"
5441
+ ];
5442
+ const dialogHasAccessibleName = defineRule({
5443
+ id: "dialog-has-accessible-name",
5444
+ title: "Dialog without accessible name",
5445
+ severity: "warn",
5446
+ recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
5447
+ create: (context) => ({ JSXOpeningElement(node) {
5448
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
5449
+ const tagName = node.name.name;
5450
+ if (tagName[0] !== tagName[0]?.toLowerCase()) return;
5451
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5452
+ const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5453
+ if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5454
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5455
+ if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5456
+ context.report({
5457
+ node: node.name,
5458
+ message: MESSAGE$54
5459
+ });
5460
+ } })
5461
+ });
5462
+ //#endregion
5309
5463
  //#region src/plugin/utils/is-es5-component.ts
5310
5464
  const PRAGMA$2 = "React";
5311
5465
  const CREATE_CLASS = "createReactClass";
@@ -5340,7 +5494,7 @@ const isEs6Component = (node) => {
5340
5494
  };
5341
5495
  //#endregion
5342
5496
  //#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`.";
5497
+ const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5344
5498
  const DEFAULT_ADDITIONAL_HOCS = [
5345
5499
  "observer",
5346
5500
  "lazy",
@@ -5543,7 +5697,7 @@ const displayName = defineRule({
5543
5697
  const reportAt = (node) => {
5544
5698
  context.report({
5545
5699
  node,
5546
- message: MESSAGE$47
5700
+ message: MESSAGE$53
5547
5701
  });
5548
5702
  };
5549
5703
  return {
@@ -7691,7 +7845,7 @@ const forbidElements = defineRule({
7691
7845
  });
7692
7846
  //#endregion
7693
7847
  //#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`.";
7848
+ const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7695
7849
  const forwardRefUsesRef = defineRule({
7696
7850
  id: "forward-ref-uses-ref",
7697
7851
  title: "forwardRef without ref parameter",
@@ -7711,7 +7865,7 @@ const forwardRefUsesRef = defineRule({
7711
7865
  if (isNodeOfType(onlyParam, "RestElement")) return;
7712
7866
  context.report({
7713
7867
  node: inner,
7714
- message: MESSAGE$46
7868
+ message: MESSAGE$52
7715
7869
  });
7716
7870
  } })
7717
7871
  });
@@ -7748,7 +7902,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7748
7902
  });
7749
7903
  //#endregion
7750
7904
  //#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`.";
7905
+ const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7752
7906
  const DEFAULT_HEADING_TAGS = [
7753
7907
  "h1",
7754
7908
  "h2",
@@ -7781,7 +7935,7 @@ const headingHasContent = defineRule({
7781
7935
  if (isHiddenFromScreenReader(node, context.settings)) return;
7782
7936
  context.report({
7783
7937
  node,
7784
- message: MESSAGE$45
7938
+ message: MESSAGE$51
7785
7939
  });
7786
7940
  } };
7787
7941
  }
@@ -7919,7 +8073,7 @@ const hooksNoNanInDeps = defineRule({
7919
8073
  });
7920
8074
  //#endregion
7921
8075
  //#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`.";
8076
+ const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
7923
8077
  const resolveSettings$38 = (settings) => {
7924
8078
  const reactDoctor = settings?.["react-doctor"];
7925
8079
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -7967,7 +8121,7 @@ const htmlHasLang = defineRule({
7967
8121
  if (!lang) {
7968
8122
  context.report({
7969
8123
  node: node.name,
7970
- message: MESSAGE$44
8124
+ message: MESSAGE$50
7971
8125
  });
7972
8126
  return;
7973
8127
  }
@@ -7975,13 +8129,13 @@ const htmlHasLang = defineRule({
7975
8129
  if (verdict === "missing" || verdict === "empty") {
7976
8130
  context.report({
7977
8131
  node: lang,
7978
- message: MESSAGE$44
8132
+ message: MESSAGE$50
7979
8133
  });
7980
8134
  return;
7981
8135
  }
7982
8136
  if (hasSpread && !lang) context.report({
7983
8137
  node: node.name,
7984
- message: MESSAGE$44
8138
+ message: MESSAGE$50
7985
8139
  });
7986
8140
  } };
7987
8141
  }
@@ -8195,7 +8349,7 @@ const htmlNoNestedInteractive = defineRule({
8195
8349
  });
8196
8350
  //#endregion
8197
8351
  //#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.";
8352
+ const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8199
8353
  const evaluateTitleValue = (value) => {
8200
8354
  if (!value) return "missing";
8201
8355
  if (isNodeOfType(value, "Literal")) {
@@ -8235,14 +8389,14 @@ const iframeHasTitle = defineRule({
8235
8389
  if (!titleAttr) {
8236
8390
  if (hasSpread || tag === "iframe") context.report({
8237
8391
  node: node.name,
8238
- message: MESSAGE$43
8392
+ message: MESSAGE$49
8239
8393
  });
8240
8394
  return;
8241
8395
  }
8242
8396
  const verdict = evaluateTitleValue(titleAttr.value);
8243
8397
  if (verdict === "missing" || verdict === "empty") context.report({
8244
8398
  node: titleAttr,
8245
- message: MESSAGE$43
8399
+ message: MESSAGE$49
8246
8400
  });
8247
8401
  } })
8248
8402
  });
@@ -8346,7 +8500,7 @@ const iframeMissingSandbox = defineRule({
8346
8500
  });
8347
8501
  //#endregion
8348
8502
  //#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.";
8503
+ const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8350
8504
  const DEFAULT_COMPONENTS = ["img"];
8351
8505
  const DEFAULT_REDUNDANT_WORDS = [
8352
8506
  "image",
@@ -8411,7 +8565,7 @@ const imgRedundantAlt = defineRule({
8411
8565
  if (!altAttribute) return;
8412
8566
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8413
8567
  node: altAttribute,
8414
- message: MESSAGE$42
8568
+ message: MESSAGE$48
8415
8569
  });
8416
8570
  } };
8417
8571
  }
@@ -10768,7 +10922,7 @@ const jsxMaxDepth = defineRule({
10768
10922
  });
10769
10923
  //#endregion
10770
10924
  //#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.";
10925
+ const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10772
10926
  const LITERAL_TEXT_TAGS = new Set([
10773
10927
  "code",
10774
10928
  "pre",
@@ -10804,7 +10958,7 @@ const jsxNoCommentTextnodes = defineRule({
10804
10958
  if (isInsideLiteralTextTag(node)) return;
10805
10959
  context.report({
10806
10960
  node,
10807
- message: MESSAGE$41
10961
+ message: MESSAGE$47
10808
10962
  });
10809
10963
  } })
10810
10964
  });
@@ -10835,7 +10989,7 @@ const isInsideFunctionScope = (node) => {
10835
10989
  };
10836
10990
  //#endregion
10837
10991
  //#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.";
10992
+ const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10839
10993
  const CONTEXT_MODULES$1 = [
10840
10994
  "react",
10841
10995
  "use-context-selector",
@@ -10933,7 +11087,7 @@ const jsxNoConstructedContextValues = defineRule({
10933
11087
  if (!isConstructedValue(innerExpression)) continue;
10934
11088
  context.report({
10935
11089
  node: attribute,
10936
- message: MESSAGE$40
11090
+ message: MESSAGE$46
10937
11091
  });
10938
11092
  }
10939
11093
  }
@@ -11019,7 +11173,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11019
11173
  };
11020
11174
  //#endregion
11021
11175
  //#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.";
11176
+ const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11023
11177
  const KNOWN_SLOT_PROP_NAMES = new Set([
11024
11178
  "icon",
11025
11179
  "Icon",
@@ -11288,7 +11442,7 @@ const jsxNoJsxAsProp = defineRule({
11288
11442
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11289
11443
  context.report({
11290
11444
  node,
11291
- message: MESSAGE$39
11445
+ message: MESSAGE$45
11292
11446
  });
11293
11447
  }
11294
11448
  };
@@ -11576,7 +11730,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11576
11730
  ];
11577
11731
  //#endregion
11578
11732
  //#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.";
11733
+ const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11580
11734
  const isDataArrayPropName = (propName) => {
11581
11735
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11582
11736
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11660,7 +11814,7 @@ const jsxNoNewArrayAsProp = defineRule({
11660
11814
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11661
11815
  context.report({
11662
11816
  node,
11663
- message: MESSAGE$38
11817
+ message: MESSAGE$44
11664
11818
  });
11665
11819
  }
11666
11820
  };
@@ -11918,7 +12072,7 @@ const SAFE_RECEIVER_NAMES = new Set([
11918
12072
  ]);
11919
12073
  //#endregion
11920
12074
  //#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.";
12075
+ const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
11922
12076
  const isAccessorPredicateName = (propName) => {
11923
12077
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
11924
12078
  if (propName.length <= prefix.length) continue;
@@ -12124,7 +12278,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12124
12278
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12125
12279
  context.report({
12126
12280
  node,
12127
- message: MESSAGE$37
12281
+ message: MESSAGE$43
12128
12282
  });
12129
12283
  }
12130
12284
  };
@@ -12344,7 +12498,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12344
12498
  ];
12345
12499
  //#endregion
12346
12500
  //#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.";
12501
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12348
12502
  const isConfigObjectPropName = (propName) => {
12349
12503
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12350
12504
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12432,7 +12586,7 @@ const jsxNoNewObjectAsProp = defineRule({
12432
12586
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12433
12587
  context.report({
12434
12588
  node,
12435
- message: MESSAGE$36
12589
+ message: MESSAGE$42
12436
12590
  });
12437
12591
  }
12438
12592
  };
@@ -12440,7 +12594,7 @@ const jsxNoNewObjectAsProp = defineRule({
12440
12594
  });
12441
12595
  //#endregion
12442
12596
  //#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.";
12597
+ const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12444
12598
  const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
12445
12599
  const resolveSettings$28 = (settings) => {
12446
12600
  const reactDoctor = settings?.["react-doctor"];
@@ -12481,7 +12635,7 @@ const jsxNoScriptUrl = defineRule({
12481
12635
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12482
12636
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12483
12637
  node: attribute,
12484
- message: MESSAGE$35
12638
+ message: MESSAGE$41
12485
12639
  });
12486
12640
  }
12487
12641
  } };
@@ -12796,7 +12950,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12796
12950
  });
12797
12951
  //#endregion
12798
12952
  //#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.";
12953
+ const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12800
12954
  const resolveSettings$25 = (settings) => {
12801
12955
  const reactDoctor = settings?.["react-doctor"];
12802
12956
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12837,7 +12991,7 @@ const jsxPropsNoSpreading = defineRule({
12837
12991
  }
12838
12992
  context.report({
12839
12993
  node: attribute,
12840
- message: MESSAGE$34
12994
+ message: MESSAGE$40
12841
12995
  });
12842
12996
  }
12843
12997
  } };
@@ -13065,7 +13219,7 @@ const labelHasAssociatedControl = defineRule({
13065
13219
  });
13066
13220
  //#endregion
13067
13221
  //#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`.";
13222
+ const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13069
13223
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13070
13224
  "aa",
13071
13225
  "ab",
@@ -13277,7 +13431,7 @@ const lang = defineRule({
13277
13431
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13278
13432
  context.report({
13279
13433
  node: langAttr,
13280
- message: MESSAGE$33
13434
+ message: MESSAGE$39
13281
13435
  });
13282
13436
  return;
13283
13437
  }
@@ -13286,7 +13440,7 @@ const lang = defineRule({
13286
13440
  if (value === null) return;
13287
13441
  if (!isValidLangTag(value)) context.report({
13288
13442
  node: langAttr,
13289
- message: MESSAGE$33
13443
+ message: MESSAGE$39
13290
13444
  });
13291
13445
  } })
13292
13446
  });
@@ -13330,7 +13484,7 @@ const mdxSsrExecutionRisk = defineRule({
13330
13484
  });
13331
13485
  //#endregion
13332
13486
  //#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>`.";
13487
+ const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13334
13488
  const DEFAULT_AUDIO = ["audio"];
13335
13489
  const DEFAULT_VIDEO = ["video"];
13336
13490
  const DEFAULT_TRACK = ["track"];
@@ -13371,7 +13525,7 @@ const mediaHasCaption = defineRule({
13371
13525
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13372
13526
  context.report({
13373
13527
  node: node.name,
13374
- message: MESSAGE$32
13528
+ message: MESSAGE$38
13375
13529
  });
13376
13530
  return;
13377
13531
  }
@@ -13388,7 +13542,7 @@ const mediaHasCaption = defineRule({
13388
13542
  return kindValue.value.toLowerCase() === "captions";
13389
13543
  })) context.report({
13390
13544
  node: node.name,
13391
- message: MESSAGE$32
13545
+ message: MESSAGE$38
13392
13546
  });
13393
13547
  } };
13394
13548
  }
@@ -15189,7 +15343,7 @@ const nextjsNoVercelOgImport = defineRule({
15189
15343
  });
15190
15344
  //#endregion
15191
15345
  //#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.";
15346
+ const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15193
15347
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15194
15348
  const noAccessKey = defineRule({
15195
15349
  id: "no-access-key",
@@ -15206,7 +15360,7 @@ const noAccessKey = defineRule({
15206
15360
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15207
15361
  context.report({
15208
15362
  node: accessKey,
15209
- message: MESSAGE$31
15363
+ message: MESSAGE$37
15210
15364
  });
15211
15365
  return;
15212
15366
  }
@@ -15216,7 +15370,7 @@ const noAccessKey = defineRule({
15216
15370
  if (isUndefinedIdentifier(expression)) return;
15217
15371
  context.report({
15218
15372
  node: accessKey,
15219
- message: MESSAGE$31
15373
+ message: MESSAGE$37
15220
15374
  });
15221
15375
  }
15222
15376
  } })
@@ -15699,7 +15853,7 @@ const noAdjustStateOnPropChange = defineRule({
15699
15853
  });
15700
15854
  //#endregion
15701
15855
  //#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.";
15856
+ const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15703
15857
  const noAriaHiddenOnFocusable = defineRule({
15704
15858
  id: "no-aria-hidden-on-focusable",
15705
15859
  title: "aria-hidden on focusable element",
@@ -15726,7 +15880,7 @@ const noAriaHiddenOnFocusable = defineRule({
15726
15880
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15727
15881
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15728
15882
  node: ariaHidden,
15729
- message: MESSAGE$30
15883
+ message: MESSAGE$36
15730
15884
  });
15731
15885
  } })
15732
15886
  });
@@ -16094,7 +16248,7 @@ const noArrayIndexAsKey = defineRule({
16094
16248
  });
16095
16249
  //#endregion
16096
16250
  //#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.";
16251
+ const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16098
16252
  const SECOND_INDEX_METHODS = new Set([
16099
16253
  "every",
16100
16254
  "filter",
@@ -16298,7 +16452,7 @@ const noArrayIndexKey = defineRule({
16298
16452
  }
16299
16453
  context.report({
16300
16454
  node: keyAttribute,
16301
- message: MESSAGE$29
16455
+ message: MESSAGE$35
16302
16456
  });
16303
16457
  },
16304
16458
  CallExpression(node) {
@@ -16318,15 +16472,35 @@ const noArrayIndexKey = defineRule({
16318
16472
  if (propName !== "key") continue;
16319
16473
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16320
16474
  node: property,
16321
- message: MESSAGE$29
16475
+ message: MESSAGE$35
16322
16476
  });
16323
16477
  }
16324
16478
  }
16325
16479
  })
16326
16480
  });
16327
16481
  //#endregion
16482
+ //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16483
+ const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16484
+ const noAsyncEffectCallback = defineRule({
16485
+ id: "no-async-effect-callback",
16486
+ title: "Async effect callback",
16487
+ severity: "warn",
16488
+ recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
16489
+ create: (context) => ({ CallExpression(node) {
16490
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
16491
+ const callback = getEffectCallback(node);
16492
+ if (!callback) return;
16493
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
16494
+ if (!callback.async) return;
16495
+ context.report({
16496
+ node: callback,
16497
+ message: MESSAGE$34
16498
+ });
16499
+ } })
16500
+ });
16501
+ //#endregion
16328
16502
  //#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.";
16503
+ const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16330
16504
  const resolveSettings$21 = (settings) => {
16331
16505
  const reactDoctor = settings?.["react-doctor"];
16332
16506
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16382,7 +16556,7 @@ const noAutofocus = defineRule({
16382
16556
  }
16383
16557
  context.report({
16384
16558
  node: autoFocusAttribute,
16385
- message: MESSAGE$28
16559
+ message: MESSAGE$33
16386
16560
  });
16387
16561
  } };
16388
16562
  }
@@ -16632,6 +16806,109 @@ const noBarrelImport = defineRule({
16632
16806
  }
16633
16807
  });
16634
16808
  //#endregion
16809
+ //#region src/plugin/utils/function-contains-react-render-output.ts
16810
+ const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
16811
+ "FunctionDeclaration",
16812
+ "FunctionExpression",
16813
+ "ArrowFunctionExpression",
16814
+ "ClassDeclaration",
16815
+ "ClassExpression"
16816
+ ]);
16817
+ const isReactImport$1 = (symbol) => {
16818
+ let importDeclaration = symbol.declarationNode?.parent;
16819
+ while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
16820
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
16821
+ return importDeclaration.source.value === "react";
16822
+ };
16823
+ const getImportedName = (symbol) => {
16824
+ if (symbol.kind !== "import") return null;
16825
+ if (!isReactImport$1(symbol)) return null;
16826
+ return getImportedName$1(symbol.declarationNode) ?? null;
16827
+ };
16828
+ const isReactNamespaceImport = (symbol) => {
16829
+ if (symbol.kind !== "import") return false;
16830
+ if (!isReactImport$1(symbol)) return false;
16831
+ return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
16832
+ };
16833
+ const isReactCreateElementIdentifierCall = (callee, scopes) => {
16834
+ if (!isNodeOfType(callee, "Identifier")) return false;
16835
+ const symbol = scopes.symbolFor(callee);
16836
+ return Boolean(symbol && getImportedName(symbol) === "createElement");
16837
+ };
16838
+ const isReactCreateElementMemberCall = (callee, scopes) => {
16839
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
16840
+ if (callee.computed) return false;
16841
+ if (!isNodeOfType(callee.object, "Identifier")) return false;
16842
+ if (!isNodeOfType(callee.property, "Identifier")) return false;
16843
+ if (callee.property.name !== "createElement") return false;
16844
+ const symbol = scopes.symbolFor(callee.object);
16845
+ return Boolean(symbol && isReactNamespaceImport(symbol));
16846
+ };
16847
+ const isReactCreateElementCall = (node, scopes) => {
16848
+ if (!isNodeOfType(node, "CallExpression")) return false;
16849
+ return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
16850
+ };
16851
+ const containsRenderOutput = (node, rootNode, scopes) => {
16852
+ if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
16853
+ if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
16854
+ if (isReactCreateElementCall(node, scopes)) return true;
16855
+ const nodeRecord = node;
16856
+ for (const key of Object.keys(nodeRecord)) {
16857
+ if (key === "parent") continue;
16858
+ const child = nodeRecord[key];
16859
+ if (Array.isArray(child)) {
16860
+ for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
16861
+ } else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
16862
+ }
16863
+ return false;
16864
+ };
16865
+ const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
16866
+ //#endregion
16867
+ //#region src/plugin/utils/is-component-declaration.ts
16868
+ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
16869
+ //#endregion
16870
+ //#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
16871
+ const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
16872
+ const symbolIsLocalComponent = (symbol, context) => {
16873
+ const declaration = symbol.declarationNode;
16874
+ if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
16875
+ if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
16876
+ return false;
16877
+ };
16878
+ const noCallComponentAsFunction = defineRule({
16879
+ id: "no-call-component-as-function",
16880
+ title: "Component called as a function",
16881
+ severity: "warn",
16882
+ tags: ["test-noise"],
16883
+ recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
16884
+ create: (context) => {
16885
+ const renderedJsxNames = /* @__PURE__ */ new Set();
16886
+ const candidateCalls = [];
16887
+ return {
16888
+ JSXOpeningElement(node) {
16889
+ if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
16890
+ },
16891
+ CallExpression(node) {
16892
+ if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
16893
+ node,
16894
+ callee: node.callee,
16895
+ name: node.callee.name
16896
+ });
16897
+ },
16898
+ "Program:exit"() {
16899
+ for (const candidate of candidateCalls) {
16900
+ const symbol = context.scopes.symbolFor(candidate.callee);
16901
+ if (!symbol) continue;
16902
+ if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
16903
+ node: candidate.node,
16904
+ message: message(candidate.name)
16905
+ });
16906
+ }
16907
+ }
16908
+ };
16909
+ }
16910
+ });
16911
+ //#endregion
16635
16912
  //#region src/plugin/utils/is-setter-identifier.ts
16636
16913
  const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
16637
16914
  //#endregion
@@ -16783,7 +17060,7 @@ const noChainStateUpdates = defineRule({
16783
17060
  });
16784
17061
  //#endregion
16785
17062
  //#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.";
17063
+ const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
16787
17064
  const noChildrenProp = defineRule({
16788
17065
  id: "no-children-prop",
16789
17066
  title: "Children passed as a prop",
@@ -16795,7 +17072,7 @@ const noChildrenProp = defineRule({
16795
17072
  if (node.name.name !== "children") return;
16796
17073
  context.report({
16797
17074
  node: node.name,
16798
- message: MESSAGE$27
17075
+ message: MESSAGE$32
16799
17076
  });
16800
17077
  },
16801
17078
  CallExpression(node) {
@@ -16808,7 +17085,7 @@ const noChildrenProp = defineRule({
16808
17085
  const propertyKey = property.key;
16809
17086
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
16810
17087
  node: propertyKey,
16811
- message: MESSAGE$27
17088
+ message: MESSAGE$32
16812
17089
  });
16813
17090
  }
16814
17091
  }
@@ -16816,7 +17093,7 @@ const noChildrenProp = defineRule({
16816
17093
  });
16817
17094
  //#endregion
16818
17095
  //#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.";
17096
+ const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
16820
17097
  const noCloneElement = defineRule({
16821
17098
  id: "no-clone-element",
16822
17099
  title: "cloneElement makes child props fragile",
@@ -16829,7 +17106,7 @@ const noCloneElement = defineRule({
16829
17106
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
16830
17107
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
16831
17108
  node: callee,
16832
- message: MESSAGE$26
17109
+ message: MESSAGE$31
16833
17110
  });
16834
17111
  return;
16835
17112
  }
@@ -16842,7 +17119,7 @@ const noCloneElement = defineRule({
16842
17119
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
16843
17120
  context.report({
16844
17121
  node: callee,
16845
- message: MESSAGE$26
17122
+ message: MESSAGE$31
16846
17123
  });
16847
17124
  }
16848
17125
  } })
@@ -16891,7 +17168,7 @@ const enclosingComponentOrHookName = (node) => {
16891
17168
  };
16892
17169
  //#endregion
16893
17170
  //#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.";
17171
+ const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
16895
17172
  const CONTEXT_MODULES = [
16896
17173
  "react",
16897
17174
  "use-context-selector",
@@ -16927,7 +17204,32 @@ const noCreateContextInRender = defineRule({
16927
17204
  if (!componentOrHookName) return;
16928
17205
  context.report({
16929
17206
  node,
16930
- message: `${MESSAGE$25} (called inside "${componentOrHookName}")`
17207
+ message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17208
+ });
17209
+ } })
17210
+ });
17211
+ //#endregion
17212
+ //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17213
+ const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17214
+ const noCreateRefInFunctionComponent = defineRule({
17215
+ id: "no-create-ref-in-function-component",
17216
+ title: "createRef in function component",
17217
+ severity: "warn",
17218
+ recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
17219
+ create: (context) => ({ CallExpression(node) {
17220
+ if (!isReactFunctionCall(node, "createRef")) return;
17221
+ if (isNodeOfType(node.callee, "Identifier")) {
17222
+ const symbol = context.scopes.symbolFor(node.callee);
17223
+ if (symbol && symbol.kind !== "import") return;
17224
+ }
17225
+ const enclosingFunction = nearestEnclosingFunction(node);
17226
+ if (!enclosingFunction) return;
17227
+ const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
17228
+ if (!displayName) return;
17229
+ if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17230
+ context.report({
17231
+ node,
17232
+ message: MESSAGE$29
16931
17233
  });
16932
17234
  } })
16933
17235
  });
@@ -17067,7 +17369,7 @@ const noCreateStoreInRender = defineRule({
17067
17369
  });
17068
17370
  //#endregion
17069
17371
  //#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.";
17372
+ const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17071
17373
  const noDanger = defineRule({
17072
17374
  id: "no-danger",
17073
17375
  title: "Raw HTML injection can run unsafe markup",
@@ -17080,7 +17382,7 @@ const noDanger = defineRule({
17080
17382
  if (!propAttribute) return;
17081
17383
  context.report({
17082
17384
  node: propAttribute.name,
17083
- message: MESSAGE$24
17385
+ message: MESSAGE$28
17084
17386
  });
17085
17387
  },
17086
17388
  CallExpression(node) {
@@ -17092,7 +17394,7 @@ const noDanger = defineRule({
17092
17394
  const propertyKey = property.key;
17093
17395
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17094
17396
  node: propertyKey,
17095
- message: MESSAGE$24
17397
+ message: MESSAGE$28
17096
17398
  });
17097
17399
  }
17098
17400
  }
@@ -17100,7 +17402,7 @@ const noDanger = defineRule({
17100
17402
  });
17101
17403
  //#endregion
17102
17404
  //#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`.";
17405
+ const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17104
17406
  const isLineBreak = (child) => {
17105
17407
  if (!isNodeOfType(child, "JSXText")) return false;
17106
17408
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17170,7 +17472,7 @@ const noDangerWithChildren = defineRule({
17170
17472
  if (!hasChildrenProp && !hasNestedChildren) return;
17171
17473
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17172
17474
  node: opening,
17173
- message: MESSAGE$23
17475
+ message: MESSAGE$27
17174
17476
  });
17175
17477
  },
17176
17478
  CallExpression(node) {
@@ -17182,7 +17484,7 @@ const noDangerWithChildren = defineRule({
17182
17484
  if (!propsShape.hasDangerously) return;
17183
17485
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17184
17486
  node,
17185
- message: MESSAGE$23
17487
+ message: MESSAGE$27
17186
17488
  });
17187
17489
  }
17188
17490
  })
@@ -17759,7 +18061,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17759
18061
  //#endregion
17760
18062
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17761
18063
  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`.";
18064
+ const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
17763
18065
  const resolveSettings$20 = (settings) => {
17764
18066
  const reactDoctor = settings?.["react-doctor"];
17765
18067
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -17778,7 +18080,7 @@ const noDidMountSetState = defineRule({
17778
18080
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17779
18081
  context.report({
17780
18082
  node: node.callee,
17781
- message: MESSAGE$22
18083
+ message: MESSAGE$26
17782
18084
  });
17783
18085
  } };
17784
18086
  }
@@ -17786,7 +18088,7 @@ const noDidMountSetState = defineRule({
17786
18088
  //#endregion
17787
18089
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
17788
18090
  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.";
18091
+ const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
17790
18092
  const resolveSettings$19 = (settings) => {
17791
18093
  const reactDoctor = settings?.["react-doctor"];
17792
18094
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -17805,7 +18107,7 @@ const noDidUpdateSetState = defineRule({
17805
18107
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
17806
18108
  context.report({
17807
18109
  node: node.callee,
17808
- message: MESSAGE$21
18110
+ message: MESSAGE$25
17809
18111
  });
17810
18112
  } };
17811
18113
  }
@@ -17828,7 +18130,7 @@ const isStateMemberExpression = (node) => {
17828
18130
  };
17829
18131
  //#endregion
17830
18132
  //#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.";
18133
+ const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
17832
18134
  const shouldIgnoreMutation = (node) => {
17833
18135
  let isConstructor = false;
17834
18136
  let isInsideCallExpression = false;
@@ -17850,7 +18152,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
17850
18152
  if (shouldIgnoreMutation(reportNode)) return;
17851
18153
  context.report({
17852
18154
  node: reportNode,
17853
- message: MESSAGE$20
18155
+ message: MESSAGE$24
17854
18156
  });
17855
18157
  };
17856
18158
  const noDirectMutationState = defineRule({
@@ -18060,6 +18362,26 @@ const noDocumentStartViewTransition = defineRule({
18060
18362
  } })
18061
18363
  });
18062
18364
  //#endregion
18365
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18366
+ const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18367
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18368
+ const noDocumentWrite = defineRule({
18369
+ id: "no-document-write",
18370
+ title: "document.write/writeln",
18371
+ severity: "warn",
18372
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18373
+ create: (context) => ({ CallExpression(node) {
18374
+ const callee = node.callee;
18375
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18376
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18377
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18378
+ context.report({
18379
+ node,
18380
+ message: MESSAGE$23
18381
+ });
18382
+ } })
18383
+ });
18384
+ //#endregion
18063
18385
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18064
18386
  const noDynamicImportPath = defineRule({
18065
18387
  id: "no-dynamic-import-path",
@@ -19438,7 +19760,7 @@ const ALLOWED_NAMESPACES = new Set([
19438
19760
  "ReactDOM",
19439
19761
  "ReactDom"
19440
19762
  ]);
19441
- const MESSAGE$19 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19763
+ const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19442
19764
  const noFindDomNode = defineRule({
19443
19765
  id: "no-find-dom-node",
19444
19766
  title: "findDOMNode breaks component encapsulation",
@@ -19449,7 +19771,7 @@ const noFindDomNode = defineRule({
19449
19771
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19450
19772
  context.report({
19451
19773
  node: callee,
19452
- message: MESSAGE$19
19774
+ message: MESSAGE$22
19453
19775
  });
19454
19776
  return;
19455
19777
  }
@@ -19460,7 +19782,7 @@ const noFindDomNode = defineRule({
19460
19782
  if (callee.property.name !== "findDOMNode") return;
19461
19783
  context.report({
19462
19784
  node: callee.property,
19463
- message: MESSAGE$19
19785
+ message: MESSAGE$22
19464
19786
  });
19465
19787
  }
19466
19788
  } })
@@ -19523,64 +19845,6 @@ const noGenericHandlerNames = defineRule({
19523
19845
  } })
19524
19846
  });
19525
19847
  //#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
19848
  //#region src/plugin/rules/architecture/no-giant-component.ts
19585
19849
  const noGiantComponent = defineRule({
19586
19850
  id: "no-giant-component",
@@ -19759,6 +20023,26 @@ const noGrayOnColoredBackground = defineRule({
19759
20023
  } })
19760
20024
  });
19761
20025
  //#endregion
20026
+ //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20027
+ const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20028
+ const noImgLazyWithHighFetchpriority = defineRule({
20029
+ id: "no-img-lazy-with-high-fetchpriority",
20030
+ title: "Lazy image with high fetchPriority",
20031
+ severity: "warn",
20032
+ 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.",
20033
+ create: (context) => ({ JSXOpeningElement(node) {
20034
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
20035
+ const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
20036
+ if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
20037
+ const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
20038
+ if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20039
+ context.report({
20040
+ node: node.name,
20041
+ message: MESSAGE$21
20042
+ });
20043
+ } })
20044
+ });
20045
+ //#endregion
19762
20046
  //#region src/plugin/rules/state-and-effects/no-initialize-state.ts
19763
20047
  const noInitializeState = defineRule({
19764
20048
  id: "no-initialize-state",
@@ -19988,8 +20272,31 @@ const noIsMounted = defineRule({
19988
20272
  } })
19989
20273
  });
19990
20274
  //#endregion
20275
+ //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20276
+ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20277
+ const isJsonMethodCall = (node, method) => {
20278
+ if (!isNodeOfType(node, "CallExpression")) return false;
20279
+ const callee = node.callee;
20280
+ return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
20281
+ };
20282
+ const noJsonParseStringifyClone = defineRule({
20283
+ id: "no-json-parse-stringify-clone",
20284
+ title: "JSON parse/stringify deep clone",
20285
+ severity: "warn",
20286
+ recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
20287
+ create: (context) => ({ CallExpression(node) {
20288
+ if (!isJsonMethodCall(node, "parse")) return;
20289
+ const firstArgument = node.arguments?.[0];
20290
+ if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20291
+ context.report({
20292
+ node,
20293
+ message: MESSAGE$20
20294
+ });
20295
+ } })
20296
+ });
20297
+ //#endregion
19991
20298
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
19992
- const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20299
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
19993
20300
  const isJsxElementTypeReference = (node) => {
19994
20301
  if (!isNodeOfType(node, "TSTypeReference")) return false;
19995
20302
  const typeName = node.typeName;
@@ -20006,7 +20313,7 @@ const checkReturnType = (context, returnType) => {
20006
20313
  if (!typeAnnotation) return;
20007
20314
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20008
20315
  node: typeAnnotation,
20009
- message: MESSAGE$18
20316
+ message: MESSAGE$19
20010
20317
  });
20011
20318
  };
20012
20319
  const noJsxElementType = defineRule({
@@ -20310,9 +20617,6 @@ const noLongTransitionDuration = defineRule({
20310
20617
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20311
20618
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
20312
20619
  //#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
20620
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
20317
20621
  const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
20318
20622
  const found = /* @__PURE__ */ new Set();
@@ -20464,7 +20768,7 @@ const noMoment = defineRule({
20464
20768
  });
20465
20769
  //#endregion
20466
20770
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20467
- const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
20771
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
20468
20772
  const resolveSettings$16 = (settings) => {
20469
20773
  const reactDoctor = settings?.["react-doctor"];
20470
20774
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -20786,7 +21090,7 @@ const noMultiComp = defineRule({
20786
21090
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
20787
21091
  for (const component of flagged.slice(1)) context.report({
20788
21092
  node: component.reportNode,
20789
- message: MESSAGE$17
21093
+ message: MESSAGE$18
20790
21094
  });
20791
21095
  } };
20792
21096
  }
@@ -20954,7 +21258,7 @@ const resolveReducerFunction = (node, currentFilename) => {
20954
21258
  };
20955
21259
  //#endregion
20956
21260
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
20957
- const MESSAGE$16 = "This reducer changes state in place, so your update is silently skipped.";
21261
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
20958
21262
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
20959
21263
  "copyWithin",
20960
21264
  "fill",
@@ -21164,7 +21468,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21164
21468
  reportedNodes.add(options.crossFileConsumerCallSite);
21165
21469
  context.report({
21166
21470
  node: options.crossFileConsumerCallSite,
21167
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21471
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21168
21472
  });
21169
21473
  return;
21170
21474
  }
@@ -21173,7 +21477,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21173
21477
  reportedNodes.add(mutation.node);
21174
21478
  context.report({
21175
21479
  node: mutation.node,
21176
- message: MESSAGE$16
21480
+ message: MESSAGE$17
21177
21481
  });
21178
21482
  }
21179
21483
  };
@@ -21445,7 +21749,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21445
21749
  });
21446
21750
  //#endregion
21447
21751
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21448
- const MESSAGE$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21752
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21449
21753
  const resolveSettings$14 = (settings) => {
21450
21754
  const reactDoctor = settings?.["react-doctor"];
21451
21755
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21473,7 +21777,7 @@ const noNoninteractiveTabindex = defineRule({
21473
21777
  if (numeric === null) {
21474
21778
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21475
21779
  node: tabIndex,
21476
- message: MESSAGE$15
21780
+ message: MESSAGE$16
21477
21781
  });
21478
21782
  return;
21479
21783
  }
@@ -21486,7 +21790,7 @@ const noNoninteractiveTabindex = defineRule({
21486
21790
  if (!roleAttribute) {
21487
21791
  context.report({
21488
21792
  node: tabIndex,
21489
- message: MESSAGE$15
21793
+ message: MESSAGE$16
21490
21794
  });
21491
21795
  return;
21492
21796
  }
@@ -21500,7 +21804,7 @@ const noNoninteractiveTabindex = defineRule({
21500
21804
  }
21501
21805
  context.report({
21502
21806
  node: tabIndex,
21503
- message: MESSAGE$15
21807
+ message: MESSAGE$16
21504
21808
  });
21505
21809
  } };
21506
21810
  }
@@ -22191,7 +22495,7 @@ const noRandomKey = defineRule({
22191
22495
  });
22192
22496
  //#endregion
22193
22497
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22194
- const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22498
+ const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22195
22499
  const isChildrenIdentifier = (node, contextNode) => {
22196
22500
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22197
22501
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22217,13 +22521,13 @@ const noReactChildren = defineRule({
22217
22521
  if (isChildrenIdentifier(memberObject, node)) {
22218
22522
  context.report({
22219
22523
  node: calleeOuter,
22220
- message: MESSAGE$14
22524
+ message: MESSAGE$15
22221
22525
  });
22222
22526
  return;
22223
22527
  }
22224
22528
  if (isReactNamespaceMember(memberObject, node)) context.report({
22225
22529
  node: calleeOuter,
22226
- message: MESSAGE$14
22530
+ message: MESSAGE$15
22227
22531
  });
22228
22532
  } })
22229
22533
  });
@@ -22546,7 +22850,7 @@ const noRenderPropChildren = defineRule({
22546
22850
  });
22547
22851
  //#endregion
22548
22852
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22549
- const MESSAGE$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22853
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22550
22854
  const isReactDomRenderCall = (node) => {
22551
22855
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22552
22856
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22570,7 +22874,7 @@ const noRenderReturnValue = defineRule({
22570
22874
  if (!isUsedAsReturnValue(node.parent)) return;
22571
22875
  context.report({
22572
22876
  node: node.callee,
22573
- message: MESSAGE$13
22877
+ message: MESSAGE$14
22574
22878
  });
22575
22879
  } })
22576
22880
  });
@@ -22730,11 +23034,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
22730
23034
  return "unknown";
22731
23035
  };
22732
23036
  //#endregion
22733
- //#region src/plugin/utils/get-identifier-trailing-word.ts
22734
- const getIdentifierTrailingWord = (identifierName) => {
22735
- return identifierName.match(/[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g)?.at(-1)?.toLowerCase() ?? identifierName.toLowerCase();
23037
+ //#region src/plugin/utils/tokenize-identifier-words.ts
23038
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
23039
+ const tokenizeIdentifierWords = (identifierName) => {
23040
+ const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
23041
+ if (!words) return [];
23042
+ return words.map((word) => word.toLowerCase());
22736
23043
  };
22737
23044
  //#endregion
23045
+ //#region src/plugin/utils/get-identifier-trailing-word.ts
23046
+ const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
23047
+ //#endregion
22738
23048
  //#region src/plugin/constants/tanstack.ts
22739
23049
  const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
22740
23050
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
@@ -23262,7 +23572,7 @@ const getParentComponent = (node) => {
23262
23572
  };
23263
23573
  //#endregion
23264
23574
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23265
- const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23575
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23266
23576
  const noSetState = defineRule({
23267
23577
  id: "no-set-state",
23268
23578
  title: "Local class state forbidden",
@@ -23277,7 +23587,7 @@ const noSetState = defineRule({
23277
23587
  if (!getParentComponent(node)) return;
23278
23588
  context.report({
23279
23589
  node: node.callee,
23280
- message: MESSAGE$12
23590
+ message: MESSAGE$13
23281
23591
  });
23282
23592
  } })
23283
23593
  });
@@ -23439,7 +23749,7 @@ const isAbstractRole = (openingElement, settings) => {
23439
23749
  };
23440
23750
  //#endregion
23441
23751
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23442
- const MESSAGE$11 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23752
+ const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23443
23753
  const DEFAULT_HANDLERS = [
23444
23754
  "onClick",
23445
23755
  "onMouseDown",
@@ -23499,7 +23809,7 @@ const noStaticElementInteractions = defineRule({
23499
23809
  if (!roleAttribute || !roleAttribute.value) {
23500
23810
  context.report({
23501
23811
  node: node.name,
23502
- message: MESSAGE$11
23812
+ message: MESSAGE$12
23503
23813
  });
23504
23814
  return;
23505
23815
  }
@@ -23509,19 +23819,66 @@ const noStaticElementInteractions = defineRule({
23509
23819
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23510
23820
  context.report({
23511
23821
  node: node.name,
23512
- message: MESSAGE$11
23822
+ message: MESSAGE$12
23513
23823
  });
23514
23824
  return;
23515
23825
  }
23516
23826
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23517
23827
  context.report({
23518
23828
  node: node.name,
23519
- message: MESSAGE$11
23829
+ message: MESSAGE$12
23520
23830
  });
23521
23831
  } };
23522
23832
  }
23523
23833
  });
23524
23834
  //#endregion
23835
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
23836
+ const BOOLEAN_ATTRIBUTES = new Set([
23837
+ "disabled",
23838
+ "checked",
23839
+ "readonly",
23840
+ "required",
23841
+ "selected",
23842
+ "multiple",
23843
+ "autofocus",
23844
+ "autoplay",
23845
+ "controls",
23846
+ "loop",
23847
+ "muted",
23848
+ "open",
23849
+ "reversed",
23850
+ "default",
23851
+ "novalidate",
23852
+ "formnovalidate",
23853
+ "playsinline",
23854
+ "itemscope",
23855
+ "allowfullscreen"
23856
+ ]);
23857
+ const noStringFalseOnBooleanAttribute = defineRule({
23858
+ id: "no-string-false-on-boolean-attribute",
23859
+ title: "String true/false on a boolean attribute",
23860
+ severity: "warn",
23861
+ recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
23862
+ create: (context) => ({ JSXOpeningElement(node) {
23863
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
23864
+ const firstCharacter = node.name.name.charCodeAt(0);
23865
+ if (firstCharacter < 97 || firstCharacter > 122) return;
23866
+ for (const attribute of node.attributes) {
23867
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
23868
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
23869
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
23870
+ const value = getJsxPropStringValue(attribute);
23871
+ if (value !== "false" && value !== "true") continue;
23872
+ const attributeName = attribute.name.name;
23873
+ const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
23874
+ context.report({
23875
+ node: attribute,
23876
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
23877
+ });
23878
+ }
23879
+ } })
23880
+ });
23881
+ //#endregion
23525
23882
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23526
23883
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23527
23884
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23572,6 +23929,27 @@ const noStringRefs = defineRule({
23572
23929
  }
23573
23930
  });
23574
23931
  //#endregion
23932
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
23933
+ const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
23934
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
23935
+ const noSyncXhr = defineRule({
23936
+ id: "no-sync-xhr",
23937
+ title: "Synchronous XMLHttpRequest",
23938
+ severity: "warn",
23939
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
23940
+ create: (context) => ({ CallExpression(node) {
23941
+ const callee = node.callee;
23942
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
23943
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
23944
+ const asyncArgument = node.arguments?.[2];
23945
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
23946
+ context.report({
23947
+ node,
23948
+ message: MESSAGE$11
23949
+ });
23950
+ } })
23951
+ });
23952
+ //#endregion
23575
23953
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23576
23954
  const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
23577
23955
  const isInsideClassMethod = (node, customClassFactoryNames) => {
@@ -34681,6 +35059,47 @@ const serverAfterNonblocking = defineRule({
34681
35059
  }
34682
35060
  });
34683
35061
  //#endregion
35062
+ //#region src/plugin/utils/is-auth-guard-name.ts
35063
+ const SIGNED_IN_HEAD_TOKENS = new Set([
35064
+ "signed",
35065
+ "logged",
35066
+ "sign"
35067
+ ]);
35068
+ const mergeSignedInTokens = (tokens) => {
35069
+ const mergedTokens = [];
35070
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
35071
+ const currentToken = tokens[tokenIndex];
35072
+ if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
35073
+ mergedTokens.push(`${currentToken}in`);
35074
+ tokenIndex += 1;
35075
+ continue;
35076
+ }
35077
+ mergedTokens.push(currentToken);
35078
+ }
35079
+ return mergedTokens;
35080
+ };
35081
+ const isAuthGuardName = (calleeName) => {
35082
+ const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
35083
+ if (tokens.length === 0) return false;
35084
+ let hasAssertiveVerb = false;
35085
+ let hasGetterVerb = false;
35086
+ let hasQualifier = false;
35087
+ let hasStrongNoun = false;
35088
+ let hasWeakNoun = false;
35089
+ for (const token of tokens) {
35090
+ if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
35091
+ if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
35092
+ if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
35093
+ if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
35094
+ if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
35095
+ if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
35096
+ }
35097
+ if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
35098
+ if (hasGetterVerb && hasStrongNoun) return true;
35099
+ if (hasQualifier && hasWeakNoun) return true;
35100
+ return false;
35101
+ };
35102
+ //#endregion
34684
35103
  //#region src/plugin/rules/server/server-auth-actions.ts
34685
35104
  const isAsyncFunctionLikeNode = (node) => {
34686
35105
  if (!node) return false;
@@ -34723,9 +35142,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
34723
35142
  const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
34724
35143
  const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
34725
35144
  if (!calleeNode) return null;
34726
- if (isNodeOfType(calleeNode, "Identifier")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
35145
+ if (isNodeOfType(calleeNode, "Identifier")) {
35146
+ const calleeName = calleeNode.name;
35147
+ return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
35148
+ }
34727
35149
  if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
34728
35150
  const methodName = calleeNode.property.name;
35151
+ if (isAuthGuardName(methodName)) return methodName;
34729
35152
  if (!allowedFunctionNames.has(methodName)) return null;
34730
35153
  if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
34731
35154
  return methodName;
@@ -37144,6 +37567,17 @@ const reactDoctorRules = [
37144
37567
  category: "Performance"
37145
37568
  }
37146
37569
  },
37570
+ {
37571
+ key: "react-doctor/auth-token-in-web-storage",
37572
+ id: "auth-token-in-web-storage",
37573
+ source: "react-doctor",
37574
+ originallyExternal: false,
37575
+ rule: {
37576
+ ...authTokenInWebStorage,
37577
+ framework: "global",
37578
+ category: "Security"
37579
+ }
37580
+ },
37147
37581
  {
37148
37582
  key: "react-doctor/autocomplete-valid",
37149
37583
  id: "autocomplete-valid",
@@ -37360,6 +37794,18 @@ const reactDoctorRules = [
37360
37794
  requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
37361
37795
  }
37362
37796
  },
37797
+ {
37798
+ key: "react-doctor/dialog-has-accessible-name",
37799
+ id: "dialog-has-accessible-name",
37800
+ source: "react-doctor",
37801
+ originallyExternal: false,
37802
+ rule: {
37803
+ ...dialogHasAccessibleName,
37804
+ framework: "global",
37805
+ category: "Accessibility",
37806
+ requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
37807
+ }
37808
+ },
37363
37809
  {
37364
37810
  key: "react-doctor/display-name",
37365
37811
  id: "display-name",
@@ -38519,6 +38965,18 @@ const reactDoctorRules = [
38519
38965
  requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
38520
38966
  }
38521
38967
  },
38968
+ {
38969
+ key: "react-doctor/no-async-effect-callback",
38970
+ id: "no-async-effect-callback",
38971
+ source: "react-doctor",
38972
+ originallyExternal: false,
38973
+ rule: {
38974
+ ...noAsyncEffectCallback,
38975
+ framework: "global",
38976
+ category: "Bugs",
38977
+ requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
38978
+ }
38979
+ },
38522
38980
  {
38523
38981
  key: "react-doctor/no-autofocus",
38524
38982
  id: "no-autofocus",
@@ -38542,6 +39000,18 @@ const reactDoctorRules = [
38542
39000
  category: "Performance"
38543
39001
  }
38544
39002
  },
39003
+ {
39004
+ key: "react-doctor/no-call-component-as-function",
39005
+ id: "no-call-component-as-function",
39006
+ source: "react-doctor",
39007
+ originallyExternal: false,
39008
+ rule: {
39009
+ ...noCallComponentAsFunction,
39010
+ framework: "global",
39011
+ category: "Bugs",
39012
+ requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
39013
+ }
39014
+ },
38545
39015
  {
38546
39016
  key: "react-doctor/no-cascading-set-state",
38547
39017
  id: "no-cascading-set-state",
@@ -38602,6 +39072,18 @@ const reactDoctorRules = [
38602
39072
  requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
38603
39073
  }
38604
39074
  },
39075
+ {
39076
+ key: "react-doctor/no-create-ref-in-function-component",
39077
+ id: "no-create-ref-in-function-component",
39078
+ source: "react-doctor",
39079
+ originallyExternal: false,
39080
+ rule: {
39081
+ ...noCreateRefInFunctionComponent,
39082
+ framework: "global",
39083
+ category: "Bugs",
39084
+ requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
39085
+ }
39086
+ },
38605
39087
  {
38606
39088
  key: "react-doctor/no-create-store-in-render",
38607
39089
  id: "no-create-store-in-render",
@@ -38779,6 +39261,17 @@ const reactDoctorRules = [
38779
39261
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
38780
39262
  }
38781
39263
  },
39264
+ {
39265
+ key: "react-doctor/no-document-write",
39266
+ id: "no-document-write",
39267
+ source: "react-doctor",
39268
+ originallyExternal: false,
39269
+ rule: {
39270
+ ...noDocumentWrite,
39271
+ framework: "global",
39272
+ category: "Performance"
39273
+ }
39274
+ },
38782
39275
  {
38783
39276
  key: "react-doctor/no-dynamic-import-path",
38784
39277
  id: "no-dynamic-import-path",
@@ -38976,6 +39469,18 @@ const reactDoctorRules = [
38976
39469
  category: "Accessibility"
38977
39470
  }
38978
39471
  },
39472
+ {
39473
+ key: "react-doctor/no-img-lazy-with-high-fetchpriority",
39474
+ id: "no-img-lazy-with-high-fetchpriority",
39475
+ source: "react-doctor",
39476
+ originallyExternal: false,
39477
+ rule: {
39478
+ ...noImgLazyWithHighFetchpriority,
39479
+ framework: "global",
39480
+ category: "Performance",
39481
+ requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
39482
+ }
39483
+ },
38979
39484
  {
38980
39485
  key: "react-doctor/no-initialize-state",
38981
39486
  id: "no-initialize-state",
@@ -39046,6 +39551,17 @@ const reactDoctorRules = [
39046
39551
  requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
39047
39552
  }
39048
39553
  },
39554
+ {
39555
+ key: "react-doctor/no-json-parse-stringify-clone",
39556
+ id: "no-json-parse-stringify-clone",
39557
+ source: "react-doctor",
39558
+ originallyExternal: false,
39559
+ rule: {
39560
+ ...noJsonParseStringifyClone,
39561
+ framework: "global",
39562
+ category: "Performance"
39563
+ }
39564
+ },
39049
39565
  {
39050
39566
  key: "react-doctor/no-jsx-element-type",
39051
39567
  id: "no-jsx-element-type",
@@ -39565,6 +40081,18 @@ const reactDoctorRules = [
39565
40081
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39566
40082
  }
39567
40083
  },
40084
+ {
40085
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40086
+ id: "no-string-false-on-boolean-attribute",
40087
+ source: "react-doctor",
40088
+ originallyExternal: false,
40089
+ rule: {
40090
+ ...noStringFalseOnBooleanAttribute,
40091
+ framework: "global",
40092
+ category: "Bugs",
40093
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40094
+ }
40095
+ },
39568
40096
  {
39569
40097
  key: "react-doctor/no-string-refs",
39570
40098
  id: "no-string-refs",
@@ -39577,6 +40105,17 @@ const reactDoctorRules = [
39577
40105
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39578
40106
  }
39579
40107
  },
40108
+ {
40109
+ key: "react-doctor/no-sync-xhr",
40110
+ id: "no-sync-xhr",
40111
+ source: "react-doctor",
40112
+ originallyExternal: false,
40113
+ rule: {
40114
+ ...noSyncXhr,
40115
+ framework: "global",
40116
+ category: "Performance"
40117
+ }
40118
+ },
39580
40119
  {
39581
40120
  key: "react-doctor/no-this-in-sfc",
39582
40121
  id: "no-this-in-sfc",