oxlint-plugin-react-doctor 0.5.6-dev.6b8e756 → 0.5.6-dev.7d9e676

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 +225 -852
  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
  });
@@ -20395,7 +20328,7 @@ const noIsMounted = defineRule({
20395
20328
  });
20396
20329
  //#endregion
20397
20330
  //#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)`.";
20331
+ 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
20332
  const isJsonMethodCall = (node, method) => {
20400
20333
  if (!isNodeOfType(node, "CallExpression")) return false;
20401
20334
  const callee = node.callee;
@@ -20412,13 +20345,13 @@ const noJsonParseStringifyClone = defineRule({
20412
20345
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20413
20346
  context.report({
20414
20347
  node,
20415
- message: MESSAGE$23
20348
+ message: MESSAGE$20
20416
20349
  });
20417
20350
  } })
20418
20351
  });
20419
20352
  //#endregion
20420
20353
  //#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.";
20354
+ const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20422
20355
  const isJsxElementTypeReference = (node) => {
20423
20356
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20424
20357
  const typeName = node.typeName;
@@ -20435,7 +20368,7 @@ const checkReturnType = (context, returnType) => {
20435
20368
  if (!typeAnnotation) return;
20436
20369
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20437
20370
  node: typeAnnotation,
20438
- message: MESSAGE$22
20371
+ message: MESSAGE$19
20439
20372
  });
20440
20373
  };
20441
20374
  const noJsxElementType = defineRule({
@@ -20545,7 +20478,7 @@ const noLayoutPropertyAnimation = defineRule({
20545
20478
  let propertyName = null;
20546
20479
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20547
20480
  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({
20481
+ if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20549
20482
  node: property,
20550
20483
  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
20484
  });
@@ -20735,134 +20668,6 @@ const noLongTransitionDuration = defineRule({
20735
20668
  } })
20736
20669
  });
20737
20670
  //#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
20671
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20867
20672
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20868
20673
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -21018,7 +20823,7 @@ const noMoment = defineRule({
21018
20823
  });
21019
20824
  //#endregion
21020
20825
  //#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.";
20826
+ const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
21022
20827
  const resolveSettings$16 = (settings) => {
21023
20828
  const reactDoctor = settings?.["react-doctor"];
21024
20829
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21340,7 +21145,7 @@ const noMultiComp = defineRule({
21340
21145
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21341
21146
  for (const component of flagged.slice(1)) context.report({
21342
21147
  node: component.reportNode,
21343
- message: MESSAGE$21
21148
+ message: MESSAGE$18
21344
21149
  });
21345
21150
  } };
21346
21151
  }
@@ -21508,7 +21313,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21508
21313
  };
21509
21314
  //#endregion
21510
21315
  //#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.";
21316
+ const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
21512
21317
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21513
21318
  "copyWithin",
21514
21319
  "fill",
@@ -21718,7 +21523,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21718
21523
  reportedNodes.add(options.crossFileConsumerCallSite);
21719
21524
  context.report({
21720
21525
  node: options.crossFileConsumerCallSite,
21721
- message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21526
+ message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21722
21527
  });
21723
21528
  return;
21724
21529
  }
@@ -21727,7 +21532,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21727
21532
  reportedNodes.add(mutation.node);
21728
21533
  context.report({
21729
21534
  node: mutation.node,
21730
- message: MESSAGE$20
21535
+ message: MESSAGE$17
21731
21536
  });
21732
21537
  }
21733
21538
  };
@@ -21999,7 +21804,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21999
21804
  });
22000
21805
  //#endregion
22001
21806
  //#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.";
