oxlint-plugin-react-doctor 0.5.6-dev.431e515 → 0.5.6-dev.44db3e0

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 +0 -505
  2. package/dist/index.js +290 -954
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
890
890
  });
891
891
  //#endregion
892
892
  //#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
893
- const stripCommentsPreservingPositions = (content) => {
893
+ const WHITESPACE_PATTERN = /\s/;
894
+ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
895
+ for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
896
+ const character = content[cursor];
897
+ if (character === "\\") {
898
+ cursor += 1;
899
+ continue;
900
+ }
901
+ if (character === delimiter) return false;
902
+ if (WHITESPACE_PATTERN.test(character)) return true;
903
+ }
904
+ return false;
905
+ };
906
+ const blankNonCodePreservingPositions = (content, blankStringContents) => {
894
907
  const characters = content.split("");
895
908
  let stringDelimiter = null;
909
+ let isBlankingString = false;
910
+ const templateExpressionDepths = [];
896
911
  let index = 0;
912
+ const blankUnlessNewline = (offset) => {
913
+ if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
914
+ };
897
915
  while (index < content.length) {
898
916
  const character = content[index];
899
917
  const nextCharacter = content[index + 1];
900
918
  if (stringDelimiter !== null) {
901
919
  if (character === "\\") {
920
+ if (isBlankingString) {
921
+ blankUnlessNewline(index);
922
+ blankUnlessNewline(index + 1);
923
+ }
902
924
  index += 2;
903
925
  continue;
904
926
  }
905
- if (character === stringDelimiter) stringDelimiter = null;
927
+ if (character === stringDelimiter) {
928
+ stringDelimiter = null;
929
+ index += 1;
930
+ continue;
931
+ }
932
+ if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
933
+ templateExpressionDepths.push(0);
934
+ stringDelimiter = null;
935
+ index += 2;
936
+ continue;
937
+ }
938
+ if (isBlankingString) blankUnlessNewline(index);
906
939
  index += 1;
907
940
  continue;
908
941
  }
909
- if (character === "\"" || character === "'" || character === "`") {
942
+ if (character === "\"" || character === "'") {
910
943
  stringDelimiter = character;
944
+ isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
945
+ index += 1;
946
+ continue;
947
+ }
948
+ if (character === "`") {
949
+ stringDelimiter = "`";
950
+ isBlankingString = blankStringContents;
911
951
  index += 1;
912
952
  continue;
913
953
  }
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
926
966
  index += 2;
927
967
  break;
928
968
  }
929
- if (content[index] !== "\n") characters[index] = " ";
969
+ blankUnlessNewline(index);
930
970
  index += 1;
931
971
  }
932
972
  continue;
933
973
  }
974
+ if (templateExpressionDepths.length > 0) {
975
+ const innermost = templateExpressionDepths.length - 1;
976
+ if (character === "{") templateExpressionDepths[innermost] += 1;
977
+ else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
978
+ templateExpressionDepths.pop();
979
+ stringDelimiter = "`";
980
+ isBlankingString = blankStringContents;
981
+ } else templateExpressionDepths[innermost] -= 1;
982
+ }
934
983
  index += 1;
935
984
  }
936
985
  return characters.join("");
937
986
  };
987
+ const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
988
+ const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
938
989
  //#endregion
939
990
  //#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
940
991
  const strippedContentCache = /* @__PURE__ */ new WeakMap();
941
- const getScannableContent = (file) => {
992
+ const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
993
+ const getScannableContent = (file, ignoreStringLiterals = false) => {
942
994
  if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
943
- const cachedContent = strippedContentCache.get(file);
995
+ const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
996
+ const cachedContent = cache.get(file);
944
997
  if (cachedContent !== void 0) return cachedContent;
945
- const strippedContent = stripCommentsPreservingPositions(file.content);
946
- strippedContentCache.set(file, strippedContent);
998
+ const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
999
+ cache.set(file, strippedContent);
947
1000
  return strippedContent;
948
1001
  };
949
- const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
1002
+ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
950
1003
  if (!shouldScan(file)) return [];
951
- const content = getScannableContent(file);
1004
+ const content = getScannableContent(file, ignoreStringLiterals);
952
1005
  if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
953
1006
  const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
954
1007
  if (matchedPattern === void 0) return [];
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
973
1026
  shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
974
1027
  pattern: AGENT_TOOL_DEFINITION_PATTERN,
975
1028
  requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
1029
+ ignoreStringLiterals: true,
976
1030
  message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
977
1031
  })
978
1032
  });
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
1861
1915
  });
1862
1916
  //#endregion
1863
1917
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1864
- const MESSAGE$64 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1918
+ const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1865
1919
  const anchorHasContent = defineRule({
1866
1920
  id: "anchor-has-content",
1867
1921
  title: "Anchor has no content",
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
1877
1931
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1878
1932
  context.report({
1879
1933
  node: opening.name,
1880
- message: MESSAGE$64
1934
+ message: MESSAGE$59
1881
1935
  });
1882
1936
  } })
1883
1937
  });
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
2271
2325
  };
2272
2326
  //#endregion
2273
2327
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2274
- const MESSAGE$63 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2328
+ const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2275
2329
  const ariaActivedescendantHasTabindex = defineRule({
2276
2330
  id: "aria-activedescendant-has-tabindex",
2277
2331
  title: "aria-activedescendant missing tabindex",
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2289
2343
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2290
2344
  context.report({
2291
2345
  node: node.name,
2292
- message: MESSAGE$63
2346
+ message: MESSAGE$58
2293
2347
  });
2294
2348
  return;
2295
2349
  }
2296
2350
  if (isInteractiveElement(tag, node)) return;
2297
2351
  context.report({
2298
2352
  node: node.name,
2299
- message: MESSAGE$63
2353
+ message: MESSAGE$58
2300
2354
  });
2301
2355
  } })
2302
2356
  });
@@ -2965,7 +3019,7 @@ const ABSTRACT_ROLES = new Set([
2965
3019
  "widget",
2966
3020
  "window"
2967
3021
  ]);
2968
- const PRESENTATION_ROLES$2 = new Set(["presentation", "none"]);
3022
+ const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
2969
3023
  //#endregion
2970
3024
  //#region src/plugin/rules/a11y/aria-role.ts
2971
3025
  const buildBaseMessage = (suffix) => `This \`role\` is not a valid ARIA role, so assistive tech cannot expose it correctly. Use a real, non-abstract role.${suffix}`;
@@ -4272,7 +4326,7 @@ const asyncParallel = defineRule({
4272
4326
  });
4273
4327
  //#endregion
4274
4328
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4275
- const MESSAGE$62 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4329
+ const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4276
4330
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4277
4331
  const STORAGE_GLOBALS = new Set([
4278
4332
  "window",
@@ -4306,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
4306
4360
  if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4307
4361
  context.report({
4308
4362
  node,
4309
- message: MESSAGE$62
4363
+ message: MESSAGE$57
4310
4364
  });
4311
4365
  },
4312
4366
  AssignmentExpression(node) {
@@ -4317,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
4317
4371
  if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4318
4372
  context.report({
4319
4373
  node: target,
4320
- message: MESSAGE$62
4374
+ message: MESSAGE$57
4321
4375
  });
4322
4376
  }
4323
4377
  })
@@ -4656,6 +4710,14 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
4656
4710
  }
4657
4711
  });
4658
4712
  //#endregion
4713
+ //#region src/plugin/utils/is-presentation-role.ts
4714
+ const isPresentationRole = (openingElement) => {
4715
+ const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
4716
+ if (!roleAttribute) return false;
4717
+ const value = getJsxPropStringValue(roleAttribute);
4718
+ return value !== null && PRESENTATION_ROLES$1.has(value);
4719
+ };
4720
+ //#endregion
4659
4721
  //#region src/plugin/utils/is-pure-event-blocker-handler.ts
