oxlint-plugin-react-doctor 0.5.6-dev.81bbfcc → 0.5.6-dev.937a7ca

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 -504
  2. package/dist/index.js +248 -872
  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
  });
@@ -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
  })
@@ -4694,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
4694
4748
  //#endregion
4695
4749
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4696
4750
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4697
- const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4751
+ const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4698
4752
  const KEY_HANDLERS = [
4699
4753
  "onKeyUp",
4700
4754
  "onKeyDown",
@@ -4726,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
4726
4780
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4727
4781
  context.report({
4728
4782
  node: node.name,
4729
- message: MESSAGE$61
4783
+ message: MESSAGE$56
4730
4784
  });
4731
4785
  } };
4732
4786
  }
@@ -4841,7 +4895,7 @@ const isReactComponentName = (name) => {
4841
4895
  };
4842
4896
  //#endregion
4843
4897
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4844
- const MESSAGE$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`.";
4898
+ const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4845
4899
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4846
4900
  const DEFAULT_LABELLING_PROPS = [
4847
4901
  "alt",
@@ -5002,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
5002
5056
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
5003
5057
  context.report({
5004
5058
  node: opening,
5005
- message: MESSAGE$60
5059
+ message: MESSAGE$55
5006
5060
  });
5007
5061
  } };
5008
5062
  }
@@ -5131,7 +5185,6 @@ const dangerousHtmlSink = defineRule({
5131
5185
  return findings;
5132
5186
  }
5133
5187
  });
5134
- const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5135
5188
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5136
5189
  const VAGUE_BUTTON_LABELS = new Set([
5137
5190
  "continue",
@@ -5430,10 +5483,10 @@ const noVagueButtonLabel = defineRule({
5430
5483
  });
5431
5484
  //#endregion
5432
5485
  //#region src/plugin/utils/has-jsx-spread-attribute.ts
5433
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5486
+ const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5434
5487
  //#endregion
5435
5488
  //#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.";
5489
+ const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5437
5490
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5438
5491
  const NAME_PROVIDING_ATTRIBUTES = [
5439
5492
  "aria-label",
@@ -5452,11 +5505,11 @@ const dialogHasAccessibleName = defineRule({
5452
5505
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5453
5506
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5454
5507
  if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5455
- if (hasJsxSpreadAttribute(node.attributes)) return;
5508
+ if (hasJsxSpreadAttribute$1(node.attributes)) return;
5456
5509
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5457
5510
  context.report({
5458
5511
  node: node.name,
5459
- message: MESSAGE$59
5512
+ message: MESSAGE$54
5460
5513
  });
5461
5514
  } })
5462
5515
  });
@@ -5495,7 +5548,7 @@ const isEs6Component = (node) => {
5495
5548
  };
5496
5549
  //#endregion
5497
5550
  //#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`.";
5551
+ const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5499
5552
  const DEFAULT_ADDITIONAL_HOCS = [
5500
5553
  "observer",
5501
5554
  "lazy",
@@ -5698,7 +5751,7 @@ const displayName = defineRule({
5698
5751
  const reportAt = (node) => {
5699
5752
  context.report({
5700
5753
  node,
5701
- message: MESSAGE$58
5754
+ message: MESSAGE$53
5702
5755
  });
5703
5756
  };
5704
5757
  return {
@@ -7846,7 +7899,7 @@ const forbidElements = defineRule({
7846
7899
  });
7847
7900
  //#endregion
7848
7901
  //#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`.";
7902
+ const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7850
7903
  const forwardRefUsesRef = defineRule({
7851
7904
  id: "forward-ref-uses-ref",
7852
7905
  title: "forwardRef without ref parameter",
@@ -7866,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
7866
7919
  if (isNodeOfType(onlyParam, "RestElement")) return;
7867
7920
  context.report({
7868
7921
  node: inner,
7869
- message: MESSAGE$57
7922
+ message: MESSAGE$52
7870
7923
  });
7871
7924
  } })
7872
7925
  });
@@ -7903,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7903
7956
  });
7904
7957
  //#endregion
7905
7958
  //#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`.";
7959
+ const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7907
7960
  const DEFAULT_HEADING_TAGS = [
7908
7961
  "h1",
7909
7962
  "h2",
@@ -7936,7 +7989,7 @@ const headingHasContent = defineRule({
7936
7989
  if (isHiddenFromScreenReader(node, context.settings)) return;
7937
7990
  context.report({
7938
7991
  node,
7939
- message: MESSAGE$56
7992
+ message: MESSAGE$51
7940
7993
  });
7941
7994
  } };
7942
7995
  }
@@ -8074,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
8074
8127
  });
8075
8128
  //#endregion
8076
8129
  //#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`.";
8130
+ const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8078
8131
  const resolveSettings$38 = (settings) => {
8079
8132
  const reactDoctor = settings?.["react-doctor"];
8080
8133
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8122,7 +8175,7 @@ const htmlHasLang = defineRule({
8122
8175
  if (!lang) {
8123
8176
  context.report({
8124
8177
  node: node.name,
8125
- message: MESSAGE$55
8178
+ message: MESSAGE$50
8126
8179
  });
8127
8180
  return;
8128
8181
  }
@@ -8130,13 +8183,13 @@ const htmlHasLang = defineRule({
8130
8183
  if (verdict === "missing" || verdict === "empty") {
8131
8184
  context.report({
8132
8185
  node: lang,
8133
- message: MESSAGE$55
8186
+ message: MESSAGE$50
8134
8187
  });
8135
8188
  return;
8136
8189
  }
8137
8190
  if (hasSpread && !lang) context.report({
8138
8191
  node: node.name,
8139
- message: MESSAGE$55
8192
+ message: MESSAGE$50
8140
8193
  });
8141
8194
  } };
8142
8195
  }
@@ -8350,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
8350
8403
  });
8351
8404
  //#endregion
8352
8405
  //#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.";
8406
+ const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8354
8407
  const evaluateTitleValue = (value) => {
8355
8408
  if (!value) return "missing";
8356
8409
  if (isNodeOfType(value, "Literal")) {
@@ -8390,14 +8443,14 @@ const iframeHasTitle = defineRule({
8390
8443
  if (!titleAttr) {
8391
8444
  if (hasSpread || tag === "iframe") context.report({
8392
8445
  node: node.name,
8393
- message: MESSAGE$54
8446
+ message: MESSAGE$49
8394
8447
  });
8395
8448
  return;
8396
8449
  }
8397
8450
  const verdict = evaluateTitleValue(titleAttr.value);
8398
8451
  if (verdict === "missing" || verdict === "empty") context.report({
8399
8452
  node: titleAttr,
8400
- message: MESSAGE$54
8453
+ message: MESSAGE$49
8401
8454
  });
8402
8455
  } })
8403
8456
  });