21807
+ const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22003
21808
  const resolveSettings$14 = (settings) => {
22004
21809
  const reactDoctor = settings?.["react-doctor"];
22005
21810
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -22027,7 +21832,7 @@ const noNoninteractiveTabindex = defineRule({
22027
21832
  if (numeric === null) {
22028
21833
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
22029
21834
  node: tabIndex,
22030
- message: MESSAGE$19
21835
+ message: MESSAGE$16
22031
21836
  });
22032
21837
  return;
22033
21838
  }
@@ -22040,7 +21845,7 @@ const noNoninteractiveTabindex = defineRule({
22040
21845
  if (!roleAttribute) {
22041
21846
  context.report({
22042
21847
  node: tabIndex,
22043
- message: MESSAGE$19
21848
+ message: MESSAGE$16
22044
21849
  });
22045
21850
  return;
22046
21851
  }
@@ -22054,12 +21859,20 @@ const noNoninteractiveTabindex = defineRule({
22054
21859
  }
22055
21860
  context.report({
22056
21861
  node: tabIndex,
22057
- message: MESSAGE$19
21862
+ message: MESSAGE$16
22058
21863
  });
22059
21864
  } };
22060
21865
  }
22061
21866
  });
22062
21867
  //#endregion
21868
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21869
+ const getStylePropertyNumberValue = (property) => {
21870
+ if (!isNodeOfType(property, "Property")) return null;
21871
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21872
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21873
+ return null;
21874
+ };
21875
+ //#endregion
22063
21876
  //#region src/plugin/rules/design/no-outline-none.ts
22064
21877
  const noOutlineNone = defineRule({
22065
21878
  id: "no-outline-none",
@@ -22737,7 +22550,7 @@ const noRandomKey = defineRule({
22737
22550
  });
22738
22551
  //#endregion
22739
22552
  //#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.";
22553
+ 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
22554
  const isChildrenIdentifier = (node, contextNode) => {
22742
22555
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22743
22556
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22763,13 +22576,13 @@ const noReactChildren = defineRule({
22763
22576
  if (isChildrenIdentifier(memberObject, node)) {
22764
22577
  context.report({
22765
22578
  node: calleeOuter,
22766
- message: MESSAGE$18
22579
+ message: MESSAGE$15
22767
22580
  });
22768
22581
  return;
22769
22582
  }
22770
22583
  if (isReactNamespaceMember(memberObject, node)) context.report({
22771
22584
  node: calleeOuter,
22772
- message: MESSAGE$18
22585
+ message: MESSAGE$15
22773
22586
  });
22774
22587
  } })
22775
22588
  });
@@ -22880,86 +22693,6 @@ const noReact19DeprecatedApis = defineRule({
22880
22693
  })
22881
22694
  });
22882
22695
  //#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
22696
  //#region src/plugin/constants/aria-element-roles.ts
22964
22697
  const ELEMENT_ROLE_PAIRS = [
22965
22698
  ["a", "link"],
@@ -23172,7 +22905,7 @@ const noRenderPropChildren = defineRule({
23172
22905
  });
23173
22906
  //#endregion
23174
22907
  //#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.";
22908
+ const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23176
22909
  const isReactDomRenderCall = (node) => {
23177
22910
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
23178
22911
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -23196,7 +22929,7 @@ const noRenderReturnValue = defineRule({
23196
22929
  if (!isUsedAsReturnValue(node.parent)) return;
23197
22930
  context.report({
23198
22931
  node: node.callee,
23199
- message: MESSAGE$17
22932
+ message: MESSAGE$14
23200
22933
  });
23201
22934
  } })
23202
22935
  });
@@ -23894,7 +23627,7 @@ const getParentComponent = (node) => {
23894
23627
  };
23895
23628
  //#endregion
23896
23629
  //#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.";
23630
+ const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23898
23631
  const noSetState = defineRule({
23899
23632
  id: "no-set-state",
23900
23633
  title: "Local class state forbidden",
@@ -23909,7 +23642,7 @@ const noSetState = defineRule({
23909
23642
  if (!getParentComponent(node)) return;
23910
23643
  context.report({
23911
23644
  node: node.callee,
23912
- message: MESSAGE$16
23645
+ message: MESSAGE$13
23913
23646
  });
23914
23647
  } })
23915
23648
  });