4660
4722
  const BLOCKER_METHOD_NAMES = new Set([
4661
4723
  "stopPropagation",
@@ -4693,8 +4755,7 @@ const isPureEventBlockerHandler = (attribute) => {
4693
4755
  };
4694
4756
  //#endregion
4695
4757
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4696
- const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4697
- const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4758
+ const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4698
4759
  const KEY_HANDLERS = [
4699
4760
  "onKeyUp",
4700
4761
  "onKeyDown",
@@ -4718,15 +4779,11 @@ const clickEventsHaveKeyEvents = defineRule({
4718
4779
  if (!onClick) return;
4719
4780
  if (isPureEventBlockerHandler(onClick)) return;
4720
4781
  if (isHiddenFromScreenReader(node, context.settings)) return;
4721
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
4722
- if (roleAttribute) {
4723
- const roleValue = getJsxPropStringValue(roleAttribute);
4724
- if (roleValue && PRESENTATION_ROLES$1.has(roleValue)) return;
4725
- }
4782
+ if (isPresentationRole(node)) return;
4726
4783
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4727
4784
  context.report({
4728
4785
  node: node.name,
4729
- message: MESSAGE$61
4786
+ message: MESSAGE$56
4730
4787
  });
4731
4788
  } };
4732
4789
  }
@@ -4841,7 +4898,7 @@ const isReactComponentName = (name) => {
4841
4898
  };
4842
4899
  //#endregion
4843
4900
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4844
- const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4901
+ const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4845
4902
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4846
4903
  const DEFAULT_LABELLING_PROPS = [
4847
4904
  "alt",
@@ -5002,7 +5059,7 @@ const controlHasAssociatedLabel = defineRule({
5002
5059
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
5003
5060
  context.report({
5004
5061
  node: opening,
5005
- message: MESSAGE$60
5062
+ message: MESSAGE$55
5006
5063
  });
5007
5064
  } };
5008
5065
  }
@@ -5131,7 +5188,6 @@ const dangerousHtmlSink = defineRule({
5131
5188
  return findings;
5132
5189
  }
5133
5190
  });
5134
- const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5135
5191
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5136
5192
  const VAGUE_BUTTON_LABELS = new Set([
5137
5193
  "continue",
@@ -5430,10 +5486,10 @@ const noVagueButtonLabel = defineRule({
5430
5486
  });
5431
5487
  //#endregion
5432
5488
  //#region src/plugin/utils/has-jsx-spread-attribute.ts
5433
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5489
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5434
5490
  //#endregion
5435
5491
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5436
- const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5492
+ 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.";
5437
5493
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5438
5494
  const NAME_PROVIDING_ATTRIBUTES = [
5439
5495
  "aria-label",
@@ -5452,11 +5508,11 @@ const dialogHasAccessibleName = defineRule({
5452
5508
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5453
5509
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5454
5510
  if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5455
- if (hasJsxSpreadAttribute(node.attributes)) return;
5511
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5456
5512
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5457
5513
  context.report({
5458
5514
  node: node.name,
5459
- message: MESSAGE$59
5515
+ message: MESSAGE$54
5460
5516
  });
5461
5517
  } })
5462
5518
  });
@@ -5495,7 +5551,7 @@ const isEs6Component = (node) => {
5495
5551
  };
5496
5552
  //#endregion
5497
5553
  //#region src/plugin/rules/react-builtins/display-name.ts
5498
- const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5554
+ const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5499
5555
  const DEFAULT_ADDITIONAL_HOCS = [
5500
5556
  "observer",
5501
5557
  "lazy",
@@ -5694,11 +5750,11 @@ const displayName = defineRule({
5694
5750
  category: "Architecture",
5695
5751
  create: (context) => {
5696
5752
  const settings = resolveSettings$44(context.settings);
5697
- const ignoreNamed = settings.ignoreTranspilerName ? false : true;
5753
+ const ignoreNamed = !settings.ignoreTranspilerName;
5698
5754
  const reportAt = (node) => {
5699
5755
  context.report({
5700
5756
  node,
5701
- message: MESSAGE$58
5757
+ message: MESSAGE$53
5702
5758
  });
5703
5759
  };
5704
5760
  return {
@@ -7846,7 +7902,7 @@ const forbidElements = defineRule({
7846
7902
  });
7847
7903
  //#endregion
7848
7904
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7849
- const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7905
+ const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7850
7906
  const forwardRefUsesRef = defineRule({
7851
7907
  id: "forward-ref-uses-ref",
7852
7908
  title: "forwardRef without ref parameter",
@@ -7866,7 +7922,7 @@ const forwardRefUsesRef = defineRule({
7866
7922
  if (isNodeOfType(onlyParam, "RestElement")) return;
7867
7923
  context.report({
7868
7924
  node: inner,
7869
- message: MESSAGE$57
7925
+ message: MESSAGE$52
7870
7926
  });
7871
7927
  } })
7872
7928
  });
@@ -7903,7 +7959,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7903
7959
  });
7904
7960
  //#endregion
7905
7961
  //#region src/plugin/rules/a11y/heading-has-content.ts
7906
- const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7962
+ 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`.";
7907
7963
  const DEFAULT_HEADING_TAGS = [
7908
7964
  "h1",
7909
7965
  "h2",
@@ -7936,7 +7992,7 @@ const headingHasContent = defineRule({
7936
7992
  if (isHiddenFromScreenReader(node, context.settings)) return;
7937
7993
  context.report({
7938
7994
  node,
7939
- message: MESSAGE$56
7995
+ message: MESSAGE$51
7940
7996
  });
7941
7997
  } };
7942
7998
  }
@@ -8074,7 +8130,7 @@ const hooksNoNanInDeps = defineRule({
8074
8130
  });
8075
8131
  //#endregion
8076
8132
  //#region src/plugin/rules/a11y/html-has-lang.ts
8077
- const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8133
+ const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8078
8134
  const resolveSettings$38 = (settings) => {
8079
8135
  const reactDoctor = settings?.["react-doctor"];
8080
8136
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8117,26 +8173,17 @@ const htmlHasLang = defineRule({
8117
8173
  return { JSXOpeningElement(node) {
8118
8174
  const tag = getElementType(node, context.settings);
8119
8175
  if (!tagSet.has(tag)) return;
8120
- const hasSpread = node.attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
8121
8176
  const lang = hasJsxPropIgnoreCase(node.attributes, "lang");
8122
8177
  if (!lang) {
8123
8178
  context.report({
8124
8179
  node: node.name,
8125
- message: MESSAGE$55
8126
- });
8127
- return;
8128
- }
8129
- const verdict = evaluateLang(lang.value);
8130
- if (verdict === "missing" || verdict === "empty") {
8131
- context.report({
8132
- node: lang,
8133
- message: MESSAGE$55
8180
+ message: MESSAGE$50
8134
8181
  });
8135
8182
  return;
8136
8183
  }
8137
- if (hasSpread && !lang) context.report({
8138
- node: node.name,
8139
- message: MESSAGE$55
8184
+ if (evaluateLang(lang.value) === "empty") context.report({
8185
+ node: lang,
8186
+ message: MESSAGE$50
8140
8187
  });
8141
8188
  } };
8142
8189
  }
@@ -8350,7 +8397,7 @@ const htmlNoNestedInteractive = defineRule({
8350
8397
  });
8351
8398
  //#endregion
8352
8399
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8353
- const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8400
+ const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8354
8401
  const evaluateTitleValue = (value) => {
8355
8402
  if (!value) return "missing";
8356
8403
  if (isNodeOfType(value, "Literal")) {
@@ -8390,14 +8437,14 @@ const iframeHasTitle = defineRule({
8390
8437
  if (!titleAttr) {
8391
8438
  if (hasSpread || tag === "iframe") context.report({
8392
8439
  node: node.name,
8393
- message: MESSAGE$54
8440
+ message: MESSAGE$49
8394
8441
  });
8395
8442
  return;
8396
8443
  }
8397
8444
  const verdict = evaluateTitleValue(titleAttr.value);
8398
8445
  if (verdict === "missing" || verdict === "empty") context.report({
8399
8446
  node: titleAttr,
8400
- message: MESSAGE$54
8447
+ message: MESSAGE$49
8401
8448
  });
8402
8449
  } })
8403
8450
  });
@@ -8501,7 +8548,7 @@ const iframeMissingSandbox = defineRule({
8501
8548
  });
8502
8549
  //#endregion
8503
8550
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8504
- const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8551
+ const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8505
8552
  const DEFAULT_COMPONENTS = ["img"];
8506
8553
  const DEFAULT_REDUNDANT_WORDS = [
8507
8554
  "image",
@@ -8566,7 +8613,7 @@ const imgRedundantAlt = defineRule({
8566
8613
  if (!altAttribute) return;
8567
8614
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8568
8615
  node: altAttribute,
8569
- message: MESSAGE$53
8616
+ message: MESSAGE$48
8570
8617
  });
8571
8618
  } };
8572
8619
  }
@@ -8849,14 +8896,6 @@ const isNonInteractiveElement = (elementType, openingElement) => {
8849
8896
  //#region src/plugin/utils/is-non-interactive-role.ts
8850
8897
  const isNonInteractiveRole = (role) => NON_INTERACTIVE_ROLES.has(role);
8851
8898
  //#endregion
8852
- //#region src/plugin/utils/is-presentation-role.ts
8853
- const isPresentationRole = (openingElement) => {
8854
- const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
8855
- if (!roleAttribute) return false;
8856
- const value = getJsxPropStringValue(roleAttribute);
8857
- return value !== null && PRESENTATION_ROLES$2.has(value);
8858
- };
8859
- //#endregion
8860
8899
  //#region src/plugin/rules/a11y/interactive-supports-focus.ts
8861
8900
  const buildTabbableMessage = (role) => `Keyboard users can't tab to this '${role}' because it isn't focusable, so add \`tabIndex={0}\`.`;
8862
8901
  const buildFocusableMessage = (role) => `Keyboard users can't focus this '${role}' because it can't receive focus, so add \`tabIndex={0}\` or \`tabIndex={-1}\`.`;
@@ -10631,6 +10670,24 @@ const hasJsxKeyAttribute = (openingElement) => {
10631
10670
  return false;
10632
10671
  };
10633
10672
  //#endregion
10673
+ //#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
10674
+ const ascendThroughJsxValueWrappers = (node) => {
10675
+ let current = node;
10676
+ while (current.parent) {
10677
+ const parent = current.parent;
10678
+ if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
10679
+ current = parent;
10680
+ }
10681
+ return current;
10682
+ };
10683
+ const isNonChildrenJsxAttributeValue = (node) => {
10684
+ const container = ascendThroughJsxValueWrappers(node).parent;
10685
+ if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
10686
+ const attribute = container.parent;
10687
+ if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
10688
+ return getJsxAttributeName(attribute.name) !== "children";
10689
+ };
10690
+ //#endregion
10634
10691
  //#region src/plugin/rules/react-builtins/jsx-key.ts
10635
10692
  const ITERATOR_METHOD_NAMES = new Set([
10636
10693
  "map",
@@ -10669,6 +10726,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
10669
10726
  const arrayParent = parent.parent;
10670
10727
  if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
10671
10728
  if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
10729
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10672
10730
  return { kind: "array" };
10673
10731
  } else if (isNodeOfType(parent, "CallExpression")) {
10674
10732
  const callee = parent.callee;
@@ -10681,10 +10739,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
10681
10739
  if (!targetArg) return null;
10682
10740
  let walker = current;
10683
10741
  while (walker && walker !== parent) {
10684
- if (walker === targetArg) return {
10685
- kind: "iterator",
10686
- callExpression: parent
10687
- };
10742
+ if (walker === targetArg) {
10743
+ if (isNonChildrenJsxAttributeValue(parent)) return null;
10744
+ return {
10745
+ kind: "iterator",
10746
+ callExpression: parent
10747
+ };
10748
+ }
10688
10749
  walker = walker.parent ?? null;
10689
10750
  }
10690
10751
  return null;
@@ -10923,7 +10984,7 @@ const jsxMaxDepth = defineRule({
10923
10984
  });
10924
10985
  //#endregion
10925
10986
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10926
- const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10987
+ const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10927
10988
  const LITERAL_TEXT_TAGS = new Set([
10928
10989
  "code",
10929
10990
  "pre",
@@ -10959,7 +11020,7 @@ const jsxNoCommentTextnodes = defineRule({
10959
11020
  if (isInsideLiteralTextTag(node)) return;
10960
11021
  context.report({
10961
11022
  node,
10962
- message: MESSAGE$52
11023
+ message: MESSAGE$47
10963
11024
  });
10964
11025
  } })
10965
11026
  });
@@ -10990,7 +11051,7 @@ const isInsideFunctionScope = (node) => {
10990
11051
  };
10991
11052
  //#endregion
10992
11053
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10993
- const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
11054
+ const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10994
11055
  const CONTEXT_MODULES$1 = [
10995
11056
  "react",
10996
11057
  "use-context-selector",
@@ -11088,7 +11149,7 @@ const jsxNoConstructedContextValues = defineRule({
11088
11149
  if (!isConstructedValue(innerExpression)) continue;
11089
11150
  context.report({
11090
11151
  node: attribute,
11091
- message: MESSAGE$51
11152
+ message: MESSAGE$46
11092
11153
  });
11093
11154
  }
11094
11155
  }
@@ -11174,7 +11235,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11174
11235
  };
11175
11236
  //#endregion
11176
11237
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11177
- const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
11238
+ const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11178
11239
  const KNOWN_SLOT_PROP_NAMES = new Set([
11179
11240
  "icon",
11180
11241
  "Icon",
@@ -11443,7 +11504,7 @@ const jsxNoJsxAsProp = defineRule({
11443
11504
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11444
11505
  context.report({
11445
11506
  node,
11446
- message: MESSAGE$50
11507
+ message: MESSAGE$45
11447
11508
  });
11448
11509
  }
11449
11510
  };
@@ -11731,7 +11792,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11731
11792
  ];
11732
11793
  //#endregion
11733
11794
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11734
- const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
11795
+ const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11735
11796
  const isDataArrayPropName = (propName) => {
11736
11797
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11737
11798
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11815,7 +11876,7 @@ const jsxNoNewArrayAsProp = defineRule({
11815
11876
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11816
11877
  context.report({
11817
11878
  node,
11818
- message: MESSAGE$49
11879
+ message: MESSAGE$44
11819
11880
  });
11820
11881
  }
11821
11882
  };
@@ -12073,7 +12134,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12073
12134
  ]);
12074
12135
  //#endregion
12075
12136
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
12076
- const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
12137
+ const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
12077
12138
  const isAccessorPredicateName = (propName) => {
12078
12139
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12079
12140
  if (propName.length <= prefix.length) continue;
@@ -12279,7 +12340,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12279
12340
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12280
12341
  context.report({
12281
12342
  node,
12282
- message: MESSAGE$48
12343
+ message: MESSAGE$43
12283
12344
  });
12284
12345
  }
12285
12346
  };
@@ -12499,7 +12560,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12499
12560
  ];
12500
12561
  //#endregion
12501
12562
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12502
- const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
12563
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12503
12564
  const isConfigObjectPropName = (propName) => {
12504
12565
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12505
12566
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12587,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
12587
12648
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12588
12649
  context.report({
12589
12650
  node,
12590
- message: MESSAGE$47
12651
+ message: MESSAGE$42
12591
12652
  });
12592
12653
  }
12593
12654
  };
@@ -12595,7 +12656,7 @@ const jsxNoNewObjectAsProp = defineRule({
12595
12656
  });
12596
12657
  //#endregion
12597
12658
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12598
- const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12659
+ const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12599
12660
  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;
12600
12661
  const resolveSettings$28 = (settings) => {
12601
12662
  const reactDoctor = settings?.["react-doctor"];
@@ -12636,7 +12697,7 @@ const jsxNoScriptUrl = defineRule({
12636
12697
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12637
12698
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12638
12699
  node: attribute,
12639
- message: MESSAGE$46
12700
+ message: MESSAGE$41
12640
12701
  });
12641
12702
  }
12642
12703
  } };