@@ -8501,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
8501
8554
  });
8502
8555
  //#endregion
8503
8556
  //#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.";
8557
+ const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8505
8558
  const DEFAULT_COMPONENTS = ["img"];
8506
8559
  const DEFAULT_REDUNDANT_WORDS = [
8507
8560
  "image",
@@ -8566,7 +8619,7 @@ const imgRedundantAlt = defineRule({
8566
8619
  if (!altAttribute) return;
8567
8620
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8568
8621
  node: altAttribute,
8569
- message: MESSAGE$53
8622
+ message: MESSAGE$48
8570
8623
  });
8571
8624
  } };
8572
8625
  }
@@ -10923,7 +10976,7 @@ const jsxMaxDepth = defineRule({
10923
10976
  });
10924
10977
  //#endregion
10925
10978
  //#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.";
10979
+ const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10927
10980
  const LITERAL_TEXT_TAGS = new Set([
10928
10981
  "code",
10929
10982
  "pre",
@@ -10959,7 +11012,7 @@ const jsxNoCommentTextnodes = defineRule({
10959
11012
  if (isInsideLiteralTextTag(node)) return;
10960
11013
  context.report({
10961
11014
  node,
10962
- message: MESSAGE$52
11015
+ message: MESSAGE$47
10963
11016
  });
10964
11017
  } })
10965
11018
  });
@@ -10990,7 +11043,7 @@ const isInsideFunctionScope = (node) => {
10990
11043
  };
10991
11044
  //#endregion
10992
11045
  //#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.";
11046
+ const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10994
11047
  const CONTEXT_MODULES$1 = [
10995
11048
  "react",
10996
11049
  "use-context-selector",
@@ -11088,7 +11141,7 @@ const jsxNoConstructedContextValues = defineRule({
11088
11141
  if (!isConstructedValue(innerExpression)) continue;
11089
11142
  context.report({
11090
11143
  node: attribute,
11091
- message: MESSAGE$51
11144
+ message: MESSAGE$46
11092
11145
  });
11093
11146
  }
11094
11147
  }
@@ -11174,7 +11227,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11174
11227
  };
11175
11228
  //#endregion
11176
11229
  //#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.";
11230
+ const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11178
11231
  const KNOWN_SLOT_PROP_NAMES = new Set([
11179
11232
  "icon",
11180
11233
  "Icon",
@@ -11443,7 +11496,7 @@ const jsxNoJsxAsProp = defineRule({
11443
11496
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11444
11497
  context.report({
11445
11498
  node,
11446
- message: MESSAGE$50
11499
+ message: MESSAGE$45
11447
11500
  });
11448
11501
  }
11449
11502
  };
@@ -11731,7 +11784,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11731
11784
  ];
11732
11785
  //#endregion
11733
11786
  //#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.";
11787
+ const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11735
11788
  const isDataArrayPropName = (propName) => {
11736
11789
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11737
11790
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11815,7 +11868,7 @@ const jsxNoNewArrayAsProp = defineRule({
11815
11868
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11816
11869
  context.report({
11817
11870
  node,
11818
- message: MESSAGE$49
11871
+ message: MESSAGE$44
11819
11872
  });
11820
11873
  }
11821
11874
  };
@@ -12073,7 +12126,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12073
12126
  ]);
12074
12127
  //#endregion
12075
12128
  //#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.";
12129
+ const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
12077
12130
  const isAccessorPredicateName = (propName) => {
12078
12131
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12079
12132
  if (propName.length <= prefix.length) continue;
@@ -12279,7 +12332,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12279
12332
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12280
12333
  context.report({
12281
12334
  node,
12282
- message: MESSAGE$48
12335
+ message: MESSAGE$43
12283
12336
  });
12284
12337
  }
12285
12338
  };
@@ -12499,7 +12552,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12499
12552
  ];
12500
12553
  //#endregion
12501
12554
  //#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.";
12555
+ const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12503
12556
  const isConfigObjectPropName = (propName) => {
12504
12557
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12505
12558
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12587,7 +12640,7 @@ const jsxNoNewObjectAsProp = defineRule({
12587
12640
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12588
12641
  context.report({
12589
12642
  node,
12590
- message: MESSAGE$47
12643
+ message: MESSAGE$42
12591
12644
  });
12592
12645
  }
12593
12646
  };
@@ -12595,7 +12648,7 @@ const jsxNoNewObjectAsProp = defineRule({
12595
12648
  });
12596
12649
  //#endregion
12597
12650
  //#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.";
12651
+ const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12599
12652
  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
12653
  const resolveSettings$28 = (settings) => {
12601
12654
  const reactDoctor = settings?.["react-doctor"];
@@ -12636,7 +12689,7 @@ const jsxNoScriptUrl = defineRule({
12636
12689
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12637
12690
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12638
12691
  node: attribute,
12639
- message: MESSAGE$46
12692
+ message: MESSAGE$41
12640
12693
  });
12641
12694
  }
12642
12695
  } };
@@ -12951,7 +13004,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12951
13004
  });
12952
13005
  //#endregion
12953
13006
  //#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.";
13007
+ const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12955
13008
  const resolveSettings$25 = (settings) => {
12956
13009
  const reactDoctor = settings?.["react-doctor"];
12957
13010
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12992,7 +13045,7 @@ const jsxPropsNoSpreading = defineRule({
12992
13045
  }
12993
13046
  context.report({
12994
13047
  node: attribute,
12995
- message: MESSAGE$45
13048
+ message: MESSAGE$40
12996
13049
  });
12997
13050
  }
12998
13051
  } };
@@ -13220,7 +13273,7 @@ const labelHasAssociatedControl = defineRule({
13220
13273
  });
13221
13274
  //#endregion
13222
13275
  //#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`.";
13276
+ 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
13277
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13225
13278
  "aa",
13226
13279
  "ab",
@@ -13432,7 +13485,7 @@ const lang = defineRule({
13432
13485
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13433
13486
  context.report({
13434
13487
  node: langAttr,
13435
- message: MESSAGE$44
13488
+ message: MESSAGE$39
13436
13489
  });
13437
13490
  return;
13438
13491
  }
@@ -13441,7 +13494,7 @@ const lang = defineRule({
13441
13494
  if (value === null) return;
13442
13495
  if (!isValidLangTag(value)) context.report({
13443
13496
  node: langAttr,
13444
- message: MESSAGE$44
13497
+ message: MESSAGE$39
13445
13498
  });
13446
13499
  } })
13447
13500
  });