@@ -24071,7 +23804,7 @@ const isAbstractRole = (openingElement, settings) => {
24071
23804
  };
24072
23805
  //#endregion
24073
23806
  //#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.";
23807
+ 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
23808
  const DEFAULT_HANDLERS = [
24076
23809
  "onClick",
24077
23810
  "onMouseDown",
@@ -24131,7 +23864,7 @@ const noStaticElementInteractions = defineRule({
24131
23864
  if (!roleAttribute || !roleAttribute.value) {
24132
23865
  context.report({
24133
23866
  node: node.name,
24134
- message: MESSAGE$15
23867
+ message: MESSAGE$12
24135
23868
  });
24136
23869
  return;
24137
23870
  }
@@ -24141,14 +23874,14 @@ const noStaticElementInteractions = defineRule({
24141
23874
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
24142
23875
  context.report({
24143
23876
  node: node.name,
24144
- message: MESSAGE$15
23877
+ message: MESSAGE$12
24145
23878
  });
24146
23879
  return;
24147
23880
  }
24148
23881
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
24149
23882
  context.report({
24150
23883
  node: node.name,
24151
- message: MESSAGE$15
23884
+ message: MESSAGE$12
24152
23885
  });
24153
23886
  } };
24154
23887
  }
@@ -24251,43 +23984,8 @@ const noStringRefs = defineRule({
24251
23984
  }
24252
23985
  });
24253
23986
  //#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
23987
  //#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)`).";
23988
+ 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
23989
  const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24292
23990
  const noSyncXhr = defineRule({
24293
23991
  id: "no-sync-xhr",
@@ -24302,103 +24000,13 @@ const noSyncXhr = defineRule({
24302
24000
  if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24303
24001
  context.report({
24304
24002
  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
24003
+ message: MESSAGE$11
24396
24004
  });
24397
24005
  } })
24398
24006
  });
24399
24007
  //#endregion
24400
24008
  //#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`.";
24009
+ const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24402
24010
  const isInsideClassMethod = (node, customClassFactoryNames) => {
24403
24011
  let ancestor = node.parent;
24404
24012
  while (ancestor) {
@@ -24467,7 +24075,7 @@ const noThisInSfc = defineRule({
24467
24075
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
24468
24076
  context.report({
24469
24077
  node,
24470
- message: MESSAGE$12
24078
+ message: MESSAGE$10
24471
24079
  });
24472
24080
  } };
24473
24081
  }
@@ -24505,39 +24113,26 @@ const noTinyText = defineRule({
24505
24113
  });
24506
24114
  //#endregion
24507
24115
  //#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
24116
  const noTransitionAll = defineRule({
24511
24117
  id: "no-transition-all",
24512
24118
  title: "transition: all animates everything",
24513
24119
  tags: ["test-noise"],
24514
24120
  severity: "warn",
24515
24121
  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
24122
+ create: (context) => ({ JSXAttribute(node) {
24123
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24124
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24125
+ const expression = node.value.expression;
24126
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24127
+ for (const property of expression.properties ?? []) {
24128
+ if (!isNodeOfType(property, "Property")) continue;
24129
+ if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
24130
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
24131
+ node: property,
24132
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24538
24133
  });
24539
24134
  }
24540
- })
24135
+ } })
24541
24136
  });
24542
24137
  //#endregion
24543
24138
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -24581,6 +24176,7 @@ const collectUndefinedInitialStateNames = (componentBody) => {
24581
24176
  }
24582
24177
  return stateNames;
24583
24178
  };
24179
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
24584
24180
  const noUncontrolledInput = defineRule({
24585
24181
  id: "no-uncontrolled-input",
24586
24182
  title: "Uncontrolled input value",
@@ -24684,38 +24280,6 @@ const noUnescapedEntities = defineRule({
24684
24280
  } })
24685
24281
  });
24686
24282
  //#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
24283
  //#region src/plugin/constants/dom-aria-properties.ts