@@ -12951,7 +13012,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12951
13012
  });
12952
13013
  //#endregion
12953
13014
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12954
- const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
13015
+ const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12955
13016
  const resolveSettings$25 = (settings) => {
12956
13017
  const reactDoctor = settings?.["react-doctor"];
12957
13018
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12992,7 +13053,7 @@ const jsxPropsNoSpreading = defineRule({
12992
13053
  }
12993
13054
  context.report({
12994
13055
  node: attribute,
12995
- message: MESSAGE$45
13056
+ message: MESSAGE$40
12996
13057
  });
12997
13058
  }
12998
13059
  } };
@@ -13220,7 +13281,7 @@ const labelHasAssociatedControl = defineRule({
13220
13281
  });
13221
13282
  //#endregion
13222
13283
  //#region src/plugin/rules/a11y/lang.ts
13223
- const MESSAGE$44 = "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`.";
13284
+ 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`.";
13224
13285
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13225
13286
  "aa",
13226
13287
  "ab",
@@ -13432,7 +13493,7 @@ const lang = defineRule({
13432
13493
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13433
13494
  context.report({
13434
13495
  node: langAttr,
13435
- message: MESSAGE$44
13496
+ message: MESSAGE$39
13436
13497
  });
13437
13498
  return;
13438
13499
  }
@@ -13441,7 +13502,7 @@ const lang = defineRule({
13441
13502
  if (value === null) return;
13442
13503
  if (!isValidLangTag(value)) context.report({
13443
13504
  node: langAttr,
13444
- message: MESSAGE$44
13505
+ message: MESSAGE$39
13445
13506
  });
13446
13507
  } })
13447
13508
  });
@@ -13467,6 +13528,7 @@ const mcpToolCapabilityRisk = defineRule({
13467
13528
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13468
13529
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13469
13530
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13531
+ ignoreStringLiterals: true,
13470
13532
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13471
13533
  })
13472
13534
  });
@@ -13485,7 +13547,7 @@ const mdxSsrExecutionRisk = defineRule({
13485
13547
  });
13486
13548
  //#endregion
13487
13549
  //#region src/plugin/rules/a11y/media-has-caption.ts
13488
- const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13550
+ const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13489
13551
  const DEFAULT_AUDIO = ["audio"];
13490
13552
  const DEFAULT_VIDEO = ["video"];
13491
13553
  const DEFAULT_TRACK = ["track"];
@@ -13526,7 +13588,7 @@ const mediaHasCaption = defineRule({
13526
13588
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13527
13589
  context.report({
13528
13590
  node: node.name,
13529
- message: MESSAGE$43
13591
+ message: MESSAGE$38
13530
13592
  });
13531
13593
  return;
13532
13594
  }
@@ -13543,7 +13605,7 @@ const mediaHasCaption = defineRule({
13543
13605
  return kindValue.value.toLowerCase() === "captions";
13544
13606
  })) context.report({
13545
13607
  node: node.name,
13546
- message: MESSAGE$43
13608
+ message: MESSAGE$38
13547
13609
  });
13548
13610
  } };
13549
13611
  }
@@ -15344,7 +15406,7 @@ const nextjsNoVercelOgImport = defineRule({
15344
15406
  });
15345
15407
  //#endregion
15346
15408
  //#region src/plugin/rules/a11y/no-access-key.ts
15347
- const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15409
+ const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15348
15410
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15349
15411
  const noAccessKey = defineRule({
15350
15412
  id: "no-access-key",
@@ -15361,7 +15423,7 @@ const noAccessKey = defineRule({
15361
15423
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15362
15424
  context.report({
15363
15425
  node: accessKey,
15364
- message: MESSAGE$42
15426
+ message: MESSAGE$37
15365
15427
  });
15366
15428
  return;
15367
15429
  }
@@ -15371,7 +15433,7 @@ const noAccessKey = defineRule({
15371
15433
  if (isUndefinedIdentifier(expression)) return;
15372
15434
  context.report({
15373
15435
  node: accessKey,
15374
- message: MESSAGE$42
15436
+ message: MESSAGE$37
15375
15437
  });
15376
15438
  }
15377
15439
  } })
@@ -15853,41 +15915,8 @@ const noAdjustStateOnPropChange = defineRule({
15853
15915
  } })
15854
15916
  });
15855
15917
  //#endregion
15856
- //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
15857
- const getStringFromClassNameAttr = (node) => {
15858
- if (!isNodeOfType(node, "JSXOpeningElement")) return null;
15859
- const classAttr = findJsxAttribute(node.attributes ?? [], "className");
15860
- if (!classAttr?.value) return null;
15861
- if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
15862
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
15863
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
15864
- return null;
15865
- };
15866
- //#endregion
15867
- //#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
15868
- const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
15869
- const noArbitraryPxFontSize = defineRule({
15870
- id: "no-arbitrary-px-font-size",
15871
- title: "Pixel arbitrary font size",
15872
- tags: ["design", "test-noise"],
15873
- severity: "warn",
15874
- category: "Accessibility",
15875
- recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
15876
- create: (context) => ({ JSXOpeningElement(node) {
15877
- const classNameValue = getStringFromClassNameAttr(node);
15878
- if (!classNameValue) return;
15879
- for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
15880
- const rem = parseFloat(match[1]) / 16;
15881
- context.report({
15882
- node,
15883
- message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
15884
- });
15885
- }
15886
- } })
15887
- });
15888
- //#endregion
15889
15918
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15890
- const MESSAGE$41 = "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.";
15919
+ 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.";
15891
15920
  const noAriaHiddenOnFocusable = defineRule({
15892
15921
  id: "no-aria-hidden-on-focusable",
15893
15922
  title: "aria-hidden on focusable element",
@@ -15914,7 +15943,7 @@ const noAriaHiddenOnFocusable = defineRule({
15914
15943
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15915
15944
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15916
15945
  node: ariaHidden,
15917
- message: MESSAGE$41
15946
+ message: MESSAGE$36
15918
15947
  });
15919
15948
  } })
15920
15949
  });
@@ -16282,7 +16311,7 @@ const noArrayIndexAsKey = defineRule({
16282
16311
  });
16283
16312
  //#endregion
16284
16313
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16285
- const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
16314
+ const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16286
16315
  const SECOND_INDEX_METHODS = new Set([
16287
16316
  "every",
16288
16317
  "filter",
@@ -16486,7 +16515,7 @@ const noArrayIndexKey = defineRule({
16486
16515
  }
16487
16516
  context.report({
16488
16517
  node: keyAttribute,
16489
- message: MESSAGE$40
16518
+ message: MESSAGE$35
16490
16519
  });
16491
16520
  },
16492
16521
  CallExpression(node) {
@@ -16506,7 +16535,7 @@ const noArrayIndexKey = defineRule({
16506
16535
  if (propName !== "key") continue;
16507
16536
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16508
16537
  node: property,
16509
- message: MESSAGE$40
16538
+ message: MESSAGE$35
16510
16539
  });
16511
16540
  }
16512
16541
  }
@@ -16514,7 +16543,7 @@ const noArrayIndexKey = defineRule({
16514
16543
  });
16515
16544
  //#endregion
16516
16545
  //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16517
- const MESSAGE$39 = "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.";
16546
+ 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.";
16518
16547
  const noAsyncEffectCallback = defineRule({
16519
16548
  id: "no-async-effect-callback",
16520
16549
  title: "Async effect callback",
@@ -16528,13 +16557,13 @@ const noAsyncEffectCallback = defineRule({
16528
16557
  if (!callback.async) return;
16529
16558
  context.report({
16530
16559
  node: callback,
16531
- message: MESSAGE$39
16560
+ message: MESSAGE$34
16532
16561
  });
16533
16562
  } })
16534
16563
  });
16535
16564
  //#endregion
16536
16565
  //#region src/plugin/rules/a11y/no-autofocus.ts
16537
- const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16566
+ 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.";
16538
16567
  const resolveSettings$21 = (settings) => {
16539
16568
  const reactDoctor = settings?.["react-doctor"];
16540
16569
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16590,45 +16619,12 @@ const noAutofocus = defineRule({
16590
16619
  }
16591
16620
  context.report({
16592
16621
  node: autoFocusAttribute,
16593
- message: MESSAGE$38
16622
+ message: MESSAGE$33
16594
16623
  });
16595
16624
  } };
16596
16625
  }
16597
16626
  });
16598
16627
  //#endregion
16599
- //#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
16600
- const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
16601
- const resolveStaticBoolean = (attribute) => {
16602
- const value = attribute.value;
16603
- if (!value) return true;
16604
- const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
16605
- if (isNodeOfType(literal, "Literal")) {
16606
- if (literal.value === true || literal.value === "true") return true;
16607
- if (literal.value === false || literal.value === "false") return false;
16608
- }
16609
- return null;
16610
- };
16611
- const noAutoplayWithoutMuted = defineRule({
16612
- id: "no-autoplay-without-muted",
16613
- title: "Autoplaying media without muted",
16614
- severity: "warn",
16615
- recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
16616
- create: (context) => ({ JSXOpeningElement(node) {
16617
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
16618
- const tagName = node.name.name;
16619
- if (tagName !== "video" && tagName !== "audio") return;
16620
- if (hasJsxSpreadAttribute(node.attributes)) return;
16621
- const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
16622
- if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
16623
- const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
16624
- if (muted && resolveStaticBoolean(muted) !== false) return;
16625
- context.report({
16626
- node: node.name,
16627
- message: MESSAGE$37
16628
- });
16629
- } })
16630
- });
16631
- //#endregion
16632
16628
  //#region src/plugin/utils/create-relative-import-source.ts
16633
16629
  const createRelativeImportSource = (filename, targetFilePath) => {
16634
16630
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -17127,7 +17123,7 @@ const noChainStateUpdates = defineRule({
17127
17123
  });
17128
17124
  //#endregion
17129
17125
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
17130
- const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17126
+ const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17131
17127
  const noChildrenProp = defineRule({
17132
17128
  id: "no-children-prop",
17133
17129
  title: "Children passed as a prop",
@@ -17139,7 +17135,7 @@ const noChildrenProp = defineRule({
17139
17135
  if (node.name.name !== "children") return;
17140
17136
  context.report({
17141
17137
  node: node.name,
17142
- message: MESSAGE$36
17138
+ message: MESSAGE$32
17143
17139
  });
17144
17140
  },
17145
17141
  CallExpression(node) {
@@ -17152,7 +17148,7 @@ const noChildrenProp = defineRule({
17152
17148
  const propertyKey = property.key;
17153
17149
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17154
17150
  node: propertyKey,
17155
- message: MESSAGE$36
17151
+ message: MESSAGE$32
17156
17152
  });
17157
17153
  }
17158
17154
  }
@@ -17160,7 +17156,7 @@ const noChildrenProp = defineRule({
17160
17156
  });
17161
17157
  //#endregion
17162
17158
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
17163
- const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17159
+ const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17164
17160
  const noCloneElement = defineRule({
17165
17161
  id: "no-clone-element",
17166
17162
  title: "cloneElement makes child props fragile",
@@ -17173,7 +17169,7 @@ const noCloneElement = defineRule({
17173
17169
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17174
17170
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17175
17171
  node: callee,
17176
- message: MESSAGE$35
17172
+ message: MESSAGE$31
17177
17173
  });
17178
17174
  return;
17179
17175
  }
@@ -17186,7 +17182,7 @@ const noCloneElement = defineRule({
17186
17182
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17187
17183
  context.report({
17188
17184
  node: callee,
17189
- message: MESSAGE$35
17185
+ message: MESSAGE$31
17190
17186
  });
17191
17187
  }
17192
17188
  } })
@@ -17235,7 +17231,7 @@ const enclosingComponentOrHookName = (node) => {
17235
17231
  };
17236
17232
  //#endregion
17237
17233
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
17238
- const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17234
+ const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17239
17235
  const CONTEXT_MODULES = [
17240
17236
  "react",
17241
17237
  "use-context-selector",
@@ -17271,13 +17267,13 @@ const noCreateContextInRender = defineRule({
17271
17267
  if (!componentOrHookName) return;
17272
17268
  context.report({
17273
17269
  node,
17274
- message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17270
+ message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17275
17271
  });
17276
17272
  } })
17277
17273
  });
17278
17274
  //#endregion
17279
17275
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17280
- const MESSAGE$33 = "`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.";
17276
+ 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.";
17281
17277
  const noCreateRefInFunctionComponent = defineRule({
17282
17278
  id: "no-create-ref-in-function-component",
17283
17279
  title: "createRef in function component",
@@ -17296,7 +17292,7 @@ const noCreateRefInFunctionComponent = defineRule({
17296
17292
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17297
17293
  context.report({
17298
17294
  node,
17299
- message: MESSAGE$33
17295
+ message: MESSAGE$29
17300
17296
  });
17301
17297
  } })
17302
17298
  });