@@ -13467,6 +13520,7 @@ const mcpToolCapabilityRisk = defineRule({
13467
13520
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13468
13521
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13469
13522
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13523
+ ignoreStringLiterals: true,
13470
13524
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13471
13525
  })
13472
13526
  });
@@ -13485,7 +13539,7 @@ const mdxSsrExecutionRisk = defineRule({
13485
13539
  });
13486
13540
  //#endregion
13487
13541
  //#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>`.";
13542
+ 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
13543
  const DEFAULT_AUDIO = ["audio"];
13490
13544
  const DEFAULT_VIDEO = ["video"];
13491
13545
  const DEFAULT_TRACK = ["track"];
@@ -13526,7 +13580,7 @@ const mediaHasCaption = defineRule({
13526
13580
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13527
13581
  context.report({
13528
13582
  node: node.name,
13529
- message: MESSAGE$43
13583
+ message: MESSAGE$38
13530
13584
  });
13531
13585
  return;
13532
13586
  }
@@ -13543,7 +13597,7 @@ const mediaHasCaption = defineRule({
13543
13597
  return kindValue.value.toLowerCase() === "captions";
13544
13598
  })) context.report({
13545
13599
  node: node.name,
13546
- message: MESSAGE$43
13600
+ message: MESSAGE$38
13547
13601
  });
13548
13602
  } };
13549
13603
  }
@@ -15344,7 +15398,7 @@ const nextjsNoVercelOgImport = defineRule({
15344
15398
  });
15345
15399
  //#endregion
15346
15400
  //#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.";
15401
+ const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15348
15402
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15349
15403
  const noAccessKey = defineRule({
15350
15404
  id: "no-access-key",
@@ -15361,7 +15415,7 @@ const noAccessKey = defineRule({
15361
15415
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15362
15416
  context.report({
15363
15417
  node: accessKey,
15364
- message: MESSAGE$42
15418
+ message: MESSAGE$37
15365
15419
  });
15366
15420
  return;
15367
15421
  }
@@ -15371,7 +15425,7 @@ const noAccessKey = defineRule({
15371
15425
  if (isUndefinedIdentifier(expression)) return;
15372
15426
  context.report({
15373
15427
  node: accessKey,
15374
- message: MESSAGE$42
15428
+ message: MESSAGE$37
15375
15429
  });
15376
15430
  }
15377
15431
  } })
@@ -15853,41 +15907,8 @@ const noAdjustStateOnPropChange = defineRule({
15853
15907
  } })
15854
15908
  });
15855
15909
  //#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
15910
  //#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.";
15911
+ 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
15912
  const noAriaHiddenOnFocusable = defineRule({
15892
15913
  id: "no-aria-hidden-on-focusable",
15893
15914
  title: "aria-hidden on focusable element",
@@ -15914,7 +15935,7 @@ const noAriaHiddenOnFocusable = defineRule({
15914
15935
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15915
15936
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15916
15937
  node: ariaHidden,
15917
- message: MESSAGE$41
15938
+ message: MESSAGE$36
15918
15939
  });
15919
15940
  } })
15920
15941
  });
@@ -16282,7 +16303,7 @@ const noArrayIndexAsKey = defineRule({
16282
16303
  });
16283
16304
  //#endregion
16284
16305
  //#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.";
16306
+ const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16286
16307
  const SECOND_INDEX_METHODS = new Set([
16287
16308
  "every",
16288
16309
  "filter",
@@ -16486,7 +16507,7 @@ const noArrayIndexKey = defineRule({
16486
16507
  }
16487
16508
  context.report({
16488
16509
  node: keyAttribute,
16489
- message: MESSAGE$40
16510
+ message: MESSAGE$35
16490
16511
  });
16491
16512
  },
16492
16513
  CallExpression(node) {
@@ -16506,7 +16527,7 @@ const noArrayIndexKey = defineRule({
16506
16527
  if (propName !== "key") continue;
16507
16528
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16508
16529
  node: property,
16509
- message: MESSAGE$40
16530
+ message: MESSAGE$35
16510
16531
  });
16511
16532
  }
16512
16533
  }
@@ -16514,7 +16535,7 @@ const noArrayIndexKey = defineRule({
16514
16535
  });
16515
16536
  //#endregion
16516
16537
  //#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.";
16538
+ 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
16539
  const noAsyncEffectCallback = defineRule({
16519
16540
  id: "no-async-effect-callback",
16520
16541
  title: "Async effect callback",
@@ -16528,13 +16549,13 @@ const noAsyncEffectCallback = defineRule({
16528
16549
  if (!callback.async) return;
16529
16550
  context.report({
16530
16551
  node: callback,
16531
- message: MESSAGE$39
16552
+ message: MESSAGE$34
16532
16553
  });
16533
16554
  } })
16534
16555
  });
16535
16556
  //#endregion
16536
16557
  //#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.";
16558
+ 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
16559
  const resolveSettings$21 = (settings) => {
16539
16560
  const reactDoctor = settings?.["react-doctor"];
16540
16561
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16590,45 +16611,12 @@ const noAutofocus = defineRule({
16590
16611
  }
16591
16612
  context.report({
16592
16613
  node: autoFocusAttribute,
16593
- message: MESSAGE$38
16614
+ message: MESSAGE$33
16594
16615
  });
16595
16616
  } };
16596
16617
  }
16597
16618
  });
16598
16619
  //#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
16620
  //#region src/plugin/utils/create-relative-import-source.ts