24720
24284
  const ARIA_PROPERTY_NAMES = new Set([
24721
24285
  "activedescendant",
@@ -26187,7 +25751,7 @@ const noWideLetterSpacing = defineRule({
26187
25751
  //#endregion
26188
25752
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
26189
25753
  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.";
25754
+ const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26191
25755
  const resolveSettings$7 = (settings) => {
26192
25756
  const reactDoctor = settings?.["react-doctor"];
26193
25757
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -26221,7 +25785,7 @@ const noWillUpdateSetState = defineRule({
26221
25785
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
26222
25786
  context.report({
26223
25787
  node: node.callee,
26224
- message: MESSAGE$10
25788
+ message: MESSAGE$9
26225
25789
  });
26226
25790
  } };
26227
25791
  }
@@ -27099,7 +26663,7 @@ const preactNoRenderArguments = defineRule({
27099
26663
  });
27100
26664
  //#endregion
27101
26665
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
27102
- const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
26666
+ const MESSAGE$8 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
27103
26667
  const preactPreferOndblclick = defineRule({
27104
26668
  id: "preact-prefer-ondblclick",
27105
26669
  title: "onDoubleClick instead of onDblClick",
@@ -27114,7 +26678,7 @@ const preactPreferOndblclick = defineRule({
27114
26678
  if (!onDoubleClickAttribute) return;
27115
26679
  context.report({
27116
26680
  node: onDoubleClickAttribute,
27117
- message: MESSAGE$9
26681
+ message: MESSAGE$8
27118
26682
  });
27119
26683
  } })
27120
26684
  });
@@ -27154,42 +26718,6 @@ const preactPreferOninput = defineRule({
27154
26718
  } })
27155
26719
  });
27156
26720
  //#endregion
27157
- //#region src/plugin/rules/design/prefer-dvh-over-vh.ts
27158
- const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
27159
- const HEIGHT_KEYS = new Set(["height", "minHeight"]);
27160
- const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
27161
- const preferDvhOverVh = defineRule({
27162
- id: "prefer-dvh-over-vh",
27163
- title: "Use dvh instead of vh for full height",
27164
- tags: ["design", "test-noise"],
27165
- severity: "warn",
27166
- requires: ["tailwind:3.4"],
27167
- recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
27168
- create: (context) => ({
27169
- JSXAttribute(node) {
27170
- const expression = getInlineStyleExpression(node);
27171
- if (!expression) return;
27172
- for (const property of expression.properties ?? []) {
27173
- const key = getStylePropertyKey(property);
27174
- if (!key || !HEIGHT_KEYS.has(key)) continue;
27175
- const value = getStylePropertyStringValue(property);
27176
- if (value && value.trim().toLowerCase() === "100vh") context.report({
27177
- node: property,
27178
- message: MESSAGE$8
27179
- });
27180
- }
27181
- },
27182
- JSXOpeningElement(node) {
27183
- const classNameValue = getStringFromClassNameAttr(node);
27184
- if (!classNameValue) return;
27185
- if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
27186
- node,
27187
- message: MESSAGE$8
27188
- });
27189
- }
27190
- })
27191
- });
27192
- //#endregion
27193
26721
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
27194
26722
  const preferDynamicImport = defineRule({
27195
26723
  id: "prefer-dynamic-import",
@@ -27781,26 +27309,6 @@ const preferTagOverRole = defineRule({
27781
27309
  } })
27782
27310
  });
27783
27311
  //#endregion
27784
- //#region src/plugin/rules/design/prefer-truncate-shorthand.ts
27785
- const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
27786
- const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
27787
- const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
27788
- const preferTruncateShorthand = defineRule({
27789
- id: "prefer-truncate-shorthand",
27790
- title: "Use truncate shorthand",
27791
- tags: ["design", "test-noise"],
27792
- severity: "warn",
27793
- recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
27794
- create: (context) => ({ JSXOpeningElement(node) {
27795
- const classNameValue = getStringFromClassNameAttr(node);
27796
- if (!classNameValue) return;
27797
- if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
27798
- node,
27799
- message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
27800
- });
27801
- } })
27802
- });
27803
- //#endregion
27804
27312
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27805
27313
  const collectFunctionTypedLocalBindings = (componentBody) => {
27806
27314
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -39475,17 +38983,6 @@ const reactDoctorRules = [
39475
38983
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
39476
38984
  }
39477
38985
  },