@@ -17436,12 +17432,13 @@ const noCreateStoreInRender = defineRule({
17436
17432
  });
17437
17433
  //#endregion
17438
17434
  //#region src/plugin/rules/react-builtins/no-danger.ts
17439
- const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17435
+ const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17440
17436
  const noDanger = defineRule({
17441
17437
  id: "no-danger",
17442
17438
  title: "Raw HTML injection can run unsafe markup",
17443
17439
  severity: "warn",
17444
17440
  category: "Security",
17441
+ defaultEnabled: false,
17445
17442
  recommendation: "Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.",
17446
17443
  create: (context) => ({
17447
17444
  JSXOpeningElement(node) {
@@ -17449,7 +17446,7 @@ const noDanger = defineRule({
17449
17446
  if (!propAttribute) return;
17450
17447
  context.report({
17451
17448
  node: propAttribute.name,
17452
- message: MESSAGE$32
17449
+ message: MESSAGE$28
17453
17450
  });
17454
17451
  },
17455
17452
  CallExpression(node) {
@@ -17461,7 +17458,7 @@ const noDanger = defineRule({
17461
17458
  const propertyKey = property.key;
17462
17459
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17463
17460
  node: propertyKey,
17464
- message: MESSAGE$32
17461
+ message: MESSAGE$28
17465
17462
  });
17466
17463
  }
17467
17464
  }
@@ -17469,7 +17466,7 @@ const noDanger = defineRule({
17469
17466
  });
17470
17467
  //#endregion
17471
17468
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17472
- const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17469
+ const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17473
17470
  const isLineBreak = (child) => {
17474
17471
  if (!isNodeOfType(child, "JSXText")) return false;
17475
17472
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17539,7 +17536,7 @@ const noDangerWithChildren = defineRule({
17539
17536
  if (!hasChildrenProp && !hasNestedChildren) return;
17540
17537
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17541
17538
  node: opening,
17542
- message: MESSAGE$31
17539
+ message: MESSAGE$27
17543
17540
  });
17544
17541
  },
17545
17542
  CallExpression(node) {
@@ -17551,7 +17548,7 @@ const noDangerWithChildren = defineRule({
17551
17548
  if (!propsShape.hasDangerously) return;
17552
17549
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17553
17550
  node,
17554
- message: MESSAGE$31
17551
+ message: MESSAGE$27
17555
17552
  });
17556
17553
  }
17557
17554
  })
@@ -17716,37 +17713,6 @@ const noDefaultProps = defineRule({
17716
17713
  } })
17717
17714
  });
17718
17715
  //#endregion