16633
16621
  const createRelativeImportSource = (filename, targetFilePath) => {
16634
16622
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -17127,7 +17115,7 @@ const noChainStateUpdates = defineRule({
17127
17115
  });
17128
17116
  //#endregion
17129
17117
  //#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.";
17118
+ const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17131
17119
  const noChildrenProp = defineRule({
17132
17120
  id: "no-children-prop",
17133
17121
  title: "Children passed as a prop",
@@ -17139,7 +17127,7 @@ const noChildrenProp = defineRule({
17139
17127
  if (node.name.name !== "children") return;
17140
17128
  context.report({
17141
17129
  node: node.name,
17142
- message: MESSAGE$36
17130
+ message: MESSAGE$32
17143
17131
  });
17144
17132
  },
17145
17133
  CallExpression(node) {
@@ -17152,7 +17140,7 @@ const noChildrenProp = defineRule({
17152
17140
  const propertyKey = property.key;
17153
17141
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17154
17142
  node: propertyKey,
17155
- message: MESSAGE$36
17143
+ message: MESSAGE$32
17156
17144
  });
17157
17145
  }
17158
17146
  }
@@ -17160,7 +17148,7 @@ const noChildrenProp = defineRule({
17160
17148
  });
17161
17149
  //#endregion
17162
17150
  //#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.";
17151
+ const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17164
17152
  const noCloneElement = defineRule({
17165
17153
  id: "no-clone-element",
17166
17154
  title: "cloneElement makes child props fragile",
@@ -17173,7 +17161,7 @@ const noCloneElement = defineRule({
17173
17161
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17174
17162
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17175
17163
  node: callee,
17176
- message: MESSAGE$35
17164
+ message: MESSAGE$31
17177
17165
  });
17178
17166
  return;
17179
17167
  }
@@ -17186,7 +17174,7 @@ const noCloneElement = defineRule({
17186
17174
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17187
17175
  context.report({
17188
17176
  node: callee,
17189
- message: MESSAGE$35
17177
+ message: MESSAGE$31
17190
17178
  });
17191
17179
  }
17192
17180
  } })
@@ -17235,7 +17223,7 @@ const enclosingComponentOrHookName = (node) => {
17235
17223
  };
17236
17224
  //#endregion
17237
17225
  //#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.";
17226
+ const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17239
17227
  const CONTEXT_MODULES = [
17240
17228
  "react",
17241
17229
  "use-context-selector",
@@ -17271,13 +17259,13 @@ const noCreateContextInRender = defineRule({
17271
17259
  if (!componentOrHookName) return;
17272
17260
  context.report({
17273
17261
  node,
17274
- message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17262
+ message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17275
17263
  });
17276
17264
  } })
17277
17265
  });
17278
17266
  //#endregion
17279
17267
  //#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.";
17268
+ 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
17269
  const noCreateRefInFunctionComponent = defineRule({
17282
17270
  id: "no-create-ref-in-function-component",
17283
17271
  title: "createRef in function component",
@@ -17296,7 +17284,7 @@ const noCreateRefInFunctionComponent = defineRule({
17296
17284
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17297
17285
  context.report({
17298
17286
  node,
17299
- message: MESSAGE$33
17287
+ message: MESSAGE$29
17300
17288
  });
17301
17289
  } })
17302
17290
  });
@@ -17436,7 +17424,7 @@ const noCreateStoreInRender = defineRule({
17436
17424
  });
17437
17425
  //#endregion
17438
17426
  //#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.";
17427
+ const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17440
17428
  const noDanger = defineRule({
17441
17429
  id: "no-danger",
17442
17430
  title: "Raw HTML injection can run unsafe markup",
@@ -17449,7 +17437,7 @@ const noDanger = defineRule({
17449
17437
  if (!propAttribute) return;
17450
17438
  context.report({
17451
17439
  node: propAttribute.name,
17452
- message: MESSAGE$32
17440
+ message: MESSAGE$28
17453
17441
  });
17454
17442
  },
17455
17443
  CallExpression(node) {
@@ -17461,7 +17449,7 @@ const noDanger = defineRule({
17461
17449
  const propertyKey = property.key;
17462
17450
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17463
17451
  node: propertyKey,
17464
- message: MESSAGE$32
17452
+ message: MESSAGE$28
17465
17453
  });
17466
17454
  }
17467
17455
  }
@@ -17469,7 +17457,7 @@ const noDanger = defineRule({
17469
17457
  });
17470
17458
  //#endregion
17471
17459
  //#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`.";
17460
+ const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17473
17461
  const isLineBreak = (child) => {
17474
17462
  if (!isNodeOfType(child, "JSXText")) return false;
17475
17463
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17539,7 +17527,7 @@ const noDangerWithChildren = defineRule({
17539
17527
  if (!hasChildrenProp && !hasNestedChildren) return;
17540
17528
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17541
17529
  node: opening,
17542
- message: MESSAGE$31
17530
+ message: MESSAGE$27
17543
17531
  });
17544
17532
  },
17545
17533
  CallExpression(node) {
@@ -17551,7 +17539,7 @@ const noDangerWithChildren = defineRule({
17551
17539
  if (!propsShape.hasDangerously) return;
17552
17540
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17553
17541
  node,
17554
- message: MESSAGE$31
17542
+ message: MESSAGE$27
17555
17543
  });
17556
17544
  }
17557
17545
  })
@@ -17716,37 +17704,6 @@ const noDefaultProps = defineRule({
17716
17704
  } })
17717
17705
  });
17718
17706
  //#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