39478
- {
39479
- key: "react-doctor/no-arbitrary-px-font-size",
39480
- id: "no-arbitrary-px-font-size",
39481
- source: "react-doctor",
39482
- originallyExternal: false,
39483
- rule: {
39484
- ...noArbitraryPxFontSize,
39485
- framework: "global",
39486
- category: "Accessibility"
39487
- }
39488
- },
39489
38986
  {
39490
38987
  key: "react-doctor/no-aria-hidden-on-focusable",
39491
38988
  id: "no-aria-hidden-on-focusable",
@@ -39545,18 +39042,6 @@ const reactDoctorRules = [
39545
39042
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
39546
39043
  }
39547
39044
  },
39548
- {
39549
- key: "react-doctor/no-autoplay-without-muted",
39550
- id: "no-autoplay-without-muted",
39551
- source: "react-doctor",
39552
- originallyExternal: false,
39553
- rule: {
39554
- ...noAutoplayWithoutMuted,
39555
- framework: "global",
39556
- category: "Accessibility",
39557
- requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39558
- }
39559
- },
39560
39045
  {
39561
39046
  key: "react-doctor/no-barrel-import",
39562
39047
  id: "no-barrel-import",
@@ -39710,17 +39195,6 @@ const reactDoctorRules = [
39710
39195
  category: "Maintainability"
39711
39196
  }
39712
39197
  },
39713
- {
39714
- key: "react-doctor/no-deprecated-tailwind-class",
39715
- id: "no-deprecated-tailwind-class",
39716
- source: "react-doctor",
39717
- originallyExternal: false,
39718
- rule: {
39719
- ...noDeprecatedTailwindClass,
39720
- framework: "global",
39721
- category: "Maintainability"
39722
- }
39723
- },
39724
39198
  {
39725
39199
  key: "react-doctor/no-derived-state",
39726
39200
  id: "no-derived-state",
@@ -39992,17 +39466,6 @@ const reactDoctorRules = [
39992
39466
  category: "Performance"
39993
39467
  }
39994
39468
  },
39995
- {
39996
- key: "react-doctor/no-full-viewport-width",
39997
- id: "no-full-viewport-width",
39998
- source: "react-doctor",
39999
- originallyExternal: false,
40000
- rule: {
40001
- ...noFullViewportWidth,
40002
- framework: "global",
40003
- category: "Maintainability"
40004
- }
40005
- },
40006
39469
  {
40007
39470
  key: "react-doctor/no-generic-handler-names",
40008
39471
  id: "no-generic-handler-names",
@@ -40242,17 +39705,6 @@ const reactDoctorRules = [
40242
39705
  category: "Performance"
40243
39706
  }
40244
39707
  },
40245
- {
40246
- key: "react-doctor/no-low-contrast-inline-style",
40247
- id: "no-low-contrast-inline-style",
40248
- source: "react-doctor",
40249
- originallyExternal: false,
40250
- rule: {
40251
- ...noLowContrastInlineStyle,
40252
- framework: "global",
40253
- category: "Accessibility"
40254
- }
40255
- },
40256
39708
  {
40257
39709
  key: "react-doctor/no-many-boolean-props",
40258
39710
  id: "no-many-boolean-props",
@@ -40530,17 +39982,6 @@ const reactDoctorRules = [
40530
39982
  category: "Maintainability"
40531
39983
  }
40532
39984
  },