17719
- //#region src/plugin/utils/get-class-name-tokens.ts
17720
- const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
17721
- //#endregion
17722
- //#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
17723
- const renameDeprecatedToken = (token) => {
17724
- if (token === "overflow-ellipsis") return "text-ellipsis";
17725
- if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
17726
- if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
17727
- if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
17728
- return null;
17729
- };
17730
- const noDeprecatedTailwindClass = defineRule({
17731
- id: "no-deprecated-tailwind-class",
17732
- title: "Deprecated Tailwind v4 utility",
17733
- tags: ["design", "test-noise"],
17734
- severity: "warn",
17735
- requires: ["tailwind:4"],
17736
- recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
17737
- create: (context) => ({ JSXOpeningElement(node) {
17738
- const classNameValue = getStringFromClassNameAttr(node);
17739
- if (!classNameValue) return;
17740
- for (const token of getClassNameTokens(classNameValue)) {
17741
- const replacement = renameDeprecatedToken(token);
17742
- if (replacement) context.report({
17743
- node,
17744
- message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
17745
- });
17746
- }
17747
- } })
17748
- });
17749
- //#endregion
17750
17716
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17751
17717
  const isInitialOnlyPropName = (propName) => {
17752
17718
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -18159,7 +18125,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
18159
18125
  //#endregion
18160
18126
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
18161
18127
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
18162
- const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18128
+ const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18163
18129
  const resolveSettings$20 = (settings) => {
18164
18130
  const reactDoctor = settings?.["react-doctor"];
18165
18131
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18178,7 +18144,7 @@ const noDidMountSetState = defineRule({
18178
18144
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18179
18145
  context.report({
18180
18146
  node: node.callee,
18181
- message: MESSAGE$30
18147
+ message: MESSAGE$26
18182
18148
  });
18183
18149
  } };
18184
18150
  }
@@ -18186,7 +18152,7 @@ const noDidMountSetState = defineRule({
18186
18152
  //#endregion
18187
18153
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18188
18154
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
18189
- const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18155
+ const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18190
18156
  const resolveSettings$19 = (settings) => {
18191
18157
  const reactDoctor = settings?.["react-doctor"];
18192
18158
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18205,7 +18171,7 @@ const noDidUpdateSetState = defineRule({
18205
18171
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18206
18172
  context.report({
18207
18173
  node: node.callee,
18208
- message: MESSAGE$29
18174
+ message: MESSAGE$25
18209
18175
  });
18210
18176
  } };
18211
18177
  }
@@ -18228,7 +18194,7 @@ const isStateMemberExpression = (node) => {
18228
18194
  };
18229
18195
  //#endregion
18230
18196
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
18231
- const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18197
+ const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18232
18198
  const shouldIgnoreMutation = (node) => {
18233
18199
  let isConstructor = false;
18234
18200
  let isInsideCallExpression = false;
@@ -18250,7 +18216,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18250
18216
  if (shouldIgnoreMutation(reportNode)) return;
18251
18217
  context.report({
18252
18218
  node: reportNode,
18253
- message: MESSAGE$28
18219
+ message: MESSAGE$24
18254
18220
  });
18255
18221
  };
18256
18222
  const noDirectMutationState = defineRule({
@@ -18461,7 +18427,7 @@ const noDocumentStartViewTransition = defineRule({
18461
18427
  });
18462
18428
  //#endregion
18463
18429
  //#region src/plugin/rules/js-performance/no-document-write.ts
18464
- const MESSAGE$27 = "`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.";
18430
+ 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.";
18465
18431
  const WRITE_METHODS = new Set(["write", "writeln"]);
18466
18432
  const noDocumentWrite = defineRule({
18467
18433
  id: "no-document-write",
@@ -18475,7 +18441,7 @@ const noDocumentWrite = defineRule({
18475
18441
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18476
18442
  context.report({
18477
18443
  node,
18478
- message: MESSAGE$27
18444
+ message: MESSAGE$23
18479
18445
  });
18480
18446
  } })
18481
18447
  });
@@ -19858,7 +19824,7 @@ const ALLOWED_NAMESPACES = new Set([
19858
19824
  "ReactDOM",
19859
19825
  "ReactDom"
19860
19826
  ]);
19861
- const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19827
+ const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19862
19828
  const noFindDomNode = defineRule({
19863
19829
  id: "no-find-dom-node",
19864
19830
  title: "findDOMNode breaks component encapsulation",
@@ -19869,7 +19835,7 @@ const noFindDomNode = defineRule({
19869
19835
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19870
19836
  context.report({
19871
19837
  node: callee,
19872
- message: MESSAGE$26
19838
+ message: MESSAGE$22
19873
19839
  });
19874
19840
  return;
19875
19841
  }
@@ -19880,7 +19846,7 @@ const noFindDomNode = defineRule({
19880
19846
  if (callee.property.name !== "findDOMNode") return;
19881
19847
  context.report({
19882
19848
  node: callee.property,
19883
- message: MESSAGE$26
19849
+ message: MESSAGE$22
19884
19850
  });
19885
19851
  }
19886
19852
  } })
@@ -19921,41 +19887,6 @@ const noFullLodashImport = defineRule({
19921
19887
  } })
19922
19888
  });
19923
19889
  //#endregion
19924
- //#region src/plugin/rules/design/no-full-viewport-width.ts
19925
- const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
19926
- const WIDTH_KEYS = new Set(["width", "minWidth"]);
19927
- const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
19928
- const noFullViewportWidth = defineRule({
19929
- id: "no-full-viewport-width",
19930
- title: "Full viewport width causes overflow",
19931
- tags: ["design", "test-noise"],
19932
- severity: "warn",
19933
- recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
19934
- create: (context) => ({
19935
- JSXAttribute(node) {
19936
- const expression = getInlineStyleExpression(node);
19937
- if (!expression) return;
19938
- for (const property of expression.properties ?? []) {
19939
- const key = getStylePropertyKey(property);
19940
- if (!key || !WIDTH_KEYS.has(key)) continue;
19941
- const value = getStylePropertyStringValue(property);
19942
- if (value && value.trim().toLowerCase() === "100vw") context.report({
19943
- node: property,
19944
- message: MESSAGE$25
19945
- });
19946
- }
19947
- },
19948
- JSXOpeningElement(node) {
19949
- const classNameValue = getStringFromClassNameAttr(node);
19950
- if (!classNameValue) return;
19951
- if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
19952
- node,
19953
- message: MESSAGE$25
19954
- });
19955
- }
19956
- })
19957
- });
19958
- //#endregion
19959
19890
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19960
19891
  const noGenericHandlerNames = defineRule({
19961
19892
  id: "no-generic-handler-names",
@@ -20018,7 +19949,7 @@ const noGiantComponent = defineRule({
20018
19949
  });
20019
19950
  //#endregion
20020
19951
  //#region src/plugin/constants/style.ts
20021
- const LAYOUT_PROPERTIES$1 = new Set([
19952
+ const LAYOUT_PROPERTIES = new Set([
20022
19953
  "width",
20023
19954
  "height",
20024
19955
  "top",
@@ -20088,6 +20019,17 @@ const noGlobalCssVariableAnimation = defineRule({
20088
20019
  } })
20089
20020
  });
20090
20021
  //#endregion
20022
+ //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
20023
+ const getStringFromClassNameAttr = (node) => {
20024
+ if (!isNodeOfType(node, "JSXOpeningElement")) return null;
20025
+ const classAttr = findJsxAttribute(node.attributes ?? [], "className");
20026
+ if (!classAttr?.value) return null;
20027
+ if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
20028
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
20029
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
20030
+ return null;
20031
+ };
20032
+ //#endregion
20091
20033
  //#region src/plugin/rules/design/no-gradient-text.ts
20092
20034
  const noGradientText = defineRule({
20093
20035
  id: "no-gradient-text",
@@ -20146,7 +20088,7 @@ const noGrayOnColoredBackground = defineRule({
20146
20088
  });
20147
20089
  //#endregion
20148
20090
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20149
- const MESSAGE$24 = "`<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.";
20091
+ 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.";
20150
20092
  const noImgLazyWithHighFetchpriority = defineRule({
20151
20093
  id: "no-img-lazy-with-high-fetchpriority",
20152
20094
  title: "Lazy image with high fetchPriority",
@@ -20160,7 +20102,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
20160
20102
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20161
20103
  context.report({
20162
20104
  node: node.name,
20163
- message: MESSAGE$24
20105
+ message: MESSAGE$21
20164
20106
  });
20165
20107
  } })
20166
20108
  });
@@ -20257,15 +20199,20 @@ const noInlineExhaustiveStyle = defineRule({
20257
20199
  severity: "warn",
20258
20200
  tags: ["test-noise", "react-jsx-only"],
20259
20201
  recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
20260
- create: (context) => ({ JSXAttribute(node) {
20261
- const expression = getInlineStyleExpression(node);
20262
- if (!expression) return;
20263
- const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20264
- if (propertyCount >= 8) context.report({
20265
- node: expression,
20266
- message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20267
- });
20268
- } })
20202
+ create: (context) => {
20203
+ if (isGeneratedImageRenderContext(context)) return {};
20204
+ return { JSXAttribute(node) {
20205
+ const expression = getInlineStyleExpression(node);
20206
+ if (!expression) return;
20207
+ const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20208
+ if (propertyCount < 8) return;
20209
+ if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
20210
+ context.report({
20211
+ node: expression,
20212
+ message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
20213
+ });
20214
+ } };
20215
+ }
20269
20216
  });
20270
20217
  //#endregion
20271
20218
  //#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
@@ -20395,7 +20342,7 @@ const noIsMounted = defineRule({
20395
20342
  });
20396
20343
  //#endregion
20397
20344
  //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20398
- const MESSAGE$23 = "`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)`.";
20345
+ 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)`.";
20399
20346
  const isJsonMethodCall = (node, method) => {
20400
20347
  if (!isNodeOfType(node, "CallExpression")) return false;
20401
20348
  const callee = node.callee;
@@ -20412,13 +20359,13 @@ const noJsonParseStringifyClone = defineRule({
20412
20359
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20413
20360
  context.report({
20414
20361
  node,
20415
- message: MESSAGE$23
20362
+ message: MESSAGE$20
20416
20363
  });
20417
20364
  } })
20418
20365
  });
20419
20366
  //#endregion
20420
20367
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
20421
- const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20368
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20422
20369
  const isJsxElementTypeReference = (node) => {
20423
20370
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20424
20371
  const typeName = node.typeName;
@@ -20435,7 +20382,7 @@ const checkReturnType = (context, returnType) => {
20435
20382
  if (!typeAnnotation) return;
20436
20383
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20437
20384
  node: typeAnnotation,
20438
- message: MESSAGE$22
20385
+ message: MESSAGE$19
20439
20386
  });
20440
20387
  };
20441
20388
  const noJsxElementType = defineRule({
@@ -20545,7 +20492,7 @@ const noLayoutPropertyAnimation = defineRule({
20545
20492
  let propertyName = null;
20546
20493
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20547
20494
  else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
20548
- if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
20495
+ if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20549
20496
  node: property,
20550
20497
  message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
20551
20498
  });
@@ -20735,134 +20682,6 @@ const noLongTransitionDuration = defineRule({
20735
20682
  } })
20736
20683
  });
20737
20684
  //#endregion
20738
- //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
20739
- const getStylePropertyNumberValue = (property) => {
20740
- if (!isNodeOfType(property, "Property")) return null;
20741
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
20742
- if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
20743
- return null;
20744
- };
20745
- //#endregion
20746
- //#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
20747
- const linearizeChannel = (channel) => {
20748
- const normalized = channel / 255;
20749
- return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
20750
- };
20751
- const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
20752
- const getWcagContrastRatio = (foreground, background) => {
20753
- const foregroundLuminance = relativeLuminance(foreground);
20754
- const backgroundLuminance = relativeLuminance(background);
20755
- const lighter = Math.max(foregroundLuminance, backgroundLuminance);
20756
- const darker = Math.min(foregroundLuminance, backgroundLuminance);
20757
- return (lighter + .05) / (darker + .05);
20758
- };
20759
- //#endregion
20760
- //#region src/plugin/rules/design/no-low-contrast-inline-style.ts
20761
- const UNRESOLVABLE = new Set([
20762
- "transparent",
20763
- "currentcolor",
20764
- "inherit",
20765
- "initial",
20766
- "unset",
20767
- "revert",
20768
- "none"
20769
- ]);
20770
- const resolveOpaqueColor = (raw) => {
20771
- const value = raw.trim().toLowerCase();
20772
- if (UNRESOLVABLE.has(value)) return null;
20773
- if (value === "white") return {
20774
- red: 255,
20775
- green: 255,
20776
- blue: 255
20777
- };
20778
- if (value === "black") return {
20779
- red: 0,
20780
- green: 0,
20781
- blue: 0
20782
- };
20783
- if (value.startsWith("var(")) return null;
20784
- if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
20785
- if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
20786
- if (value.startsWith("rgb")) {
20787
- const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
20788
- if (inner.includes("/") || inner.split(",").length >= 4) return null;
20789
- }
20790
- return parseColorToRgb(value);
20791
- };
20792
- const toPx = (property) => {
20793
- const numberValue = getStylePropertyNumberValue(property);
20794
- if (numberValue !== null) return numberValue;
20795
- const stringValue = getStylePropertyStringValue(property);
20796
- if (stringValue === null) return null;
20797
- const pxMatch = stringValue.match(/^([\d.]+)px$/);
20798
- if (pxMatch) return parseFloat(pxMatch[1]);
20799
- const remMatch = stringValue.match(/^([\d.]+)rem$/);
20800
- if (remMatch) return parseFloat(remMatch[1]) * 16;
20801
- return null;
20802
- };
20803
- const isBoldWeight = (property) => {
20804
- const numberValue = getStylePropertyNumberValue(property);
20805
- if (numberValue !== null) return numberValue >= 700;
20806
- const stringValue = getStylePropertyStringValue(property);
20807
- if (stringValue === null) return false;
20808
- if (stringValue === "bold" || stringValue === "bolder") return true;
20809
- const numericWeight = Number(stringValue);
20810
- return Number.isFinite(numericWeight) && numericWeight >= 700;
20811
- };
20812
- const noLowContrastInlineStyle = defineRule({
20813
- id: "no-low-contrast-inline-style",
20814
- title: "Low-contrast text in inline style",
20815
- tags: ["test-noise"],
20816
- severity: "warn",
20817
- category: "Accessibility",
20818
- recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
20819
- create: (context) => ({ JSXAttribute(node) {
20820
- const expression = getInlineStyleExpression(node);
20821
- if (!expression) return;
20822
- const properties = expression.properties ?? [];
20823
- if (properties.some((property) => property.type === "SpreadElement")) return;
20824
- let foreground = null;
20825
- let backgroundColorRaw = null;
20826
- let backgroundShorthandRaw = null;
20827
- let backgroundIsUnknown = false;
20828
- let fontSizePx = null;
20829
- let isBold = false;
20830
- for (const property of properties) {
20831
- const key = getStylePropertyKey(property);
20832
- if (!key) continue;
20833
- if (key === "backgroundImage") {
20834
- backgroundIsUnknown = true;
20835
- continue;
20836
- }
20837
- if (key === "fontSize" && property.type === "Property") {
20838
- fontSizePx = toPx(property);
20839
- continue;
20840
- }
20841
- if (key === "fontWeight" && property.type === "Property") {
20842
- isBold = isBoldWeight(property);
20843
- continue;
20844
- }
20845
- const stringValue = getStylePropertyStringValue(property);
20846
- if (key === "color") {
20847
- if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
20848
- } else if (key === "backgroundColor") backgroundColorRaw = stringValue;
20849
- else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
20850
- else backgroundShorthandRaw = stringValue;
20851
- }
20852
- if (backgroundIsUnknown) return;
20853
- if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
20854
- const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
20855
- const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
20856
- if (!foreground || !background) return;
20857
- const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
20858
- const ratio = getWcagContrastRatio(foreground, background);
20859
- if (ratio < threshold) context.report({
20860
- node,
20861
- message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
20862
- });
20863
- } })
20864
- });
20865
- //#endregion
20866
20685
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20867
20686
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20868
20687
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -21018,7 +20837,7 @@ const noMoment = defineRule({
21018
20837
  });
21019
20838
  //#endregion
21020
20839
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
21021
- const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
20840
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
21022
20841
  const resolveSettings$16 = (settings) => {
21023
20842
  const reactDoctor = settings?.["react-doctor"];
21024
20843
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21340,7 +21159,7 @@ const noMultiComp = defineRule({
21340
21159
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21341
21160
  for (const component of flagged.slice(1)) context.report({
21342
21161
  node: component.reportNode,
21343
- message: MESSAGE$21
21162
+ message: MESSAGE$18
21344
21163
  });
21345
21164
  } };
21346
21165
  }
@@ -21508,7 +21327,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21508
21327
  };
21509
21328
  //#endregion
21510
21329
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
21511
- const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
21330
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
21512
21331
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21513
21332
  "copyWithin",
21514
21333
  "fill",
@@ -21718,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21718
21537
  reportedNodes.add(options.crossFileConsumerCallSite);
21719
21538
  context.report({
21720
21539
  node: options.crossFileConsumerCallSite,
21721
- message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21540
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21722
21541
  });
21723
21542
  return;
21724
21543
  }
@@ -21727,7 +21546,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21727
21546
  reportedNodes.add(mutation.node);
21728
21547
  context.report({
21729
21548
  node: mutation.node,
21730
- message: MESSAGE$20
21549
+ message: MESSAGE$17
21731
21550
  });
21732
21551
  }
21733
21552
  };
@@ -21999,7 +21818,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21999
21818
  });
22000
21819
  //#endregion
22001
21820
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
22002
- const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21821
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22003
21822
  const resolveSettings$14 = (settings) => {
22004
21823
  const reactDoctor = settings?.["react-doctor"];
22005
21824
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -22027,7 +21846,7 @@ const noNoninteractiveTabindex = defineRule({
22027
21846
  if (numeric === null) {
22028
21847
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
22029
21848
  node: tabIndex,
22030
- message: MESSAGE$19
21849
+ message: MESSAGE$16
22031
21850
  });
22032
21851
  return;
22033
21852
  }
@@ -22040,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
22040
21859
  if (!roleAttribute) {
22041
21860
  context.report({
22042
21861
  node: tabIndex,
22043
- message: MESSAGE$19
21862
+ message: MESSAGE$16
22044
21863
  });
22045
21864
  return;
22046
21865
  }
@@ -22054,12 +21873,20 @@ const noNoninteractiveTabindex = defineRule({
22054
21873
  }
22055
21874
  context.report({
22056
21875
  node: tabIndex,
22057
- message: MESSAGE$19
21876
+ message: MESSAGE$16
22058
21877
  });
22059
21878
  } };
22060
21879
  }
22061
21880
  });
22062
21881
  //#endregion
21882
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21883
+ const getStylePropertyNumberValue = (property) => {
21884
+ if (!isNodeOfType(property, "Property")) return null;
21885
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21886
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21887
+ return null;
21888
+ };
21889
+ //#endregion
22063
21890
  //#region src/plugin/rules/design/no-outline-none.ts
22064
21891
  const noOutlineNone = defineRule({
22065
21892
  id: "no-outline-none",
@@ -22737,7 +22564,7 @@ const noRandomKey = defineRule({
22737
22564
  });
22738
22565
  //#endregion
22739
22566
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22740
- const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22567
+ const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22741
22568
  const isChildrenIdentifier = (node, contextNode) => {
22742
22569
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22743
22570
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22763,13 +22590,13 @@ const noReactChildren = defineRule({
22763
22590
  if (isChildrenIdentifier(memberObject, node)) {
22764
22591
  context.report({
22765
22592
  node: calleeOuter,
22766
- message: MESSAGE$18
22593
+ message: MESSAGE$15
22767
22594
  });
22768
22595
  return;
22769
22596
  }
22770
22597
  if (isReactNamespaceMember(memberObject, node)) context.report({
22771
22598
  node: calleeOuter,
22772
- message: MESSAGE$18
22599
+ message: MESSAGE$15
22773
22600
  });
22774
22601
  } })
22775
22602
  });
@@ -22880,86 +22707,6 @@ const noReact19DeprecatedApis = defineRule({
22880
22707
  })
22881
22708
  });
22882
22709
  //#endregion
22883
- //#region src/plugin/rules/design/no-redundant-display-class.ts
22884
- const BLOCK_DEFAULT_TAGS = new Set([
22885
- "div",
22886
- "p",
22887
- "section",
22888
- "article",
22889
- "main",
22890
- "header",
22891
- "footer",
22892
- "nav",
22893
- "aside",
22894
- "figure",
22895
- "figcaption",
22896
- "blockquote",
22897
- "form",
22898
- "fieldset",
22899
- "address",
22900
- "pre",
22901
- "ul",
22902
- "ol",
22903
- "dl",
22904
- "dt",
22905
- "dd",
22906
- "h1",
22907
- "h2",
22908
- "h3",
22909
- "h4",
22910
- "h5",
22911
- "h6"
22912
- ]);
22913
- const INLINE_DEFAULT_TAGS = new Set([
22914
- "span",
22915
- "a",
22916
- "b",
22917
- "i",
22918
- "em",
22919
- "strong",
22920
- "small",
22921
- "code",
22922
- "abbr",
22923
- "cite",
22924
- "label",
22925
- "mark",
22926
- "q",
22927
- "s",
22928
- "u",
22929
- "sub",
22930
- "sup",
22931
- "kbd",
22932
- "samp",
22933
- "var",
22934
- "time"
22935
- ]);
22936
- const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
22937
- const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
22938
- const noRedundantDisplayClass = defineRule({
22939
- id: "no-redundant-display-class",
22940
- title: "Redundant display utility",
22941
- tags: ["design", "test-noise"],
22942
- severity: "warn",
22943
- recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
22944
- create: (context) => ({ JSXOpeningElement(node) {
22945
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
22946
- const tagName = node.name.name;
22947
- const classNameValue = getStringFromClassNameAttr(node);
22948
- if (!classNameValue) return;
22949
- if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
22950
- context.report({
22951
- node,
22952
- message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
22953
- });
22954
- return;
22955
- }
22956
- if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
22957
- node,
22958
- message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
22959
- });
22960
- } })
22961
- });
22962
- //#endregion
22963
22710
  //#region src/plugin/constants/aria-element-roles.ts
22964
22711
  const ELEMENT_ROLE_PAIRS = [
22965
22712
  ["a", "link"],
@@ -23172,7 +22919,7 @@ const noRenderPropChildren = defineRule({
23172
22919
  });
23173
22920
  //#endregion
23174
22921
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
23175
- const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22922
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23176
22923
  const isReactDomRenderCall = (node) => {
23177
22924
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
23178
22925
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -23196,7 +22943,7 @@ const noRenderReturnValue = defineRule({
23196
22943
  if (!isUsedAsReturnValue(node.parent)) return;
23197
22944
  context.report({
23198
22945
  node: node.callee,
23199
- message: MESSAGE$17
22946
+ message: MESSAGE$14
23200
22947
  });
23201
22948
  } })
23202
22949
  });
@@ -23894,7 +23641,7 @@ const getParentComponent = (node) => {
23894
23641
  };
23895
23642
  //#endregion
23896
23643
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23897
- const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23644
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23898
23645
  const noSetState = defineRule({
23899
23646
  id: "no-set-state",
23900
23647
  title: "Local class state forbidden",
@@ -23909,7 +23656,7 @@ const noSetState = defineRule({
23909
23656
  if (!getParentComponent(node)) return;
23910
23657
  context.report({
23911
23658
  node: node.callee,
23912
- message: MESSAGE$16
23659
+ message: MESSAGE$13
23913
23660
  });
23914
23661
  } })
23915
23662
  });
@@ -24071,7 +23818,7 @@ const isAbstractRole = (openingElement, settings) => {
24071
23818
  };
24072
23819
  //#endregion
24073
23820
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
24074
- const MESSAGE$15 = "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.";
23821
+ 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.";
24075
23822
  const DEFAULT_HANDLERS = [
24076
23823
  "onClick",
24077
23824
  "onMouseDown",
@@ -24131,7 +23878,7 @@ const noStaticElementInteractions = defineRule({
24131
23878
  if (!roleAttribute || !roleAttribute.value) {
24132
23879
  context.report({
24133
23880
  node: node.name,
24134
- message: MESSAGE$15
23881
+ message: MESSAGE$12
24135
23882
  });
24136
23883
  return;
24137
23884
  }
@@ -24141,14 +23888,14 @@ const noStaticElementInteractions = defineRule({
24141
23888
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
24142
23889
  context.report({
24143
23890
  node: node.name,
24144
- message: MESSAGE$15
23891
+ message: MESSAGE$12
24145
23892
  });
24146
23893
  return;
24147
23894
  }
24148
23895
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
24149
23896
  context.report({
24150
23897
  node: node.name,
24151
- message: MESSAGE$15
23898
+ message: MESSAGE$12
24152
23899
  });
24153
23900
  } };
24154
23901
  }
@@ -24251,43 +23998,8 @@ const noStringRefs = defineRule({
24251
23998
  }
24252
23999
  });
24253
24000
  //#endregion
24254
- //#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
24255
- const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
24256
- if (token.includes(":")) return false;
24257
- if (!token.startsWith(prefix)) return false;
24258
- const value = token.slice(prefix.length);
24259
- if (value === "" || value === "current") return false;
24260
- if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
24261
- return true;
24262
- });
24263
- const isCurrentColor = (attribute) => {
24264
- const value = getJsxPropStringValue(attribute);
24265
- return value !== null && value.trim().toLowerCase() === "currentcolor";
24266
- };
24267
- const noSvgCurrentcolorWithFillClass = defineRule({
24268
- id: "no-svg-currentcolor-with-fill-class",
24269
- title: "currentColor fights a fill/stroke class",
24270
- tags: ["design", "test-noise"],
24271
- severity: "warn",
24272
- recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
24273
- create: (context) => ({ JSXOpeningElement(node) {
24274
- const classNameValue = getStringFromClassNameAttr(node);
24275
- if (!classNameValue) return;
24276
- for (const paint of ["fill", "stroke"]) {
24277
- const attribute = findJsxAttribute(node.attributes, paint);
24278
- if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
24279
- context.report({
24280
- node: attribute,
24281
- message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
24282
- });
24283
- return;
24284
- }
24285
- }
24286
- } })
24287
- });
24288
- //#endregion
24289
24001
  //#region src/plugin/rules/js-performance/no-sync-xhr.ts
24290
- const MESSAGE$14 = "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)`).";
24002
+ 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)`).";
24291
24003
  const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24292
24004
  const noSyncXhr = defineRule({
24293
24005
  id: "no-sync-xhr",
@@ -24302,103 +24014,13 @@ const noSyncXhr = defineRule({
24302
24014
  if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24303
24015
  context.report({
24304
24016
  node,
24305
- message: MESSAGE$14
24306
- });
24307
- } })
24308
- });
24309
- //#endregion
24310
- //#region src/plugin/rules/design/no-tailwind-layout-transition.ts
24311
- const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
24312
- const LAYOUT_PROPERTIES = new Set([
24313
- "width",
24314
- "height",
24315
- "min-width",
24316
- "max-width",
24317
- "min-height",
24318
- "max-height",
24319
- "top",
24320
- "left",
24321
- "right",
24322
- "bottom",
24323
- "inset",
24324
- "inset-block",
24325
- "inset-inline",
24326
- "margin",
24327
- "margin-top",
24328
- "margin-right",
24329
- "margin-bottom",
24330
- "margin-left",
24331
- "margin-block",
24332
- "margin-inline",
24333
- "padding",
24334
- "padding-top",
24335
- "padding-right",
24336
- "padding-bottom",
24337
- "padding-left",
24338
- "padding-block",
24339
- "padding-inline"
24340
- ]);
24341
- const noTailwindLayoutTransition = defineRule({
24342
- id: "no-tailwind-layout-transition",
24343
- title: "Animating a layout property",
24344
- tags: ["design", "test-noise"],
24345
- severity: "warn",
24346
- category: "Performance",
24347
- recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
24348
- create: (context) => ({ JSXOpeningElement(node) {
24349
- const classNameValue = getStringFromClassNameAttr(node);
24350
- if (!classNameValue) return;
24351
- for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
24352
- const animatedProperties = transitionMatch[1];
24353
- const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
24354
- if (layoutProperty) context.report({
24355
- node,
24356
- message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
24357
- });
24358
- }
24359
- } })
24360
- });
24361
- //#endregion
24362
- //#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
24363
- const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
24364
- const targetIsBlank = (attribute) => {
24365
- const stringValue = getJsxPropStringValue(attribute);
24366
- if (stringValue !== null) return stringValue === "_blank";
24367
- const value = attribute.value;
24368
- if (value && isNodeOfType(value, "JSXExpressionContainer")) {
24369
- const expression = value.expression;
24370
- if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
24371
- }
24372
- return false;
24373
- };
24374
- const noTargetBlankWithoutRel = defineRule({
24375
- id: "no-target-blank-without-rel",
24376
- title: "target=_blank without rel=noopener",
24377
- severity: "warn",
24378
- recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
24379
- create: (context) => ({ JSXOpeningElement(node) {
24380
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24381
- const tagName = node.name.name;
24382
- if (tagName !== "a" && tagName !== "area") return;
24383
- if (hasJsxSpreadAttribute(node.attributes)) return;
24384
- const targetAttribute = findJsxAttribute(node.attributes, "target");
24385
- if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
24386
- const relAttribute = findJsxAttribute(node.attributes, "rel");
24387
- if (relAttribute) {
24388
- const relValue = getJsxPropStringValue(relAttribute);
24389
- if (relValue === null) return;
24390
- const tokens = relValue.toLowerCase().split(/\s+/);
24391
- if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
24392
- }
24393
- context.report({
24394
- node: node.name,
24395
- message: MESSAGE$13
24017
+ message: MESSAGE$11
24396
24018
  });
24397
24019
  } })
24398
24020
  });
24399
24021
  //#endregion
24400
24022
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
24401
- const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
24023
+ const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24402
24024
  const isInsideClassMethod = (node, customClassFactoryNames) => {
24403
24025
  let ancestor = node.parent;
24404
24026
  while (ancestor) {
@@ -24467,7 +24089,7 @@ const noThisInSfc = defineRule({
24467
24089
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
24468
24090
  context.report({
24469
24091
  node,
24470
- message: MESSAGE$12
24092
+ message: MESSAGE$10
24471
24093
  });
24472
24094
  } };
24473
24095
  }
@@ -24505,39 +24127,26 @@ const noTinyText = defineRule({
24505
24127
  });
24506
24128
  //#endregion
24507
24129
  //#region src/plugin/rules/performance/no-transition-all.ts
24508
- const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
24509
- const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
24510
24130
  const noTransitionAll = defineRule({
24511
24131
  id: "no-transition-all",
24512
24132
  title: "transition: all animates everything",
24513
24133
  tags: ["test-noise"],
24514
24134
  severity: "warn",
24515
24135
  recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
24516
- create: (context) => ({
24517
- JSXAttribute(node) {
24518
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24519
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24520
- const expression = node.value.expression;
24521
- if (!isNodeOfType(expression, "ObjectExpression")) return;
24522
- for (const property of expression.properties ?? []) {
24523
- if (!isNodeOfType(property, "Property")) continue;
24524
- const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
24525
- if (key !== "transition" && key !== "transitionProperty") continue;
24526
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
24527
- node: property,
24528
- message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24529
- });
24530
- }
24531
- },
24532
- JSXOpeningElement(node) {
24533
- const classNameValue = getStringFromClassNameAttr(node);
24534
- if (!classNameValue) return;
24535
- if (hasTransitionAllClass(classNameValue)) context.report({
24536
- node,
24537
- message: TAILWIND_MESSAGE
24136
+ create: (context) => ({ JSXAttribute(node) {
24137
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24138
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24139
+ const expression = node.value.expression;
24140
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24141
+ for (const property of expression.properties ?? []) {
24142
+ if (!isNodeOfType(property, "Property")) continue;
24143
+ if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
24144
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
24145
+ node: property,
24146
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24538
24147
  });
24539
24148
  }
24540
- })
24149
+ } })
24541
24150
  });
24542
24151
  //#endregion
24543
24152
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -24581,6 +24190,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
24581
24190
  }
24582
24191
  return stateNames;
24583
24192
  };
24193
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
24584
24194
  const noUncontrolledInput = defineRule({
24585
24195
  id: "no-uncontrolled-input",
24586
24196
  title: "Uncontrolled input value",
@@ -24684,38 +24294,6 @@ const noUnescapedEntities = defineRule({
24684
24294
  } })
24685
24295
  });
24686
24296
  //#endregion
24687
- //#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
24688
- const UNINFORMATIVE_LABELS = new Set([
24689
- "icon",
24690
- "button",
24691
- "image",
24692
- "img",
24693
- "link",
24694
- "graphic",
24695
- "svg",
24696
- "picture",
24697
- "element",
24698
- "field",
24699
- "input"
24700
- ]);
24701
- const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
24702
- const noUninformativeAriaLabel = defineRule({
24703
- id: "no-uninformative-aria-label",
24704
- title: "Uninformative aria-label",
24705
- severity: "warn",
24706
- recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
24707
- create: (context) => ({ JSXOpeningElement(node) {
24708
- const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
24709
- if (!ariaLabel) return;
24710
- const labelValue = getJsxPropStringValue(ariaLabel);
24711
- if (labelValue === null) return;
24712
- if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
24713
- node: ariaLabel,
24714
- message: MESSAGE$11
24715
- });
24716
- } })
24717
- });
24718
- //#endregion
24719
24297
  //#region src/plugin/constants/dom-aria-properties.ts
24720
24298
  const ARIA_PROPERTY_NAMES = new Set([
24721
24299
  "activedescendant",
@@ -25850,15 +25428,8 @@ const expressionContainsJsxOrCreateElement = (root) => {
25850
25428
  visit(root);
25851
25429
  return found;
25852
25430
  };
25853
- const classExtendsReactComponent$1 = (classNode) => {
25854
- const superClass = classNode.superClass;
25855
- if (!superClass) return false;
25856
- if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
25857
- if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
25858
- return false;
25859
- };
25860
25431
  const isReactClassComponent = (classNode) => {
25861
- if (classExtendsReactComponent$1(classNode)) return true;
25432
+ if (isEs6Component(classNode)) return true;
25862
25433
  return expressionContainsJsxOrCreateElement(classNode);
25863
25434
  };
25864
25435
  const findEnclosingComponent = (node) => {
@@ -26018,7 +25589,7 @@ const noUnstableNestedComponents = defineRule({
26018
25589
  create: (context) => {
26019
25590
  const settings = resolveSettings$8(context.settings);
26020
25591
  const renderPropRegex = compileGlob(settings.propNamePattern);
26021
- const reportCandidate = (candidateNode, reportNode, candidateName) => {
25592
+ const reportCandidate = (candidateNode, reportNode) => {
26022
25593
  if (isFirstArgumentOfHocCall(candidateNode)) return;
26023
25594
  if (isReturnOfMapCallback(candidateNode)) return;
26024
25595
  const propInfo = isComponentDeclaredInProp(candidateNode);
@@ -26039,7 +25610,7 @@ const noUnstableNestedComponents = defineRule({
26039
25610
  const inferredName = inferFunctionLikeName(node);
26040
25611
  const propInfo = isComponentDeclaredInProp(node);
26041
25612
  if (!(inferredName !== null && isReactComponentName(inferredName) || propInfo !== null || isObjectCallbackCandidate(node))) return;
26042
- reportCandidate(node, node, inferredName);
25613
+ reportCandidate(node, node);
26043
25614
  };
26044
25615
  return {
26045
25616
  FunctionDeclaration: checkFunctionLike,
@@ -26049,18 +25620,18 @@ const noUnstableNestedComponents = defineRule({
26049
25620
  if (!node.id) return;
26050
25621
  if (!isReactComponentName(node.id.name)) return;
26051
25622
  if (!isReactClassComponent(node)) return;
26052
- reportCandidate(node, node, node.id.name);
25623
+ reportCandidate(node, node);
26053
25624
  },
26054
25625
  ClassExpression(node) {
26055
25626
  const inferredName = node.id?.name ?? inferFunctionLikeName(node);
26056
25627
  if (!inferredName || !isReactComponentName(inferredName)) return;
26057
25628
  if (!isReactClassComponent(node)) return;
26058
- reportCandidate(node, node, inferredName);
25629
+ reportCandidate(node, node);
26059
25630
  },
26060
25631
  CallExpression(node) {
26061
25632
  if (!isHocCallee$1(node)) return;
26062
25633
  if (!hocCallContainsComponent(node)) return;
26063
- reportCandidate(node, node, null);
25634
+ reportCandidate(node, node);
26064
25635
  }
26065
25636
  };
26066
25637
  }
@@ -26187,7 +25758,7 @@ const noWideLetterSpacing = defineRule({
26187
25758
  //#endregion
26188
25759
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
26189
25760
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
26190
- const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
25761
+ const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26191
25762
  const resolveSettings$7 = (settings) => {
26192
25763
  const reactDoctor = settings?.["react-doctor"];
26193
25764
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -26221,7 +25792,7 @@ const noWillUpdateSetState = defineRule({
26221
25792
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
26222
25793
  context.report({
26223
25794
  node: node.callee,
26224
- message: MESSAGE$10
25795
+ message: MESSAGE$9
26225
25796
  });
26226
25797
  } };
26227
25798
  }
@@ -26500,13 +26071,6 @@ const skipTsExpression = (expression) => {
26500
26071
  if (expression.type === "TSAsExpression" || expression.type === "TSSatisfiesExpression" || expression.type === "TSNonNullExpression") return skipTsExpression(expression.expression);
26501
26072
  return expression;
26502
26073
  };
26503
- const classExtendsReactComponent = (classNode) => {
26504
- const superClass = classNode.superClass;
26505
- if (!superClass) return false;
26506
- if (isNodeOfType(superClass, "Identifier") && (superClass.name === "Component" || superClass.name === "PureComponent")) return true;
26507
- if (isNodeOfType(superClass, "MemberExpression") && isNodeOfType(superClass.object, "Identifier") && superClass.object.name === "React" && isNodeOfType(superClass.property, "Identifier") && (superClass.property.name === "Component" || superClass.property.name === "PureComponent")) return true;
26508
- return false;
26509
- };
26510
26074
  const isReactCreateContext = (initializer) => {
26511
26075
  if (!initializer) return false;
26512
26076
  const expression = skipTsExpression(initializer);
@@ -26697,7 +26261,7 @@ const onlyExportComponents = defineRule({
26697
26261
  if (stripped.id) {
26698
26262
  const idNode = stripped.id;
26699
26263
  isExportedNodeIds.add(stripped);
26700
- if (isReactComponentName(idNode.name) && classExtendsReactComponent(stripped)) hasReactExport = true;
26264
+ if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
26701
26265
  else exports.push({
26702
26266
  kind: "non-component",
26703
26267
  reportNode: idNode
@@ -26757,7 +26321,7 @@ const onlyExportComponents = defineRule({
26757
26321
  exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
26758
26322
  } else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
26759
26323
  isExportedNodeIds.add(declaration);
26760
- if (isReactComponentName(declaration.id.name) && classExtendsReactComponent(declaration)) exports.push({ kind: "react-component" });
26324
+ if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
26761
26325
  else exports.push({
26762
26326
  kind: "non-component",
26763
26327
  reportNode: declaration.id
@@ -27099,7 +26663,7 @@ const preactNoRenderArguments = defineRule({
27099
26663
  });
27100
26664
  //#endregion
27101
26665
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
27102
- const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
26666
+ const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
27103
26667
  const preactPreferOndblclick = defineRule({
27104
26668
  id: "preact-prefer-ondblclick",
27105
26669
  title: "onDoubleClick instead of onDblClick",
@@ -27114,7 +26678,7 @@ const preactPreferOndblclick = defineRule({
27114
26678
  if (!onDoubleClickAttribute) return;
27115
26679
  context.report({
27116
26680
  node: onDoubleClickAttribute,
27117
- message: MESSAGE$9
26681
+ message: MESSAGE$8
27118
26682
  });
27119
26683
  } })
27120
26684
  });
@@ -27154,42 +26718,6 @@ const preactPreferOninput = defineRule({
27154
26718
  } })
27155
26719
  });
27156
26720
  //#endregion
27157
- //#region src/plugin/rules/design/prefer-dvh-over-vh.ts
27158
- const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
27159
- const HEIGHT_KEYS = new Set(["height", "minHeight"]);
27160
- const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
27161
- const preferDvhOverVh = defineRule({
27162
- id: "prefer-dvh-over-vh",
27163
- title: "Use dvh instead of vh for full height",
27164
- tags: ["design", "test-noise"],
27165
- severity: "warn",
27166
- requires: ["tailwind:3.4"],
27167
- recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
27168
- create: (context) => ({
27169
- JSXAttribute(node) {
27170
- const expression = getInlineStyleExpression(node);
27171
- if (!expression) return;
27172
- for (const property of expression.properties ?? []) {
27173
- const key = getStylePropertyKey(property);
27174
- if (!key || !HEIGHT_KEYS.has(key)) continue;
27175
- const value = getStylePropertyStringValue(property);
27176
- if (value && value.trim().toLowerCase() === "100vh") context.report({
27177
- node: property,
27178
- message: MESSAGE$8
27179
- });
27180
- }
27181
- },
27182
- JSXOpeningElement(node) {
27183
- const classNameValue = getStringFromClassNameAttr(node);
27184
- if (!classNameValue) return;
27185
- if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
27186
- node,
27187
- message: MESSAGE$8
27188
- });
27189
- }
27190
- })
27191
- });
27192
- //#endregion
27193
26721
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
27194
26722
  const preferDynamicImport = defineRule({
27195
26723
  id: "prefer-dynamic-import",
@@ -27781,26 +27309,6 @@ const preferTagOverRole = defineRule({
27781
27309
  } })
27782
27310
  });
27783
27311
  //#endregion
27784
- //#region src/plugin/rules/design/prefer-truncate-shorthand.ts
27785
- const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
27786
- const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
27787
- const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
27788
- const preferTruncateShorthand = defineRule({
27789
- id: "prefer-truncate-shorthand",
27790
- title: "Use truncate shorthand",
27791
- tags: ["design", "test-noise"],
27792
- severity: "warn",
27793
- recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
27794
- create: (context) => ({ JSXOpeningElement(node) {
27795
- const classNameValue = getStringFromClassNameAttr(node);
27796
- if (!classNameValue) return;
27797
- if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
27798
- node,
27799
- message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
27800
- });
27801
- } })
27802
- });
27803
- //#endregion
27804
27312
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27805
27313
  const collectFunctionTypedLocalBindings = (componentBody) => {
27806
27314
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -35917,6 +35425,7 @@ const serverFetchWithoutRevalidate = defineRule({
35917
35425
  CallExpression(node) {
35918
35426
  if (!isServerSideFile) return;
35919
35427
  if (!isFetchCall(node)) return;
35428
+ if (isMutatingFetchCall(node)) return;
35920
35429
  const optionsArg = node.arguments?.[1];
35921
35430
  if (optionsArg && objectExpressionHasNextRevalidate(optionsArg)) return;
35922
35431
  const urlArg = node.arguments?.[0];
@@ -39475,17 +38984,6 @@ const reactDoctorRules = [
39475
38984
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
39476
38985
  }
39477
38986
  },
39478
- {
39479
- key: "react-doctor/no-arbitrary-px-font-size",
39480
- id: "no-arbitrary-px-font-size",
39481
- source: "react-doctor",
39482
- originallyExternal: false,
39483
- rule: {
39484
- ...noArbitraryPxFontSize,
39485
- framework: "global",
39486
- category: "Accessibility"
39487
- }
39488
- },
39489
38987
  {
39490
38988
  key: "react-doctor/no-aria-hidden-on-focusable",
39491
38989
  id: "no-aria-hidden-on-focusable",
@@ -39545,18 +39043,6 @@ const reactDoctorRules = [
39545
39043
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
39546
39044
  }
39547
39045
  },
39548
- {
39549
- key: "react-doctor/no-autoplay-without-muted",
39550
- id: "no-autoplay-without-muted",
39551
- source: "react-doctor",
39552
- originallyExternal: false,
39553
- rule: {
39554
- ...noAutoplayWithoutMuted,
39555
- framework: "global",
39556
- category: "Accessibility",
39557
- requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39558
- }
39559
- },
39560
39046
  {
39561
39047
  key: "react-doctor/no-barrel-import",
39562
39048
  id: "no-barrel-import",
@@ -39710,17 +39196,6 @@ const reactDoctorRules = [
39710
39196
  category: "Maintainability"
39711
39197
  }
39712
39198
  },
39713
- {
39714
- key: "react-doctor/no-deprecated-tailwind-class",
39715
- id: "no-deprecated-tailwind-class",
39716
- source: "react-doctor",
39717
- originallyExternal: false,
39718
- rule: {
39719
- ...noDeprecatedTailwindClass,
39720
- framework: "global",
39721
- category: "Maintainability"
39722
- }
39723
- },
39724
39199
  {
39725
39200
  key: "react-doctor/no-derived-state",
39726
39201
  id: "no-derived-state",
@@ -39992,17 +39467,6 @@ const reactDoctorRules = [
39992
39467
  category: "Performance"
39993
39468
  }
39994
39469
  },
39995
- {
39996
- key: "react-doctor/no-full-viewport-width",
39997
- id: "no-full-viewport-width",
39998
- source: "react-doctor",
39999
- originallyExternal: false,
40000
- rule: {
40001
- ...noFullViewportWidth,
40002
- framework: "global",
40003
- category: "Maintainability"
40004
- }
40005
- },
40006
39470
  {
40007
39471
  key: "react-doctor/no-generic-handler-names",
40008
39472
  id: "no-generic-handler-names",
@@ -40242,17 +39706,6 @@ const reactDoctorRules = [
40242
39706
  category: "Performance"
40243
39707
  }
40244
39708
  },
40245
- {
40246
- key: "react-doctor/no-low-contrast-inline-style",
40247
- id: "no-low-contrast-inline-style",
40248
- source: "react-doctor",
40249
- originallyExternal: false,
40250
- rule: {
40251
- ...noLowContrastInlineStyle,
40252
- framework: "global",
40253
- category: "Accessibility"
40254
- }
40255
- },
40256
39709
  {
40257
39710
  key: "react-doctor/no-many-boolean-props",
40258
39711
  id: "no-many-boolean-props",
@@ -40530,17 +39983,6 @@ const reactDoctorRules = [
40530
39983
  category: "Maintainability"
40531
39984
  }
40532
39985
  },
40533
- {
40534
- key: "react-doctor/no-redundant-display-class",
40535
- id: "no-redundant-display-class",
40536
- source: "react-doctor",
40537
- originallyExternal: false,
40538
- rule: {
40539
- ...noRedundantDisplayClass,
40540
- framework: "global",
40541
- category: "Maintainability"
40542
- }
40543
- },
40544
39986
  {
40545
39987
  key: "react-doctor/no-redundant-roles",
40546
39988
  id: "no-redundant-roles",
@@ -40717,17 +40159,6 @@ const reactDoctorRules = [
40717
40159
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
40718
40160
  }
40719
40161
  },
40720
- {
40721
- key: "react-doctor/no-svg-currentcolor-with-fill-class",
40722
- id: "no-svg-currentcolor-with-fill-class",
40723
- source: "react-doctor",
40724
- originallyExternal: false,
40725
- rule: {
40726
- ...noSvgCurrentcolorWithFillClass,
40727
- framework: "global",
40728
- category: "Maintainability"
40729
- }
40730
- },
40731
40162
  {
40732
40163
  key: "react-doctor/no-sync-xhr",
40733
40164
  id: "no-sync-xhr",
@@ -40739,29 +40170,6 @@ const reactDoctorRules = [
40739
40170
  category: "Performance"
40740
40171
  }
40741
40172
  },
40742
- {
40743
- key: "react-doctor/no-tailwind-layout-transition",
40744
- id: "no-tailwind-layout-transition",
40745
- source: "react-doctor",
40746
- originallyExternal: false,
40747
- rule: {
40748
- ...noTailwindLayoutTransition,
40749
- framework: "global",
40750
- category: "Performance"
40751
- }
40752
- },
40753
- {
40754
- key: "react-doctor/no-target-blank-without-rel",
40755
- id: "no-target-blank-without-rel",
40756
- source: "react-doctor",
40757
- originallyExternal: false,
40758
- rule: {
40759
- ...noTargetBlankWithoutRel,
40760
- framework: "global",
40761
- category: "Accessibility",
40762
- requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40763
- }
40764
- },
40765
40173
  {
40766
40174
  key: "react-doctor/no-this-in-sfc",
40767
40175
  id: "no-this-in-sfc",
@@ -40831,18 +40239,6 @@ const reactDoctorRules = [
40831
40239
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
40832
40240
  }
40833
40241
  },
40834
- {
40835
- key: "react-doctor/no-uninformative-aria-label",
40836
- id: "no-uninformative-aria-label",
40837
- source: "react-doctor",
40838
- originallyExternal: false,
40839
- rule: {
40840
- ...noUninformativeAriaLabel,
40841
- framework: "global",
40842
- category: "Accessibility",
40843
- requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40844
- }
40845
- },
40846
40242
  {
40847
40243
  key: "react-doctor/no-unknown-property",
40848
40244
  id: "no-unknown-property",
@@ -41052,17 +40448,6 @@ const reactDoctorRules = [
41052
40448
  category: "Bugs"
41053
40449
  }
41054
40450
  },
41055
- {
41056
- key: "react-doctor/prefer-dvh-over-vh",
41057
- id: "prefer-dvh-over-vh",
41058
- source: "react-doctor",
41059
- originallyExternal: false,
41060
- rule: {
41061
- ...preferDvhOverVh,
41062
- framework: "global",
41063
- category: "Maintainability"
41064
- }
41065
- },
41066
40451
  {
41067
40452
  key: "react-doctor/prefer-dynamic-import",
41068
40453
  id: "prefer-dynamic-import",
@@ -41167,17 +40552,6 @@ const reactDoctorRules = [
41167
40552
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
41168
40553
  }
41169
40554
  },
41170
- {
41171
- key: "react-doctor/prefer-truncate-shorthand",
41172
- id: "prefer-truncate-shorthand",
41173
- source: "react-doctor",
41174
- originallyExternal: false,
41175
- rule: {
41176
- ...preferTruncateShorthand,
41177
- framework: "global",
41178
- category: "Maintainability"
41179
- }
41180
- },
41181
40555
  {
41182
40556
  key: "react-doctor/prefer-use-effect-event",
41183
40557
  id: "prefer-use-effect-event",
@@ -42904,32 +42278,6 @@ const computeUnconditionalSet = (cfg) => {
42904
42278
  }
42905
42279
  return unconditional;
42906
42280
  };
42907
- const computeDominatesExit = (cfg) => {
42908
- const reachableToExit = /* @__PURE__ */ new Set();
42909
- const queue = [cfg.exit];
42910
- while (queue.length > 0) {
42911
- const block = queue.shift();
42912
- if (reachableToExit.has(block)) continue;
42913
- reachableToExit.add(block);
42914
- for (const edge of block.predecessors) queue.push(edge.from);
42915
- }
42916
- const dominatesExit = /* @__PURE__ */ new Set();
42917
- const visit = (block) => {
42918
- if (block === cfg.exit) return true;
42919
- if (dominatesExit.has(block)) return true;
42920
- if (block.successors.length === 0) return false;
42921
- dominatesExit.add(block);
42922
- let allReach = true;
42923
- for (const edge of block.successors) if (!visit(edge.to)) {
42924
- allReach = false;
42925
- break;
42926
- }
42927
- if (!allReach) dominatesExit.delete(block);
42928
- return allReach;
42929
- };
42930
- for (const block of cfg.blocks) visit(block);
42931
- return dominatesExit;
42932
- };
42933
42281
  const analyzeControlFlow = (program) => {
42934
42282
  nextBlockId = 0;
42935
42283
  const functionCfgs = /* @__PURE__ */ new Map();
@@ -42937,8 +42285,7 @@ const analyzeControlFlow = (program) => {
42937
42285
  const cfg = buildFunctionCfg(functionNode, body);
42938
42286
  functionCfgs.set(functionNode, {
42939
42287
  cfg,
42940
- unconditionalSet: computeUnconditionalSet(cfg),
42941
- dominatesExitSet: computeDominatesExit(cfg)
42288
+ unconditionalSet: computeUnconditionalSet(cfg)
42942
42289
  });
42943
42290
  };
42944
42291
  if (isNodeOfType(program, "Program")) buildFor(program, {
@@ -42981,20 +42328,10 @@ const analyzeControlFlow = (program) => {
42981
42328
  if (!block) return true;
42982
42329
  return entry.unconditionalSet.has(block);
42983
42330
  };
42984
- const dominatesExit = (node) => {
42985
- const owner = enclosingFunction(node);
42986
- if (!owner) return true;
42987
- const entry = functionCfgs.get(owner);
42988
- if (!entry) return true;
42989
- const block = entry.cfg.blockOf(node);
42990
- if (!block) return true;
42991
- return entry.dominatesExitSet.has(block);
42992
- };
42993
42331
  return {
42994
42332
  cfgFor,
42995
42333
  enclosingFunction,
42996
- isUnconditionalFromEntry,
42997
- dominatesExit
42334
+ isUnconditionalFromEntry
42998
42335
  };
42999
42336
  };
43000
42337
  //#endregion
@@ -43019,8 +42356,7 @@ const buildFallbackScopes = () => ({
43019
42356
  const FALLBACK_CFG = {
43020
42357
  cfgFor: () => null,
43021
42358
  enclosingFunction: () => null,
43022
- isUnconditionalFromEntry: () => false,
43023
- dominatesExit: () => false
42359
+ isUnconditionalFromEntry: () => false
43024
42360
  };
43025
42361
  const wrapWithSemanticContext = (rule) => ({
43026
42362
  ...rule,