17707
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17751
17708
  const isInitialOnlyPropName = (propName) => {
17752
17709
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -18159,7 +18116,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
18159
18116
  //#endregion
18160
18117
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
18161
18118
  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`.";
18119
+ const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18163
18120
  const resolveSettings$20 = (settings) => {
18164
18121
  const reactDoctor = settings?.["react-doctor"];
18165
18122
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18178,7 +18135,7 @@ const noDidMountSetState = defineRule({
18178
18135
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18179
18136
  context.report({
18180
18137
  node: node.callee,
18181
- message: MESSAGE$30
18138
+ message: MESSAGE$26
18182
18139
  });
18183
18140
  } };
18184
18141
  }
@@ -18186,7 +18143,7 @@ const noDidMountSetState = defineRule({
18186
18143
  //#endregion
18187
18144
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18188
18145
  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.";
18146
+ const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18190
18147
  const resolveSettings$19 = (settings) => {
18191
18148
  const reactDoctor = settings?.["react-doctor"];
18192
18149
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18205,7 +18162,7 @@ const noDidUpdateSetState = defineRule({
18205
18162
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18206
18163
  context.report({
18207
18164
  node: node.callee,
18208
- message: MESSAGE$29
18165
+ message: MESSAGE$25
18209
18166
  });
18210
18167
  } };
18211
18168
  }
@@ -18228,7 +18185,7 @@ const isStateMemberExpression = (node) => {
18228
18185
  };
18229
18186
  //#endregion
18230
18187
  //#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.";
18188
+ const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18232
18189
  const shouldIgnoreMutation = (node) => {
18233
18190
  let isConstructor = false;
18234
18191
  let isInsideCallExpression = false;
@@ -18250,7 +18207,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18250
18207
  if (shouldIgnoreMutation(reportNode)) return;
18251
18208
  context.report({
18252
18209
  node: reportNode,
18253
- message: MESSAGE$28
18210
+ message: MESSAGE$24
18254
18211
  });
18255
18212
  };
18256
18213
  const noDirectMutationState = defineRule({
@@ -18461,7 +18418,7 @@ const noDocumentStartViewTransition = defineRule({
18461
18418
  });
18462
18419
  //#endregion
18463
18420
  //#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.";
18421
+ 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
18422
  const WRITE_METHODS = new Set(["write", "writeln"]);
18466
18423
  const noDocumentWrite = defineRule({
18467
18424
  id: "no-document-write",
@@ -18475,7 +18432,7 @@ const noDocumentWrite = defineRule({
18475
18432
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18476
18433
  context.report({
18477
18434
  node,
18478
- message: MESSAGE$27
18435
+ message: MESSAGE$23
18479
18436
  });
18480
18437
  } })
18481
18438
  });
@@ -19858,7 +19815,7 @@ const ALLOWED_NAMESPACES = new Set([
19858
19815
  "ReactDOM",
19859
19816
  "ReactDom"
19860
19817
  ]);
19861
- const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19818
+ const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19862
19819
  const noFindDomNode = defineRule({
19863
19820
  id: "no-find-dom-node",
19864
19821
  title: "findDOMNode breaks component encapsulation",
@@ -19869,7 +19826,7 @@ const noFindDomNode = defineRule({
19869
19826
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19870
19827
  context.report({
19871
19828
  node: callee,
19872
- message: MESSAGE$26
19829
+ message: MESSAGE$22
19873
19830
  });
19874
19831
  return;
19875
19832
  }
@@ -19880,7 +19837,7 @@ const noFindDomNode = defineRule({
19880
19837
  if (callee.property.name !== "findDOMNode") return;
19881
19838
  context.report({
19882
19839
  node: callee.property,
19883
- message: MESSAGE$26
19840
+ message: MESSAGE$22
19884
19841
  });
19885
19842
  }
19886
19843
  } })
@@ -19921,41 +19878,6 @@ const noFullLodashImport = defineRule({
19921
19878
  } })
19922
19879
  });
19923
19880
  //#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
19881
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19960
19882
  const noGenericHandlerNames = defineRule({
19961
19883
  id: "no-generic-handler-names",
@@ -20018,7 +19940,7 @@ const noGiantComponent = defineRule({
20018
19940
  });
20019
19941
  //#endregion
20020
19942
  //#region src/plugin/constants/style.ts
20021
- const LAYOUT_PROPERTIES$1 = new Set([
19943
+ const LAYOUT_PROPERTIES = new Set([
20022
19944
  "width",
20023
19945
  "height",
20024
19946
  "top",
@@ -20088,6 +20010,17 @@ const noGlobalCssVariableAnimation = defineRule({
20088
20010
  } })
20089
20011
  });
20090
20012
  //#endregion
20013
+ //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
20014
+ const getStringFromClassNameAttr = (node) => {
20015
+ if (!isNodeOfType(node, "JSXOpeningElement")) return null;
20016
+ const classAttr = findJsxAttribute(node.attributes ?? [], "className");
20017
+ if (!classAttr?.value) return null;
20018
+ if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
20019
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
20020
+ 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;
20021
+ return null;
20022
+ };
20023
+ //#endregion
20091
20024
  //#region src/plugin/rules/design/no-gradient-text.ts
20092
20025
  const noGradientText = defineRule({
20093
20026
  id: "no-gradient-text",
@@ -20146,7 +20079,7 @@ const noGrayOnColoredBackground = defineRule({
20146
20079
  });
20147
20080
  //#endregion
20148
20081
  //#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.";
20082
+ 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
20083
  const noImgLazyWithHighFetchpriority = defineRule({
20151
20084
  id: "no-img-lazy-with-high-fetchpriority",
20152
20085
  title: "Lazy image with high fetchPriority",
@@ -20160,7 +20093,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
20160
20093
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20161
20094
  context.report({
20162
20095
  node: node.name,
20163
- message: MESSAGE$24
20096
+ message: MESSAGE$21
20164
20097
  });
20165
20098
  } })
20166
20099
  });
@@ -20257,15 +20190,20 @@ const noInlineExhaustiveStyle = defineRule({
20257
20190
  severity: "warn",
20258
20191
  tags: ["test-noise", "react-jsx-only"],
20259
20192
  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
- } })
20193
+ create: (context) => {
20194
+ if (isGeneratedImageRenderContext(context)) return {};
20195
+ return { JSXAttribute(node) {
20196
+ const expression = getInlineStyleExpression(node);
20197
+ if (!expression) return;
20198
+ const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
20199
+ if (propertyCount < 8) return;
20200
+ if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
20201
+ context.report({
20202
+ node: expression,
20203
+ 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.`
20204
+ });
20205
+ } };
20206
+ }
20269
20207
  });
20270
20208
  //#endregion
20271
20209
  //#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
@@ -20395,7 +20333,7 @@ const noIsMounted = defineRule({
20395
20333
  });
20396
20334
  //#endregion
20397
20335
  //#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)`.";
20336
+ 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
20337
  const isJsonMethodCall = (node, method) => {
20400
20338
  if (!isNodeOfType(node, "CallExpression")) return false;
20401
20339
  const callee = node.callee;
@@ -20412,13 +20350,13 @@ const noJsonParseStringifyClone = defineRule({
20412
20350
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20413
20351
  context.report({
20414
20352
  node,
20415
- message: MESSAGE$23
20353
+ message: MESSAGE$20
20416
20354
  });
20417
20355
  } })
20418
20356
  });
20419
20357
  //#endregion
20420
20358
  //#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.";
20359
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20422
20360
  const isJsxElementTypeReference = (node) => {
20423
20361
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20424
20362
  const typeName = node.typeName;
@@ -20435,7 +20373,7 @@ const checkReturnType = (context, returnType) => {
20435
20373
  if (!typeAnnotation) return;
20436
20374
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20437
20375
  node: typeAnnotation,
20438
- message: MESSAGE$22
20376
+ message: MESSAGE$19
20439
20377
  });
20440
20378
  };
20441
20379
  const noJsxElementType = defineRule({
@@ -20545,7 +20483,7 @@ const noLayoutPropertyAnimation = defineRule({
20545
20483
  let propertyName = null;
20546
20484
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20547
20485
  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({
20486
+ if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20549
20487
  node: property,
20550
20488
  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
20489
  });
@@ -20735,134 +20673,6 @@ const noLongTransitionDuration = defineRule({
20735
20673
  } })
20736
20674
  });
20737
20675
  //#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
20676
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20867
20677
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20868
20678
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -21018,7 +20828,7 @@ const noMoment = defineRule({
21018
20828
  });
21019
20829
  //#endregion
21020
20830
  //#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.";
20831
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
21022
20832
  const resolveSettings$16 = (settings) => {
21023
20833
  const reactDoctor = settings?.["react-doctor"];
21024
20834
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21340,7 +21150,7 @@ const noMultiComp = defineRule({
21340
21150
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21341
21151
  for (const component of flagged.slice(1)) context.report({
21342
21152
  node: component.reportNode,
21343
- message: MESSAGE$21
21153
+ message: MESSAGE$18
21344
21154
  });
21345
21155
  } };
21346
21156
  }
@@ -21508,7 +21318,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21508
21318
  };