40533
- {
40534
- key: "react-doctor/no-redundant-display-class",
40535
- id: "no-redundant-display-class",
40536
- source: "react-doctor",
40537
- originallyExternal: false,
40538
- rule: {
40539
- ...noRedundantDisplayClass,
40540
- framework: "global",
40541
- category: "Maintainability"
40542
- }
40543
- },
40544
39985
  {
40545
39986
  key: "react-doctor/no-redundant-roles",
40546
39987
  id: "no-redundant-roles",
@@ -40717,17 +40158,6 @@ const reactDoctorRules = [
40717
40158
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
40718
40159
  }
40719
40160
  },
40720
- {
40721
- key: "react-doctor/no-svg-currentcolor-with-fill-class",
40722
- id: "no-svg-currentcolor-with-fill-class",
40723
- source: "react-doctor",
40724
- originallyExternal: false,
40725
- rule: {
40726
- ...noSvgCurrentcolorWithFillClass,
40727
- framework: "global",
40728
- category: "Maintainability"
40729
- }
40730
- },
40731
40161
  {
40732
40162
  key: "react-doctor/no-sync-xhr",
40733
40163
  id: "no-sync-xhr",
@@ -40739,29 +40169,6 @@ const reactDoctorRules = [
40739
40169
  category: "Performance"
40740
40170
  }
40741
40171
  },
40742
- {
40743
- key: "react-doctor/no-tailwind-layout-transition",
40744
- id: "no-tailwind-layout-transition",
40745
- source: "react-doctor",
40746
- originallyExternal: false,
40747
- rule: {
40748
- ...noTailwindLayoutTransition,
40749
- framework: "global",
40750
- category: "Performance"
40751
- }
40752
- },
40753
- {
40754
- key: "react-doctor/no-target-blank-without-rel",
40755
- id: "no-target-blank-without-rel",
40756
- source: "react-doctor",
40757
- originallyExternal: false,
40758
- rule: {
40759
- ...noTargetBlankWithoutRel,
40760
- framework: "global",
40761
- category: "Accessibility",
40762
- requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40763
- }
40764
- },
40765
40172
  {
40766
40173
  key: "react-doctor/no-this-in-sfc",
40767
40174
  id: "no-this-in-sfc",
@@ -40831,18 +40238,6 @@ const reactDoctorRules = [
40831
40238
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
40832
40239
  }
40833
40240
  },
40834
- {
40835
- key: "react-doctor/no-uninformative-aria-label",
40836
- id: "no-uninformative-aria-label",
40837
- source: "react-doctor",
40838
- originallyExternal: false,
40839
- rule: {
40840
- ...noUninformativeAriaLabel,
40841
- framework: "global",
40842
- category: "Accessibility",
40843
- requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40844
- }
40845
- },
40846
40241
  {
40847
40242
  key: "react-doctor/no-unknown-property",
40848
40243
  id: "no-unknown-property",
@@ -41052,17 +40447,6 @@ const reactDoctorRules = [
41052
40447
  category: "Bugs"
41053
40448
  }
41054
40449
  },
41055
- {
41056
- key: "react-doctor/prefer-dvh-over-vh",
41057
- id: "prefer-dvh-over-vh",
41058
- source: "react-doctor",
41059
- originallyExternal: false,
41060
- rule: {
41061
- ...preferDvhOverVh,
41062
- framework: "global",
41063
- category: "Maintainability"
41064
- }
41065
- },
41066
40450
  {
41067
40451
  key: "react-doctor/prefer-dynamic-import",
41068
40452
  id: "prefer-dynamic-import",
@@ -41167,17 +40551,6 @@ const reactDoctorRules = [
41167
40551
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
41168
40552
  }
41169
40553
  },
41170
- {
41171
- key: "react-doctor/prefer-truncate-shorthand",
41172
- id: "prefer-truncate-shorthand",
41173
- source: "react-doctor",
41174
- originallyExternal: false,
41175
- rule: {
41176
- ...preferTruncateShorthand,
41177
- framework: "global",
41178
- category: "Maintainability"
41179
- }
41180
- },
41181
40554
  {
41182
40555
  key: "react-doctor/prefer-use-effect-event",
41183
40556
  id: "prefer-use-effect-event",