21509
21319
  //#endregion
21510
21320
  //#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.";
21321
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
21512
21322
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21513
21323
  "copyWithin",
21514
21324
  "fill",
@@ -21718,7 +21528,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21718
21528
  reportedNodes.add(options.crossFileConsumerCallSite);
21719
21529
  context.report({
21720
21530
  node: options.crossFileConsumerCallSite,
21721
- message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21531
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21722
21532
  });
21723
21533
  return;
21724
21534
  }
@@ -21727,7 +21537,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21727
21537
  reportedNodes.add(mutation.node);
21728
21538
  context.report({
21729
21539
  node: mutation.node,
21730
- message: MESSAGE$20
21540
+ message: MESSAGE$17
21731
21541
  });
21732
21542
  }
21733
21543
  };
@@ -21999,7 +21809,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21999
21809
  });
22000
21810
  //#endregion
22001
21811
  //#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.";
21812
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22003
21813
  const resolveSettings$14 = (settings) => {
22004
21814
  const reactDoctor = settings?.["react-doctor"];
22005
21815
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -22027,7 +21837,7 @@ const noNoninteractiveTabindex = defineRule({
22027
21837
  if (numeric === null) {
22028
21838
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
22029
21839
  node: tabIndex,
22030
- message: MESSAGE$19
21840
+ message: MESSAGE$16
22031
21841
  });
22032
21842
  return;
22033
21843
  }
@@ -22040,7 +21850,7 @@ const noNoninteractiveTabindex = defineRule({
22040
21850
  if (!roleAttribute) {
22041
21851
  context.report({
22042
21852
  node: tabIndex,
22043
- message: MESSAGE$19
21853
+ message: MESSAGE$16
22044
21854
  });
22045
21855
  return;
22046
21856
  }
@@ -22054,12 +21864,20 @@ const noNoninteractiveTabindex = defineRule({
22054
21864
  }
22055
21865
  context.report({
22056
21866
  node: tabIndex,
22057
- message: MESSAGE$19
21867
+ message: MESSAGE$16
22058
21868
  });
22059
21869
  } };
22060
21870
  }
22061
21871
  });
22062
21872
  //#endregion
21873
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21874
+ const getStylePropertyNumberValue = (property) => {
21875
+ if (!isNodeOfType(property, "Property")) return null;
21876
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21877
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21878
+ return null;
21879
+ };
21880
+ //#endregion
22063
21881
  //#region src/plugin/rules/design/no-outline-none.ts
22064
21882
  const noOutlineNone = defineRule({
22065
21883
  id: "no-outline-none",
@@ -22737,7 +22555,7 @@ const noRandomKey = defineRule({
22737
22555
  });
22738
22556
  //#endregion
22739
22557
  //#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.";
22558
+ 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
22559
  const isChildrenIdentifier = (node, contextNode) => {
22742
22560
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22743
22561
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22763,13 +22581,13 @@ const noReactChildren = defineRule({
22763
22581
  if (isChildrenIdentifier(memberObject, node)) {
22764
22582
  context.report({
22765
22583
  node: calleeOuter,
22766
- message: MESSAGE$18
22584
+ message: MESSAGE$15
22767
22585
  });
22768
22586
  return;
22769
22587
  }
22770
22588
  if (isReactNamespaceMember(memberObject, node)) context.report({
22771
22589
  node: calleeOuter,
22772
- message: MESSAGE$18
22590
+ message: MESSAGE$15
22773
22591
  });
22774
22592
  } })
22775
22593
  });
@@ -22880,86 +22698,6 @@ const noReact19DeprecatedApis = defineRule({
22880
22698
  })
22881
22699
  });
22882
22700
  //#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
22701
  //#region src/plugin/constants/aria-element-roles.ts
22964
22702
  const ELEMENT_ROLE_PAIRS = [
22965
22703
  ["a", "link"],
@@ -23172,7 +22910,7 @@ const noRenderPropChildren = defineRule({
23172
22910
  });
23173
22911
  //#endregion
23174
22912
  //#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.";
22913
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23176
22914
  const isReactDomRenderCall = (node) => {
23177
22915
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
23178
22916
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -23196,7 +22934,7 @@ const noRenderReturnValue = defineRule({
23196
22934
  if (!isUsedAsReturnValue(node.parent)) return;
23197
22935
  context.report({
23198
22936
  node: node.callee,
23199
- message: MESSAGE$17
22937
+ message: MESSAGE$14
23200
22938
  });
23201
22939
  } })
23202
22940
  });
@@ -23894,7 +23632,7 @@ const getParentComponent = (node) => {
23894
23632
  };
23895
23633
  //#endregion
23896
23634
  //#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.";
23635
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23898
23636
  const noSetState = defineRule({
23899
23637
  id: "no-set-state",
23900
23638
  title: "Local class state forbidden",
@@ -23909,7 +23647,7 @@ const noSetState = defineRule({
23909
23647
  if (!getParentComponent(node)) return;
23910
23648
  context.report({
23911
23649
  node: node.callee,
23912
- message: MESSAGE$16
23650
+ message: MESSAGE$13
23913
23651
  });
23914
23652
  } })
23915
23653
  });
@@ -24071,7 +23809,7 @@ const isAbstractRole = (openingElement, settings) => {
24071
23809
  };
24072
23810
  //#endregion
24073
23811
  //#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.";
23812
+ 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
23813
  const DEFAULT_HANDLERS = [
24076
23814
  "onClick",
24077
23815
  "onMouseDown",
@@ -24131,7 +23869,7 @@ const noStaticElementInteractions = defineRule({
24131
23869
  if (!roleAttribute || !roleAttribute.value) {
24132
23870
  context.report({
24133
23871
  node: node.name,
24134
- message: MESSAGE$15
23872
+ message: MESSAGE$12
24135
23873
  });
24136
23874
  return;
24137
23875
  }
@@ -24141,14 +23879,14 @@ const noStaticElementInteractions = defineRule({
24141
23879
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
24142
23880
  context.report({
24143
23881
  node: node.name,
24144
- message: MESSAGE$15
23882
+ message: MESSAGE$12
24145
23883
  });
24146
23884
  return;
24147
23885
  }
24148
23886
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
24149
23887
  context.report({
24150
23888
  node: node.name,
24151
- message: MESSAGE$15
23889
+ message: MESSAGE$12
24152
23890
  });
24153
23891
  } };
24154
23892
  }
@@ -24251,43 +23989,8 @@ const noStringRefs = defineRule({
24251
23989
  }
24252
23990
  });
24253
23991
  //#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
23992
  //#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)`).";
23993
+ 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
23994
  const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24292
23995
  const noSyncXhr = defineRule({
24293
23996
  id: "no-sync-xhr",
@@ -24302,103 +24005,13 @@ const noSyncXhr = defineRule({
24302
24005
  if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24303
24006
  context.report({
24304
24007
  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
24008
+ message: MESSAGE$11
24396
24009
  });
24397
24010
  } })
24398
24011
  });
24399
24012
  //#endregion
24400
24013
  //#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`.";
24014
+ const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24402
24015
  const isInsideClassMethod = (node, customClassFactoryNames) => {
24403
24016
  let ancestor = node.parent;
24404
24017
  while (ancestor) {
@@ -24467,7 +24080,7 @@ const noThisInSfc = defineRule({
24467
24080
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
24468
24081
  context.report({
24469
24082
  node,
24470
- message: MESSAGE$12
24083
+ message: MESSAGE$10
24471
24084
  });
24472
24085
  } };
24473
24086
  }
@@ -24505,39 +24118,26 @@ const noTinyText = defineRule({
24505
24118
  });
24506
24119
  //#endregion
24507
24120
  //#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
24121
  const noTransitionAll = defineRule({
24511
24122
  id: "no-transition-all",
24512
24123
  title: "transition: all animates everything",
24513
24124
  tags: ["test-noise"],
24514
24125
  severity: "warn",
24515
24126
  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
24127
+ create: (context) => ({ JSXAttribute(node) {
24128
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24129
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24130
+ const expression = node.value.expression;
24131
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24132
+ for (const property of expression.properties ?? []) {
24133
+ if (!isNodeOfType(property, "Property")) continue;
24134
+ if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
24135
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
24136
+ node: property,
24137
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24538
24138
  });
24539
24139
  }
24540
- })
24140
+ } })
24541
24141
  });
24542
24142
  //#endregion
24543
24143
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -24581,6 +24181,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
24581
24181
  }
24582
24182
  return stateNames;
24583
24183
  };
24184
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
24584
24185
  const noUncontrolledInput = defineRule({
24585
24186
  id: "no-uncontrolled-input",
24586
24187
  title: "Uncontrolled input value",
@@ -24684,38 +24285,6 @@ const noUnescapedEntities = defineRule({
24684
24285
  } })
24685
24286
  });
24686
24287
  //#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
24288
  //#region src/plugin/constants/dom-aria-properties.ts
24720
24289
  const ARIA_PROPERTY_NAMES = new Set([
24721
24290
  "activedescendant",
@@ -26187,7 +25756,7 @@ const noWideLetterSpacing = defineRule({
26187
25756
  //#endregion
26188
25757
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
26189
25758
  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.";
25759
+ const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26191
25760
  const resolveSettings$7 = (settings) => {
26192
25761
  const reactDoctor = settings?.["react-doctor"];
26193
25762
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -26221,7 +25790,7 @@ const noWillUpdateSetState = defineRule({
26221
25790
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
26222
25791
  context.report({
26223
25792
  node: node.callee,
26224
- message: MESSAGE$10
25793
+ message: MESSAGE$9
26225
25794
  });
26226
25795
  } };
26227
25796
  }
@@ -27099,7 +26668,7 @@ const preactNoRenderArguments = defineRule({
27099
26668
  });
27100
26669
  //#endregion
27101
26670
  //#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.";
26671
+ 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
26672
  const preactPreferOndblclick = defineRule({
27104
26673
  id: "preact-prefer-ondblclick",
27105
26674
  title: "onDoubleClick instead of onDblClick",
@@ -27114,7 +26683,7 @@ const preactPreferOndblclick = defineRule({
27114
26683
  if (!onDoubleClickAttribute) return;
27115
26684
  context.report({
27116
26685
  node: onDoubleClickAttribute,
27117
- message: MESSAGE$9
26686
+ message: MESSAGE$8
27118
26687
  });
27119
26688
  } })
27120
26689
  });
@@ -27154,42 +26723,6 @@ const preactPreferOninput = defineRule({
27154
26723
  } })
27155
26724
  });
27156
26725
  //#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
26726
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
27194
26727
  const preferDynamicImport = defineRule({
27195
26728
  id: "prefer-dynamic-import",
@@ -27781,26 +27314,6 @@ const preferTagOverRole = defineRule({
27781
27314
  } })
27782
27315
  });
27783
27316
  //#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
27317
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27805
27318
  const collectFunctionTypedLocalBindings = (componentBody) => {
27806
27319
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -36072,13 +35585,7 @@ const serverNoMutableModuleState = defineRule({
36072
35585
  const collectDeclaredNames = (declaration) => {
36073
35586
  const names = /* @__PURE__ */ new Set();
36074
35587
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
36075
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
36076
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
36077
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
36078
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
36079
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
36080
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
36081
- }
35588
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
36082
35589
  return names;
36083
35590
  };
36084
35591
  const declarationStartsWithAwait = (declaration) => {
@@ -36088,11 +35595,15 @@ const declarationStartsWithAwait = (declaration) => {
36088
35595
  };
36089
35596
  const declarationReadsAnyName = (declaration, names) => {
36090
35597
  if (names.size === 0) return false;
35598
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
36091
35599
  let didRead = false;
36092
- walkAst(declaration, (child) => {
36093
- if (didRead) return;
36094
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
36095
- });
35600
+ for (const declarator of declaration.declarations ?? []) {
35601
+ if (!declarator.init) continue;
35602
+ walkAst(declarator.init, (child) => {
35603
+ if (didRead) return;
35604
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35605
+ });
35606
+ }
36096
35607
  return didRead;
36097
35608
  };
36098
35609
  const serverSequentialIndependentAwait = defineRule({
@@ -39477,17 +38988,6 @@ const reactDoctorRules = [
39477
38988
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
39478
38989
  }
39479
38990
  },
39480
- {
39481
- key: "react-doctor/no-arbitrary-px-font-size",
39482
- id: "no-arbitrary-px-font-size",
39483
- source: "react-doctor",
39484
- originallyExternal: false,
39485
- rule: {
39486
- ...noArbitraryPxFontSize,
39487
- framework: "global",
39488
- category: "Accessibility"
39489
- }
39490
- },
39491
38991
  {
39492
38992
  key: "react-doctor/no-aria-hidden-on-focusable",
39493
38993
  id: "no-aria-hidden-on-focusable",
@@ -39547,18 +39047,6 @@ const reactDoctorRules = [
39547
39047
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
39548
39048
  }
39549
39049
  },
39550
- {
39551
- key: "react-doctor/no-autoplay-without-muted",
39552
- id: "no-autoplay-without-muted",
39553
- source: "react-doctor",
39554
- originallyExternal: false,
39555
- rule: {
39556
- ...noAutoplayWithoutMuted,
39557
- framework: "global",
39558
- category: "Accessibility",
39559
- requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39560
- }
39561
- },
39562
39050
  {
39563
39051
  key: "react-doctor/no-barrel-import",
39564
39052
  id: "no-barrel-import",
@@ -39712,17 +39200,6 @@ const reactDoctorRules = [
39712
39200
  category: "Maintainability"
39713
39201
  }
39714
39202
  },
39715
- {
39716
- key: "react-doctor/no-deprecated-tailwind-class",
39717
- id: "no-deprecated-tailwind-class",
39718
- source: "react-doctor",
39719
- originallyExternal: false,
39720
- rule: {
39721
- ...noDeprecatedTailwindClass,
39722
- framework: "global",
39723
- category: "Maintainability"
39724
- }
39725
- },
39726
39203
  {
39727
39204
  key: "react-doctor/no-derived-state",
39728
39205
  id: "no-derived-state",
@@ -39994,17 +39471,6 @@ const reactDoctorRules = [
39994
39471
  category: "Performance"
39995
39472
  }
39996
39473
  },
39997
- {
39998
- key: "react-doctor/no-full-viewport-width",
39999
- id: "no-full-viewport-width",
40000
- source: "react-doctor",
40001
- originallyExternal: false,
40002
- rule: {
40003
- ...noFullViewportWidth,
40004
- framework: "global",
40005
- category: "Maintainability"
40006
- }
40007
- },
40008
39474
  {
40009
39475
  key: "react-doctor/no-generic-handler-names",
40010
39476
  id: "no-generic-handler-names",
@@ -40244,17 +39710,6 @@ const reactDoctorRules = [
40244
39710
  category: "Performance"
40245
39711
  }
40246
39712
  },
40247
- {
40248
- key: "react-doctor/no-low-contrast-inline-style",
40249
- id: "no-low-contrast-inline-style",
40250
- source: "react-doctor",
40251
- originallyExternal: false,
40252
- rule: {
40253
- ...noLowContrastInlineStyle,
40254
- framework: "global",
40255
- category: "Accessibility"
40256
- }
40257
- },
40258
39713
  {
40259
39714
  key: "react-doctor/no-many-boolean-props",
40260
39715
  id: "no-many-boolean-props",
@@ -40532,17 +39987,6 @@ const reactDoctorRules = [
40532
39987
  category: "Maintainability"
40533
39988
  }
40534
39989
  },
40535
- {
40536
- key: "react-doctor/no-redundant-display-class",
40537
- id: "no-redundant-display-class",
40538
- source: "react-doctor",
40539
- originallyExternal: false,
40540
- rule: {
40541
- ...noRedundantDisplayClass,
40542
- framework: "global",
40543
- category: "Maintainability"
40544
- }
40545
- },
40546
39990
  {
40547
39991
  key: "react-doctor/no-redundant-roles",
40548
39992
  id: "no-redundant-roles",
@@ -40719,17 +40163,6 @@ const reactDoctorRules = [
40719
40163
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
40720
40164
  }
40721
40165
  },
40722
- {
40723
- key: "react-doctor/no-svg-currentcolor-with-fill-class",
40724
- id: "no-svg-currentcolor-with-fill-class",
40725
- source: "react-doctor",
40726
- originallyExternal: false,
40727
- rule: {
40728
- ...noSvgCurrentcolorWithFillClass,
40729
- framework: "global",
40730
- category: "Maintainability"
40731
- }
40732
- },
40733
40166
  {
40734
40167
  key: "react-doctor/no-sync-xhr",
40735
40168
  id: "no-sync-xhr",
@@ -40741,29 +40174,6 @@ const reactDoctorRules = [
40741
40174
  category: "Performance"
40742
40175
  }
40743
40176
  },
40744
- {
40745
- key: "react-doctor/no-tailwind-layout-transition",
40746
- id: "no-tailwind-layout-transition",
40747
- source: "react-doctor",
40748
- originallyExternal: false,
40749
- rule: {
40750
- ...noTailwindLayoutTransition,
40751
- framework: "global",
40752
- category: "Performance"
40753
- }
40754
- },
40755
- {
40756
- key: "react-doctor/no-target-blank-without-rel",
40757
- id: "no-target-blank-without-rel",
40758
- source: "react-doctor",
40759
- originallyExternal: false,
40760
- rule: {
40761
- ...noTargetBlankWithoutRel,
40762
- framework: "global",
40763
- category: "Accessibility",
40764
- requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40765
- }
40766
- },
40767
40177
  {
40768
40178
  key: "react-doctor/no-this-in-sfc",
40769
40179
  id: "no-this-in-sfc",
@@ -40833,18 +40243,6 @@ const reactDoctorRules = [
40833
40243
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
40834
40244
  }
40835
40245
  },
40836
- {
40837
- key: "react-doctor/no-uninformative-aria-label",
40838
- id: "no-uninformative-aria-label",
40839
- source: "react-doctor",
40840
- originallyExternal: false,
40841
- rule: {
40842
- ...noUninformativeAriaLabel,
40843
- framework: "global",
40844
- category: "Accessibility",
40845
- requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40846
- }
40847
- },
40848
40246
  {
40849
40247
  key: "react-doctor/no-unknown-property",
40850
40248
  id: "no-unknown-property",
@@ -41054,17 +40452,6 @@ const reactDoctorRules = [
41054
40452
  category: "Bugs"
41055
40453
  }
41056
40454
  },
41057
- {
41058
- key: "react-doctor/prefer-dvh-over-vh",
41059
- id: "prefer-dvh-over-vh",
41060
- source: "react-doctor",
41061
- originallyExternal: false,
41062
- rule: {
41063
- ...preferDvhOverVh,
41064
- framework: "global",
41065
- category: "Maintainability"
41066
- }
41067
- },
41068
40455
  {
41069
40456
  key: "react-doctor/prefer-dynamic-import",
41070
40457
  id: "prefer-dynamic-import",
@@ -41169,17 +40556,6 @@ const reactDoctorRules = [
41169
40556
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
41170
40557
  }
41171
40558
  },
41172
- {
41173
- key: "react-doctor/prefer-truncate-shorthand",
41174
- id: "prefer-truncate-shorthand",
41175
- source: "react-doctor",
41176
- originallyExternal: false,
41177
- rule: {
41178
- ...preferTruncateShorthand,
41179
- framework: "global",
41180
- category: "Maintainability"
41181
- }
41182
- },
41183
40559
  {
41184
40560
  key: "react-doctor/prefer-use-effect-event",
41185
40561
  id: "prefer-use-effect-event",