oxlint-plugin-react-doctor 0.5.6-dev.0a7edbd → 0.5.6-dev.424d8f9

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 +635 -5
  2. package/dist/index.js +1036 -179
  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$57 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1918
+ const MESSAGE$64 = "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$57
1934
+ message: MESSAGE$64
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$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2328
+ const MESSAGE$63 = "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$56
2346
+ message: MESSAGE$63
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$56
2353
+ message: MESSAGE$63
2300
2354
  });
2301
2355
  } })
2302
2356
  });
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
3085
3139
  scan: scanByPattern({
3086
3140
  shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
3087
3141
  pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
3088
- requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
3142
+ requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
3089
3143
  message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
3090
3144
  })
3091
3145
  });
@@ -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$55 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4329
+ 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.";
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$55
4363
+ message: MESSAGE$62
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$55
4374
+ message: MESSAGE$62
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$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4751
+ const MESSAGE$61 = "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$54
4783
+ message: MESSAGE$61
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$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4898
+ 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`.";
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$53
5059
+ message: MESSAGE$60
5006
5060
  });
5007
5061
  } };
5008
5062
  }
@@ -5131,6 +5185,7 @@ const dangerousHtmlSink = defineRule({
5131
5185
  return findings;
5132
5186
  }
5133
5187
  });
5188
+ const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5134
5189
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5135
5190
  const VAGUE_BUTTON_LABELS = new Set([
5136
5191
  "continue",
@@ -5429,10 +5484,10 @@ const noVagueButtonLabel = defineRule({
5429
5484
  });
5430
5485
  //#endregion
5431
5486
  //#region src/plugin/utils/has-jsx-spread-attribute.ts
5432
- const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5487
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
5488
  //#endregion
5434
5489
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5435
- const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5490
+ 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.";
5436
5491
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5437
5492
  const NAME_PROVIDING_ATTRIBUTES = [
5438
5493
  "aria-label",
@@ -5451,11 +5506,11 @@ const dialogHasAccessibleName = defineRule({
5451
5506
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5452
5507
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5453
5508
  if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5454
- if (hasJsxSpreadAttribute$1(node.attributes)) return;
5509
+ if (hasJsxSpreadAttribute(node.attributes)) return;
5455
5510
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5456
5511
  context.report({
5457
5512
  node: node.name,
5458
- message: MESSAGE$52
5513
+ message: MESSAGE$59
5459
5514
  });
5460
5515
  } })
5461
5516
  });
@@ -5494,7 +5549,7 @@ const isEs6Component = (node) => {
5494
5549
  };
5495
5550
  //#endregion
5496
5551
  //#region src/plugin/rules/react-builtins/display-name.ts
5497
- const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5552
+ const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5498
5553
  const DEFAULT_ADDITIONAL_HOCS = [
5499
5554
  "observer",
5500
5555
  "lazy",
@@ -5697,7 +5752,7 @@ const displayName = defineRule({
5697
5752
  const reportAt = (node) => {
5698
5753
  context.report({
5699
5754
  node,
5700
- message: MESSAGE$51
5755
+ message: MESSAGE$58
5701
5756
  });
5702
5757
  };
5703
5758
  return {
@@ -7845,7 +7900,7 @@ const forbidElements = defineRule({
7845
7900
  });
7846
7901
  //#endregion
7847
7902
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7848
- const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7903
+ const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7849
7904
  const forwardRefUsesRef = defineRule({
7850
7905
  id: "forward-ref-uses-ref",
7851
7906
  title: "forwardRef without ref parameter",
@@ -7865,7 +7920,7 @@ const forwardRefUsesRef = defineRule({
7865
7920
  if (isNodeOfType(onlyParam, "RestElement")) return;
7866
7921
  context.report({
7867
7922
  node: inner,
7868
- message: MESSAGE$50
7923
+ message: MESSAGE$57
7869
7924
  });
7870
7925
  } })
7871
7926
  });
@@ -7902,7 +7957,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7902
7957
  });
7903
7958
  //#endregion
7904
7959
  //#region src/plugin/rules/a11y/heading-has-content.ts
7905
- const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7960
+ 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`.";
7906
7961
  const DEFAULT_HEADING_TAGS = [
7907
7962
  "h1",
7908
7963
  "h2",
@@ -7935,7 +7990,7 @@ const headingHasContent = defineRule({
7935
7990
  if (isHiddenFromScreenReader(node, context.settings)) return;
7936
7991
  context.report({
7937
7992
  node,
7938
- message: MESSAGE$49
7993
+ message: MESSAGE$56
7939
7994
  });
7940
7995
  } };
7941
7996
  }
@@ -8073,7 +8128,7 @@ const hooksNoNanInDeps = defineRule({
8073
8128
  });
8074
8129
  //#endregion
8075
8130
  //#region src/plugin/rules/a11y/html-has-lang.ts
8076
- const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8131
+ const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8077
8132
  const resolveSettings$38 = (settings) => {
8078
8133
  const reactDoctor = settings?.["react-doctor"];
8079
8134
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8121,7 +8176,7 @@ const htmlHasLang = defineRule({
8121
8176
  if (!lang) {
8122
8177
  context.report({
8123
8178
  node: node.name,
8124
- message: MESSAGE$48
8179
+ message: MESSAGE$55
8125
8180
  });
8126
8181
  return;
8127
8182
  }
@@ -8129,13 +8184,13 @@ const htmlHasLang = defineRule({
8129
8184
  if (verdict === "missing" || verdict === "empty") {
8130
8185
  context.report({
8131
8186
  node: lang,
8132
- message: MESSAGE$48
8187
+ message: MESSAGE$55
8133
8188
  });
8134
8189
  return;
8135
8190
  }
8136
8191
  if (hasSpread && !lang) context.report({
8137
8192
  node: node.name,
8138
- message: MESSAGE$48
8193
+ message: MESSAGE$55
8139
8194
  });
8140
8195
  } };
8141
8196
  }
@@ -8349,7 +8404,7 @@ const htmlNoNestedInteractive = defineRule({
8349
8404
  });
8350
8405
  //#endregion
8351
8406
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8352
- const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8407
+ const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8353
8408
  const evaluateTitleValue = (value) => {
8354
8409
  if (!value) return "missing";
8355
8410
  if (isNodeOfType(value, "Literal")) {
@@ -8389,14 +8444,14 @@ const iframeHasTitle = defineRule({
8389
8444
  if (!titleAttr) {
8390
8445
  if (hasSpread || tag === "iframe") context.report({
8391
8446
  node: node.name,
8392
- message: MESSAGE$47
8447
+ message: MESSAGE$54
8393
8448
  });
8394
8449
  return;
8395
8450
  }
8396
8451
  const verdict = evaluateTitleValue(titleAttr.value);
8397
8452
  if (verdict === "missing" || verdict === "empty") context.report({
8398
8453
  node: titleAttr,
8399
- message: MESSAGE$47
8454
+ message: MESSAGE$54
8400
8455
  });
8401
8456
  } })
8402
8457
  });
@@ -8500,7 +8555,7 @@ const iframeMissingSandbox = defineRule({
8500
8555
  });
8501
8556
  //#endregion
8502
8557
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8503
- const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8558
+ const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8504
8559
  const DEFAULT_COMPONENTS = ["img"];
8505
8560
  const DEFAULT_REDUNDANT_WORDS = [
8506
8561
  "image",
@@ -8565,7 +8620,7 @@ const imgRedundantAlt = defineRule({
8565
8620
  if (!altAttribute) return;
8566
8621
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8567
8622
  node: altAttribute,
8568
- message: MESSAGE$46
8623
+ message: MESSAGE$53
8569
8624
  });
8570
8625
  } };
8571
8626
  }
@@ -10922,7 +10977,7 @@ const jsxMaxDepth = defineRule({
10922
10977
  });
10923
10978
  //#endregion
10924
10979
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10925
- const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10980
+ const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10926
10981
  const LITERAL_TEXT_TAGS = new Set([
10927
10982
  "code",
10928
10983
  "pre",
@@ -10958,7 +11013,7 @@ const jsxNoCommentTextnodes = defineRule({
10958
11013
  if (isInsideLiteralTextTag(node)) return;
10959
11014
  context.report({
10960
11015
  node,
10961
- message: MESSAGE$45
11016
+ message: MESSAGE$52
10962
11017
  });
10963
11018
  } })
10964
11019
  });
@@ -10989,7 +11044,7 @@ const isInsideFunctionScope = (node) => {
10989
11044
  };
10990
11045
  //#endregion
10991
11046
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10992
- const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
11047
+ const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
10993
11048
  const CONTEXT_MODULES$1 = [
10994
11049
  "react",
10995
11050
  "use-context-selector",
@@ -11087,7 +11142,7 @@ const jsxNoConstructedContextValues = defineRule({
11087
11142
  if (!isConstructedValue(innerExpression)) continue;
11088
11143
  context.report({
11089
11144
  node: attribute,
11090
- message: MESSAGE$44
11145
+ message: MESSAGE$51
11091
11146
  });
11092
11147
  }
11093
11148
  }
@@ -11173,7 +11228,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11173
11228
  };
11174
11229
  //#endregion
11175
11230
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11176
- const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
11231
+ const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
11177
11232
  const KNOWN_SLOT_PROP_NAMES = new Set([
11178
11233
  "icon",
11179
11234
  "Icon",
@@ -11442,7 +11497,7 @@ const jsxNoJsxAsProp = defineRule({
11442
11497
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11443
11498
  context.report({
11444
11499
  node,
11445
- message: MESSAGE$43
11500
+ message: MESSAGE$50
11446
11501
  });
11447
11502
  }
11448
11503
  };
@@ -11730,7 +11785,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11730
11785
  ];
11731
11786
  //#endregion
11732
11787
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11733
- const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
11788
+ const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
11734
11789
  const isDataArrayPropName = (propName) => {
11735
11790
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11736
11791
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11814,7 +11869,7 @@ const jsxNoNewArrayAsProp = defineRule({
11814
11869
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11815
11870
  context.report({
11816
11871
  node,
11817
- message: MESSAGE$42
11872
+ message: MESSAGE$49
11818
11873
  });
11819
11874
  }
11820
11875
  };
@@ -12072,7 +12127,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12072
12127
  ]);
12073
12128
  //#endregion
12074
12129
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
12075
- const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
12130
+ const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
12076
12131
  const isAccessorPredicateName = (propName) => {
12077
12132
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12078
12133
  if (propName.length <= prefix.length) continue;
@@ -12278,7 +12333,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12278
12333
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12279
12334
  context.report({
12280
12335
  node,
12281
- message: MESSAGE$41
12336
+ message: MESSAGE$48
12282
12337
  });
12283
12338
  }
12284
12339
  };
@@ -12498,7 +12553,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12498
12553
  ];
12499
12554
  //#endregion
12500
12555
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12501
- const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
12556
+ const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
12502
12557
  const isConfigObjectPropName = (propName) => {
12503
12558
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12504
12559
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12586,7 +12641,7 @@ const jsxNoNewObjectAsProp = defineRule({
12586
12641
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12587
12642
  context.report({
12588
12643
  node,
12589
- message: MESSAGE$40
12644
+ message: MESSAGE$47
12590
12645
  });
12591
12646
  }
12592
12647
  };
@@ -12594,7 +12649,7 @@ const jsxNoNewObjectAsProp = defineRule({
12594
12649
  });
12595
12650
  //#endregion
12596
12651
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12597
- const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12652
+ const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12598
12653
  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;
12599
12654
  const resolveSettings$28 = (settings) => {
12600
12655
  const reactDoctor = settings?.["react-doctor"];
@@ -12635,7 +12690,7 @@ const jsxNoScriptUrl = defineRule({
12635
12690
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12636
12691
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12637
12692
  node: attribute,
12638
- message: MESSAGE$39
12693
+ message: MESSAGE$46
12639
12694
  });
12640
12695
  }
12641
12696
  } };
@@ -12950,7 +13005,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12950
13005
  });
12951
13006
  //#endregion
12952
13007
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12953
- const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
13008
+ const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
12954
13009
  const resolveSettings$25 = (settings) => {
12955
13010
  const reactDoctor = settings?.["react-doctor"];
12956
13011
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12991,7 +13046,7 @@ const jsxPropsNoSpreading = defineRule({
12991
13046
  }
12992
13047
  context.report({
12993
13048
  node: attribute,
12994
- message: MESSAGE$38
13049
+ message: MESSAGE$45
12995
13050
  });
12996
13051
  }
12997
13052
  } };
@@ -13219,7 +13274,7 @@ const labelHasAssociatedControl = defineRule({
13219
13274
  });
13220
13275
  //#endregion
13221
13276
  //#region src/plugin/rules/a11y/lang.ts
13222
- const MESSAGE$37 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13277
+ 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`.";
13223
13278
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13224
13279
  "aa",
13225
13280
  "ab",
@@ -13431,7 +13486,7 @@ const lang = defineRule({
13431
13486
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13432
13487
  context.report({
13433
13488
  node: langAttr,
13434
- message: MESSAGE$37
13489
+ message: MESSAGE$44
13435
13490
  });
13436
13491
  return;
13437
13492
  }
@@ -13440,7 +13495,7 @@ const lang = defineRule({
13440
13495
  if (value === null) return;
13441
13496
  if (!isValidLangTag(value)) context.report({
13442
13497
  node: langAttr,
13443
- message: MESSAGE$37
13498
+ message: MESSAGE$44
13444
13499
  });
13445
13500
  } })
13446
13501
  });
@@ -13466,6 +13521,7 @@ const mcpToolCapabilityRisk = defineRule({
13466
13521
  shouldScan: (file) => isProductionSourcePath(file.relativePath),
13467
13522
  pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
13468
13523
  requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
13524
+ ignoreStringLiterals: true,
13469
13525
  message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
13470
13526
  })
13471
13527
  });
@@ -13484,7 +13540,7 @@ const mdxSsrExecutionRisk = defineRule({
13484
13540
  });
13485
13541
  //#endregion
13486
13542
  //#region src/plugin/rules/a11y/media-has-caption.ts
13487
- const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13543
+ const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13488
13544
  const DEFAULT_AUDIO = ["audio"];
13489
13545
  const DEFAULT_VIDEO = ["video"];
13490
13546
  const DEFAULT_TRACK = ["track"];
@@ -13525,7 +13581,7 @@ const mediaHasCaption = defineRule({
13525
13581
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13526
13582
  context.report({
13527
13583
  node: node.name,
13528
- message: MESSAGE$36
13584
+ message: MESSAGE$43
13529
13585
  });
13530
13586
  return;
13531
13587
  }
@@ -13542,7 +13598,7 @@ const mediaHasCaption = defineRule({
13542
13598
  return kindValue.value.toLowerCase() === "captions";
13543
13599
  })) context.report({
13544
13600
  node: node.name,
13545
- message: MESSAGE$36
13601
+ message: MESSAGE$43
13546
13602
  });
13547
13603
  } };
13548
13604
  }
@@ -15343,7 +15399,7 @@ const nextjsNoVercelOgImport = defineRule({
15343
15399
  });
15344
15400
  //#endregion
15345
15401
  //#region src/plugin/rules/a11y/no-access-key.ts
15346
- const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15402
+ const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15347
15403
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15348
15404
  const noAccessKey = defineRule({
15349
15405
  id: "no-access-key",
@@ -15360,7 +15416,7 @@ const noAccessKey = defineRule({
15360
15416
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15361
15417
  context.report({
15362
15418
  node: accessKey,
15363
- message: MESSAGE$35
15419
+ message: MESSAGE$42
15364
15420
  });
15365
15421
  return;
15366
15422
  }
@@ -15370,7 +15426,7 @@ const noAccessKey = defineRule({
15370
15426
  if (isUndefinedIdentifier(expression)) return;
15371
15427
  context.report({
15372
15428
  node: accessKey,
15373
- message: MESSAGE$35
15429
+ message: MESSAGE$42
15374
15430
  });
15375
15431
  }
15376
15432
  } })
@@ -15852,8 +15908,41 @@ const noAdjustStateOnPropChange = defineRule({
15852
15908
  } })
15853
15909
  });
15854
15910
  //#endregion
15911
+ //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
15912
+ const getStringFromClassNameAttr = (node) => {
15913
+ if (!isNodeOfType(node, "JSXOpeningElement")) return null;
15914
+ const classAttr = findJsxAttribute(node.attributes ?? [], "className");
15915
+ if (!classAttr?.value) return null;
15916
+ if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
15917
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
15918
+ 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;
15919
+ return null;
15920
+ };
15921
+ //#endregion
15922
+ //#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
15923
+ const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
15924
+ const noArbitraryPxFontSize = defineRule({
15925
+ id: "no-arbitrary-px-font-size",
15926
+ title: "Pixel arbitrary font size",
15927
+ tags: ["design", "test-noise"],
15928
+ severity: "warn",
15929
+ category: "Accessibility",
15930
+ 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-*`.",
15931
+ create: (context) => ({ JSXOpeningElement(node) {
15932
+ const classNameValue = getStringFromClassNameAttr(node);
15933
+ if (!classNameValue) return;
15934
+ for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
15935
+ const rem = parseFloat(match[1]) / 16;
15936
+ context.report({
15937
+ node,
15938
+ message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
15939
+ });
15940
+ }
15941
+ } })
15942
+ });
15943
+ //#endregion
15855
15944
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15856
- const MESSAGE$34 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15945
+ 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.";
15857
15946
  const noAriaHiddenOnFocusable = defineRule({
15858
15947
  id: "no-aria-hidden-on-focusable",
15859
15948
  title: "aria-hidden on focusable element",
@@ -15880,7 +15969,7 @@ const noAriaHiddenOnFocusable = defineRule({
15880
15969
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15881
15970
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15882
15971
  node: ariaHidden,
15883
- message: MESSAGE$34
15972
+ message: MESSAGE$41
15884
15973
  });
15885
15974
  } })
15886
15975
  });
@@ -16248,7 +16337,7 @@ const noArrayIndexAsKey = defineRule({
16248
16337
  });
16249
16338
  //#endregion
16250
16339
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16251
- const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
16340
+ const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
16252
16341
  const SECOND_INDEX_METHODS = new Set([
16253
16342
  "every",
16254
16343
  "filter",
@@ -16452,7 +16541,7 @@ const noArrayIndexKey = defineRule({
16452
16541
  }
16453
16542
  context.report({
16454
16543
  node: keyAttribute,
16455
- message: MESSAGE$33
16544
+ message: MESSAGE$40
16456
16545
  });
16457
16546
  },
16458
16547
  CallExpression(node) {
@@ -16472,7 +16561,7 @@ const noArrayIndexKey = defineRule({
16472
16561
  if (propName !== "key") continue;
16473
16562
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16474
16563
  node: property,
16475
- message: MESSAGE$33
16564
+ message: MESSAGE$40
16476
16565
  });
16477
16566
  }
16478
16567
  }
@@ -16480,7 +16569,7 @@ const noArrayIndexKey = defineRule({
16480
16569
  });
16481
16570
  //#endregion
16482
16571
  //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16483
- const MESSAGE$32 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16572
+ 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.";
16484
16573
  const noAsyncEffectCallback = defineRule({
16485
16574
  id: "no-async-effect-callback",
16486
16575
  title: "Async effect callback",
@@ -16494,13 +16583,13 @@ const noAsyncEffectCallback = defineRule({
16494
16583
  if (!callback.async) return;
16495
16584
  context.report({
16496
16585
  node: callback,
16497
- message: MESSAGE$32
16586
+ message: MESSAGE$39
16498
16587
  });
16499
16588
  } })
16500
16589
  });
16501
16590
  //#endregion
16502
16591
  //#region src/plugin/rules/a11y/no-autofocus.ts
16503
- const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16592
+ 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.";
16504
16593
  const resolveSettings$21 = (settings) => {
16505
16594
  const reactDoctor = settings?.["react-doctor"];
16506
16595
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16556,12 +16645,45 @@ const noAutofocus = defineRule({
16556
16645
  }
16557
16646
  context.report({
16558
16647
  node: autoFocusAttribute,
16559
- message: MESSAGE$31
16648
+ message: MESSAGE$38
16560
16649
  });
16561
16650
  } };
16562
16651
  }
16563
16652
  });
16564
16653
  //#endregion
16654
+ //#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
16655
+ 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`.";
16656
+ const resolveStaticBoolean = (attribute) => {
16657
+ const value = attribute.value;
16658
+ if (!value) return true;
16659
+ const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
16660
+ if (isNodeOfType(literal, "Literal")) {
16661
+ if (literal.value === true || literal.value === "true") return true;
16662
+ if (literal.value === false || literal.value === "false") return false;
16663
+ }
16664
+ return null;
16665
+ };
16666
+ const noAutoplayWithoutMuted = defineRule({
16667
+ id: "no-autoplay-without-muted",
16668
+ title: "Autoplaying media without muted",
16669
+ severity: "warn",
16670
+ recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
16671
+ create: (context) => ({ JSXOpeningElement(node) {
16672
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
16673
+ const tagName = node.name.name;
16674
+ if (tagName !== "video" && tagName !== "audio") return;
16675
+ if (hasJsxSpreadAttribute(node.attributes)) return;
16676
+ const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
16677
+ if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
16678
+ const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
16679
+ if (muted && resolveStaticBoolean(muted) !== false) return;
16680
+ context.report({
16681
+ node: node.name,
16682
+ message: MESSAGE$37
16683
+ });
16684
+ } })
16685
+ });
16686
+ //#endregion
16565
16687
  //#region src/plugin/utils/create-relative-import-source.ts
16566
16688
  const createRelativeImportSource = (filename, targetFilePath) => {
16567
16689
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -17060,7 +17182,7 @@ const noChainStateUpdates = defineRule({
17060
17182
  });
17061
17183
  //#endregion
17062
17184
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
17063
- const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17185
+ const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17064
17186
  const noChildrenProp = defineRule({
17065
17187
  id: "no-children-prop",
17066
17188
  title: "Children passed as a prop",
@@ -17072,7 +17194,7 @@ const noChildrenProp = defineRule({
17072
17194
  if (node.name.name !== "children") return;
17073
17195
  context.report({
17074
17196
  node: node.name,
17075
- message: MESSAGE$30
17197
+ message: MESSAGE$36
17076
17198
  });
17077
17199
  },
17078
17200
  CallExpression(node) {
@@ -17085,7 +17207,7 @@ const noChildrenProp = defineRule({
17085
17207
  const propertyKey = property.key;
17086
17208
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17087
17209
  node: propertyKey,
17088
- message: MESSAGE$30
17210
+ message: MESSAGE$36
17089
17211
  });
17090
17212
  }
17091
17213
  }
@@ -17093,7 +17215,7 @@ const noChildrenProp = defineRule({
17093
17215
  });
17094
17216
  //#endregion
17095
17217
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
17096
- const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17218
+ const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17097
17219
  const noCloneElement = defineRule({
17098
17220
  id: "no-clone-element",
17099
17221
  title: "cloneElement makes child props fragile",
@@ -17106,7 +17228,7 @@ const noCloneElement = defineRule({
17106
17228
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17107
17229
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17108
17230
  node: callee,
17109
- message: MESSAGE$29
17231
+ message: MESSAGE$35
17110
17232
  });
17111
17233
  return;
17112
17234
  }
@@ -17119,7 +17241,7 @@ const noCloneElement = defineRule({
17119
17241
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17120
17242
  context.report({
17121
17243
  node: callee,
17122
- message: MESSAGE$29
17244
+ message: MESSAGE$35
17123
17245
  });
17124
17246
  }
17125
17247
  } })
@@ -17168,7 +17290,7 @@ const enclosingComponentOrHookName = (node) => {
17168
17290
  };
17169
17291
  //#endregion
17170
17292
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
17171
- const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17293
+ const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17172
17294
  const CONTEXT_MODULES = [
17173
17295
  "react",
17174
17296
  "use-context-selector",
@@ -17204,13 +17326,13 @@ const noCreateContextInRender = defineRule({
17204
17326
  if (!componentOrHookName) return;
17205
17327
  context.report({
17206
17328
  node,
17207
- message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
17329
+ message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17208
17330
  });
17209
17331
  } })
17210
17332
  });
17211
17333
  //#endregion
17212
17334
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17213
- const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17335
+ 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.";
17214
17336
  const noCreateRefInFunctionComponent = defineRule({
17215
17337
  id: "no-create-ref-in-function-component",
17216
17338
  title: "createRef in function component",
@@ -17229,7 +17351,7 @@ const noCreateRefInFunctionComponent = defineRule({
17229
17351
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17230
17352
  context.report({
17231
17353
  node,
17232
- message: MESSAGE$27
17354
+ message: MESSAGE$33
17233
17355
  });
17234
17356
  } })
17235
17357
  });
@@ -17369,7 +17491,7 @@ const noCreateStoreInRender = defineRule({
17369
17491
  });
17370
17492
  //#endregion
17371
17493
  //#region src/plugin/rules/react-builtins/no-danger.ts
17372
- const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17494
+ const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17373
17495
  const noDanger = defineRule({
17374
17496
  id: "no-danger",
17375
17497
  title: "Raw HTML injection can run unsafe markup",
@@ -17382,7 +17504,7 @@ const noDanger = defineRule({
17382
17504
  if (!propAttribute) return;
17383
17505
  context.report({
17384
17506
  node: propAttribute.name,
17385
- message: MESSAGE$26
17507
+ message: MESSAGE$32
17386
17508
  });
17387
17509
  },
17388
17510
  CallExpression(node) {
@@ -17394,7 +17516,7 @@ const noDanger = defineRule({
17394
17516
  const propertyKey = property.key;
17395
17517
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17396
17518
  node: propertyKey,
17397
- message: MESSAGE$26
17519
+ message: MESSAGE$32
17398
17520
  });
17399
17521
  }
17400
17522
  }
@@ -17402,7 +17524,7 @@ const noDanger = defineRule({
17402
17524
  });
17403
17525
  //#endregion
17404
17526
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17405
- const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17527
+ const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17406
17528
  const isLineBreak = (child) => {
17407
17529
  if (!isNodeOfType(child, "JSXText")) return false;
17408
17530
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17472,7 +17594,7 @@ const noDangerWithChildren = defineRule({
17472
17594
  if (!hasChildrenProp && !hasNestedChildren) return;
17473
17595
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17474
17596
  node: opening,
17475
- message: MESSAGE$25
17597
+ message: MESSAGE$31
17476
17598
  });
17477
17599
  },
17478
17600
  CallExpression(node) {
@@ -17484,7 +17606,7 @@ const noDangerWithChildren = defineRule({
17484
17606
  if (!propsShape.hasDangerously) return;
17485
17607
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17486
17608
  node,
17487
- message: MESSAGE$25
17609
+ message: MESSAGE$31
17488
17610
  });
17489
17611
  }
17490
17612
  })
@@ -17649,6 +17771,37 @@ const noDefaultProps = defineRule({
17649
17771
  } })
17650
17772
  });
17651
17773
  //#endregion
17774
+ //#region src/plugin/utils/get-class-name-tokens.ts
17775
+ const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
17776
+ //#endregion
17777
+ //#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
17778
+ const renameDeprecatedToken = (token) => {
17779
+ if (token === "overflow-ellipsis") return "text-ellipsis";
17780
+ if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
17781
+ if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
17782
+ if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
17783
+ return null;
17784
+ };
17785
+ const noDeprecatedTailwindClass = defineRule({
17786
+ id: "no-deprecated-tailwind-class",
17787
+ title: "Deprecated Tailwind v4 utility",
17788
+ tags: ["design", "test-noise"],
17789
+ severity: "warn",
17790
+ requires: ["tailwind:4"],
17791
+ recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
17792
+ create: (context) => ({ JSXOpeningElement(node) {
17793
+ const classNameValue = getStringFromClassNameAttr(node);
17794
+ if (!classNameValue) return;
17795
+ for (const token of getClassNameTokens(classNameValue)) {
17796
+ const replacement = renameDeprecatedToken(token);
17797
+ if (replacement) context.report({
17798
+ node,
17799
+ message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
17800
+ });
17801
+ }
17802
+ } })
17803
+ });
17804
+ //#endregion
17652
17805
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17653
17806
  const isInitialOnlyPropName = (propName) => {
17654
17807
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -18061,7 +18214,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
18061
18214
  //#endregion
18062
18215
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
18063
18216
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
18064
- const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18217
+ const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18065
18218
  const resolveSettings$20 = (settings) => {
18066
18219
  const reactDoctor = settings?.["react-doctor"];
18067
18220
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18080,7 +18233,7 @@ const noDidMountSetState = defineRule({
18080
18233
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18081
18234
  context.report({
18082
18235
  node: node.callee,
18083
- message: MESSAGE$24
18236
+ message: MESSAGE$30
18084
18237
  });
18085
18238
  } };
18086
18239
  }
@@ -18088,7 +18241,7 @@ const noDidMountSetState = defineRule({
18088
18241
  //#endregion
18089
18242
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18090
18243
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
18091
- const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18244
+ const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18092
18245
  const resolveSettings$19 = (settings) => {
18093
18246
  const reactDoctor = settings?.["react-doctor"];
18094
18247
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18107,7 +18260,7 @@ const noDidUpdateSetState = defineRule({
18107
18260
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18108
18261
  context.report({
18109
18262
  node: node.callee,
18110
- message: MESSAGE$23
18263
+ message: MESSAGE$29
18111
18264
  });
18112
18265
  } };
18113
18266
  }
@@ -18130,7 +18283,7 @@ const isStateMemberExpression = (node) => {
18130
18283
  };
18131
18284
  //#endregion
18132
18285
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
18133
- const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18286
+ const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18134
18287
  const shouldIgnoreMutation = (node) => {
18135
18288
  let isConstructor = false;
18136
18289
  let isInsideCallExpression = false;
@@ -18152,7 +18305,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18152
18305
  if (shouldIgnoreMutation(reportNode)) return;
18153
18306
  context.report({
18154
18307
  node: reportNode,
18155
- message: MESSAGE$22
18308
+ message: MESSAGE$28
18156
18309
  });
18157
18310
  };
18158
18311
  const noDirectMutationState = defineRule({
@@ -18362,6 +18515,26 @@ const noDocumentStartViewTransition = defineRule({
18362
18515
  } })
18363
18516
  });
18364
18517
  //#endregion
18518
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18519
+ 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.";
18520
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18521
+ const noDocumentWrite = defineRule({
18522
+ id: "no-document-write",
18523
+ title: "document.write/writeln",
18524
+ severity: "warn",
18525
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18526
+ create: (context) => ({ CallExpression(node) {
18527
+ const callee = node.callee;
18528
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18529
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18530
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18531
+ context.report({
18532
+ node,
18533
+ message: MESSAGE$27
18534
+ });
18535
+ } })
18536
+ });
18537
+ //#endregion
18365
18538
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18366
18539
  const noDynamicImportPath = defineRule({
18367
18540
  id: "no-dynamic-import-path",
@@ -19740,7 +19913,7 @@ const ALLOWED_NAMESPACES = new Set([
19740
19913
  "ReactDOM",
19741
19914
  "ReactDom"
19742
19915
  ]);
19743
- const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19916
+ const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19744
19917
  const noFindDomNode = defineRule({
19745
19918
  id: "no-find-dom-node",
19746
19919
  title: "findDOMNode breaks component encapsulation",
@@ -19751,7 +19924,7 @@ const noFindDomNode = defineRule({
19751
19924
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19752
19925
  context.report({
19753
19926
  node: callee,
19754
- message: MESSAGE$21
19927
+ message: MESSAGE$26
19755
19928
  });
19756
19929
  return;
19757
19930
  }
@@ -19762,7 +19935,7 @@ const noFindDomNode = defineRule({
19762
19935
  if (callee.property.name !== "findDOMNode") return;
19763
19936
  context.report({
19764
19937
  node: callee.property,
19765
- message: MESSAGE$21
19938
+ message: MESSAGE$26
19766
19939
  });
19767
19940
  }
19768
19941
  } })
@@ -19803,6 +19976,41 @@ const noFullLodashImport = defineRule({
19803
19976
  } })
19804
19977
  });
19805
19978
  //#endregion
19979
+ //#region src/plugin/rules/design/no-full-viewport-width.ts
19980
+ const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
19981
+ const WIDTH_KEYS = new Set(["width", "minWidth"]);
19982
+ 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.";
19983
+ const noFullViewportWidth = defineRule({
19984
+ id: "no-full-viewport-width",
19985
+ title: "Full viewport width causes overflow",
19986
+ tags: ["design", "test-noise"],
19987
+ severity: "warn",
19988
+ recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
19989
+ create: (context) => ({
19990
+ JSXAttribute(node) {
19991
+ const expression = getInlineStyleExpression(node);
19992
+ if (!expression) return;
19993
+ for (const property of expression.properties ?? []) {
19994
+ const key = getStylePropertyKey(property);
19995
+ if (!key || !WIDTH_KEYS.has(key)) continue;
19996
+ const value = getStylePropertyStringValue(property);
19997
+ if (value && value.trim().toLowerCase() === "100vw") context.report({
19998
+ node: property,
19999
+ message: MESSAGE$25
20000
+ });
20001
+ }
20002
+ },
20003
+ JSXOpeningElement(node) {
20004
+ const classNameValue = getStringFromClassNameAttr(node);
20005
+ if (!classNameValue) return;
20006
+ if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
20007
+ node,
20008
+ message: MESSAGE$25
20009
+ });
20010
+ }
20011
+ })
20012
+ });
20013
+ //#endregion
19806
20014
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19807
20015
  const noGenericHandlerNames = defineRule({
19808
20016
  id: "no-generic-handler-names",
@@ -19865,7 +20073,7 @@ const noGiantComponent = defineRule({
19865
20073
  });
19866
20074
  //#endregion
19867
20075
  //#region src/plugin/constants/style.ts
19868
- const LAYOUT_PROPERTIES = new Set([
20076
+ const LAYOUT_PROPERTIES$1 = new Set([
19869
20077
  "width",
19870
20078
  "height",
19871
20079
  "top",
@@ -19935,17 +20143,6 @@ const noGlobalCssVariableAnimation = defineRule({
19935
20143
  } })
19936
20144
  });
19937
20145
  //#endregion
19938
- //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
19939
- const getStringFromClassNameAttr = (node) => {
19940
- if (!isNodeOfType(node, "JSXOpeningElement")) return null;
19941
- const classAttr = findJsxAttribute(node.attributes ?? [], "className");
19942
- if (!classAttr?.value) return null;
19943
- if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
19944
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
19945
- 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;
19946
- return null;
19947
- };
19948
- //#endregion
19949
20146
  //#region src/plugin/rules/design/no-gradient-text.ts
19950
20147
  const noGradientText = defineRule({
19951
20148
  id: "no-gradient-text",
@@ -20004,7 +20201,7 @@ const noGrayOnColoredBackground = defineRule({
20004
20201
  });
20005
20202
  //#endregion
20006
20203
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20007
- const MESSAGE$20 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20204
+ 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.";
20008
20205
  const noImgLazyWithHighFetchpriority = defineRule({
20009
20206
  id: "no-img-lazy-with-high-fetchpriority",
20010
20207
  title: "Lazy image with high fetchPriority",
@@ -20018,7 +20215,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
20018
20215
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20019
20216
  context.report({
20020
20217
  node: node.name,
20021
- message: MESSAGE$20
20218
+ message: MESSAGE$24
20022
20219
  });
20023
20220
  } })
20024
20221
  });
@@ -20253,7 +20450,7 @@ const noIsMounted = defineRule({
20253
20450
  });
20254
20451
  //#endregion
20255
20452
  //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20256
- const MESSAGE$19 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20453
+ 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)`.";
20257
20454
  const isJsonMethodCall = (node, method) => {
20258
20455
  if (!isNodeOfType(node, "CallExpression")) return false;
20259
20456
  const callee = node.callee;
@@ -20270,13 +20467,13 @@ const noJsonParseStringifyClone = defineRule({
20270
20467
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20271
20468
  context.report({
20272
20469
  node,
20273
- message: MESSAGE$19
20470
+ message: MESSAGE$23
20274
20471
  });
20275
20472
  } })
20276
20473
  });
20277
20474
  //#endregion
20278
20475
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
20279
- const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20476
+ const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20280
20477
  const isJsxElementTypeReference = (node) => {
20281
20478
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20282
20479
  const typeName = node.typeName;
@@ -20293,7 +20490,7 @@ const checkReturnType = (context, returnType) => {
20293
20490
  if (!typeAnnotation) return;
20294
20491
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20295
20492
  node: typeAnnotation,
20296
- message: MESSAGE$18
20493
+ message: MESSAGE$22
20297
20494
  });
20298
20495
  };
20299
20496
  const noJsxElementType = defineRule({
@@ -20403,7 +20600,7 @@ const noLayoutPropertyAnimation = defineRule({
20403
20600
  let propertyName = null;
20404
20601
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20405
20602
  else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
20406
- if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20603
+ if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
20407
20604
  node: property,
20408
20605
  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`
20409
20606
  });
@@ -20593,6 +20790,134 @@ const noLongTransitionDuration = defineRule({
20593
20790
  } })
20594
20791
  });
20595
20792
  //#endregion
20793
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
20794
+ const getStylePropertyNumberValue = (property) => {
20795
+ if (!isNodeOfType(property, "Property")) return null;
20796
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
20797
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
20798
+ return null;
20799
+ };
20800
+ //#endregion
20801
+ //#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
20802
+ const linearizeChannel = (channel) => {
20803
+ const normalized = channel / 255;
20804
+ return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
20805
+ };
20806
+ const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
20807
+ const getWcagContrastRatio = (foreground, background) => {
20808
+ const foregroundLuminance = relativeLuminance(foreground);
20809
+ const backgroundLuminance = relativeLuminance(background);
20810
+ const lighter = Math.max(foregroundLuminance, backgroundLuminance);
20811
+ const darker = Math.min(foregroundLuminance, backgroundLuminance);
20812
+ return (lighter + .05) / (darker + .05);
20813
+ };
20814
+ //#endregion
20815
+ //#region src/plugin/rules/design/no-low-contrast-inline-style.ts
20816
+ const UNRESOLVABLE = new Set([
20817
+ "transparent",
20818
+ "currentcolor",
20819
+ "inherit",
20820
+ "initial",
20821
+ "unset",
20822
+ "revert",
20823
+ "none"
20824
+ ]);
20825
+ const resolveOpaqueColor = (raw) => {
20826
+ const value = raw.trim().toLowerCase();
20827
+ if (UNRESOLVABLE.has(value)) return null;
20828
+ if (value === "white") return {
20829
+ red: 255,
20830
+ green: 255,
20831
+ blue: 255
20832
+ };
20833
+ if (value === "black") return {
20834
+ red: 0,
20835
+ green: 0,
20836
+ blue: 0
20837
+ };
20838
+ if (value.startsWith("var(")) return null;
20839
+ if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
20840
+ if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
20841
+ if (value.startsWith("rgb")) {
20842
+ const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
20843
+ if (inner.includes("/") || inner.split(",").length >= 4) return null;
20844
+ }
20845
+ return parseColorToRgb(value);
20846
+ };
20847
+ const toPx = (property) => {
20848
+ const numberValue = getStylePropertyNumberValue(property);
20849
+ if (numberValue !== null) return numberValue;
20850
+ const stringValue = getStylePropertyStringValue(property);
20851
+ if (stringValue === null) return null;
20852
+ const pxMatch = stringValue.match(/^([\d.]+)px$/);
20853
+ if (pxMatch) return parseFloat(pxMatch[1]);
20854
+ const remMatch = stringValue.match(/^([\d.]+)rem$/);
20855
+ if (remMatch) return parseFloat(remMatch[1]) * 16;
20856
+ return null;
20857
+ };
20858
+ const isBoldWeight = (property) => {
20859
+ const numberValue = getStylePropertyNumberValue(property);
20860
+ if (numberValue !== null) return numberValue >= 700;
20861
+ const stringValue = getStylePropertyStringValue(property);
20862
+ if (stringValue === null) return false;
20863
+ if (stringValue === "bold" || stringValue === "bolder") return true;
20864
+ const numericWeight = Number(stringValue);
20865
+ return Number.isFinite(numericWeight) && numericWeight >= 700;
20866
+ };
20867
+ const noLowContrastInlineStyle = defineRule({
20868
+ id: "no-low-contrast-inline-style",
20869
+ title: "Low-contrast text in inline style",
20870
+ tags: ["test-noise"],
20871
+ severity: "warn",
20872
+ category: "Accessibility",
20873
+ 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.",
20874
+ create: (context) => ({ JSXAttribute(node) {
20875
+ const expression = getInlineStyleExpression(node);
20876
+ if (!expression) return;
20877
+ const properties = expression.properties ?? [];
20878
+ if (properties.some((property) => property.type === "SpreadElement")) return;
20879
+ let foreground = null;
20880
+ let backgroundColorRaw = null;
20881
+ let backgroundShorthandRaw = null;
20882
+ let backgroundIsUnknown = false;
20883
+ let fontSizePx = null;
20884
+ let isBold = false;
20885
+ for (const property of properties) {
20886
+ const key = getStylePropertyKey(property);
20887
+ if (!key) continue;
20888
+ if (key === "backgroundImage") {
20889
+ backgroundIsUnknown = true;
20890
+ continue;
20891
+ }
20892
+ if (key === "fontSize" && property.type === "Property") {
20893
+ fontSizePx = toPx(property);
20894
+ continue;
20895
+ }
20896
+ if (key === "fontWeight" && property.type === "Property") {
20897
+ isBold = isBoldWeight(property);
20898
+ continue;
20899
+ }
20900
+ const stringValue = getStylePropertyStringValue(property);
20901
+ if (key === "color") {
20902
+ if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
20903
+ } else if (key === "backgroundColor") backgroundColorRaw = stringValue;
20904
+ else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
20905
+ else backgroundShorthandRaw = stringValue;
20906
+ }
20907
+ if (backgroundIsUnknown) return;
20908
+ if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
20909
+ const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
20910
+ const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
20911
+ if (!foreground || !background) return;
20912
+ const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
20913
+ const ratio = getWcagContrastRatio(foreground, background);
20914
+ if (ratio < threshold) context.report({
20915
+ node,
20916
+ 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.`
20917
+ });
20918
+ } })
20919
+ });
20920
+ //#endregion
20596
20921
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20597
20922
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20598
20923
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -20748,7 +21073,7 @@ const noMoment = defineRule({
20748
21073
  });
20749
21074
  //#endregion
20750
21075
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20751
- const MESSAGE$17 = "This file declares several components, so each component is harder to find, test, and change.";
21076
+ const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
20752
21077
  const resolveSettings$16 = (settings) => {
20753
21078
  const reactDoctor = settings?.["react-doctor"];
20754
21079
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21070,7 +21395,7 @@ const noMultiComp = defineRule({
21070
21395
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21071
21396
  for (const component of flagged.slice(1)) context.report({
21072
21397
  node: component.reportNode,
21073
- message: MESSAGE$17
21398
+ message: MESSAGE$21
21074
21399
  });
21075
21400
  } };
21076
21401
  }
@@ -21238,7 +21563,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21238
21563
  };
21239
21564
  //#endregion
21240
21565
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
21241
- const MESSAGE$16 = "This reducer changes state in place, so your update is silently skipped.";
21566
+ const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
21242
21567
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21243
21568
  "copyWithin",
21244
21569
  "fill",
@@ -21448,7 +21773,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21448
21773
  reportedNodes.add(options.crossFileConsumerCallSite);
21449
21774
  context.report({
21450
21775
  node: options.crossFileConsumerCallSite,
21451
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21776
+ message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21452
21777
  });
21453
21778
  return;
21454
21779
  }
@@ -21457,7 +21782,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21457
21782
  reportedNodes.add(mutation.node);
21458
21783
  context.report({
21459
21784
  node: mutation.node,
21460
- message: MESSAGE$16
21785
+ message: MESSAGE$20
21461
21786
  });
21462
21787
  }
21463
21788
  };
@@ -21729,7 +22054,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21729
22054
  });
21730
22055
  //#endregion
21731
22056
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21732
- const MESSAGE$15 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22057
+ const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21733
22058
  const resolveSettings$14 = (settings) => {
21734
22059
  const reactDoctor = settings?.["react-doctor"];
21735
22060
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21757,7 +22082,7 @@ const noNoninteractiveTabindex = defineRule({
21757
22082
  if (numeric === null) {
21758
22083
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21759
22084
  node: tabIndex,
21760
- message: MESSAGE$15
22085
+ message: MESSAGE$19
21761
22086
  });
21762
22087
  return;
21763
22088
  }
@@ -21770,7 +22095,7 @@ const noNoninteractiveTabindex = defineRule({
21770
22095
  if (!roleAttribute) {
21771
22096
  context.report({
21772
22097
  node: tabIndex,
21773
- message: MESSAGE$15
22098
+ message: MESSAGE$19
21774
22099
  });
21775
22100
  return;
21776
22101
  }
@@ -21784,20 +22109,12 @@ const noNoninteractiveTabindex = defineRule({
21784
22109
  }
21785
22110
  context.report({
21786
22111
  node: tabIndex,
21787
- message: MESSAGE$15
22112
+ message: MESSAGE$19
21788
22113
  });
21789
22114
  } };
21790
22115
  }
21791
22116
  });
21792
22117
  //#endregion
21793
- //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21794
- const getStylePropertyNumberValue = (property) => {
21795
- if (!isNodeOfType(property, "Property")) return null;
21796
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21797
- if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21798
- return null;
21799
- };
21800
- //#endregion
21801
22118
  //#region src/plugin/rules/design/no-outline-none.ts
21802
22119
  const noOutlineNone = defineRule({
21803
22120
  id: "no-outline-none",
@@ -22475,7 +22792,7 @@ const noRandomKey = defineRule({
22475
22792
  });
22476
22793
  //#endregion
22477
22794
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22478
- const MESSAGE$14 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22795
+ const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22479
22796
  const isChildrenIdentifier = (node, contextNode) => {
22480
22797
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22481
22798
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22501,13 +22818,13 @@ const noReactChildren = defineRule({
22501
22818
  if (isChildrenIdentifier(memberObject, node)) {
22502
22819
  context.report({
22503
22820
  node: calleeOuter,
22504
- message: MESSAGE$14
22821
+ message: MESSAGE$18
22505
22822
  });
22506
22823
  return;
22507
22824
  }
22508
22825
  if (isReactNamespaceMember(memberObject, node)) context.report({
22509
22826
  node: calleeOuter,
22510
- message: MESSAGE$14
22827
+ message: MESSAGE$18
22511
22828
  });
22512
22829
  } })
22513
22830
  });
@@ -22618,6 +22935,86 @@ const noReact19DeprecatedApis = defineRule({
22618
22935
  })
22619
22936
  });
22620
22937
  //#endregion
22938
+ //#region src/plugin/rules/design/no-redundant-display-class.ts
22939
+ const BLOCK_DEFAULT_TAGS = new Set([
22940
+ "div",
22941
+ "p",
22942
+ "section",
22943
+ "article",
22944
+ "main",
22945
+ "header",
22946
+ "footer",
22947
+ "nav",
22948
+ "aside",
22949
+ "figure",
22950
+ "figcaption",
22951
+ "blockquote",
22952
+ "form",
22953
+ "fieldset",
22954
+ "address",
22955
+ "pre",
22956
+ "ul",
22957
+ "ol",
22958
+ "dl",
22959
+ "dt",
22960
+ "dd",
22961
+ "h1",
22962
+ "h2",
22963
+ "h3",
22964
+ "h4",
22965
+ "h5",
22966
+ "h6"
22967
+ ]);
22968
+ const INLINE_DEFAULT_TAGS = new Set([
22969
+ "span",
22970
+ "a",
22971
+ "b",
22972
+ "i",
22973
+ "em",
22974
+ "strong",
22975
+ "small",
22976
+ "code",
22977
+ "abbr",
22978
+ "cite",
22979
+ "label",
22980
+ "mark",
22981
+ "q",
22982
+ "s",
22983
+ "u",
22984
+ "sub",
22985
+ "sup",
22986
+ "kbd",
22987
+ "samp",
22988
+ "var",
22989
+ "time"
22990
+ ]);
22991
+ const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
22992
+ const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
22993
+ const noRedundantDisplayClass = defineRule({
22994
+ id: "no-redundant-display-class",
22995
+ title: "Redundant display utility",
22996
+ tags: ["design", "test-noise"],
22997
+ severity: "warn",
22998
+ 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`.",
22999
+ create: (context) => ({ JSXOpeningElement(node) {
23000
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
23001
+ const tagName = node.name.name;
23002
+ const classNameValue = getStringFromClassNameAttr(node);
23003
+ if (!classNameValue) return;
23004
+ if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
23005
+ context.report({
23006
+ node,
23007
+ message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
23008
+ });
23009
+ return;
23010
+ }
23011
+ if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
23012
+ node,
23013
+ message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
23014
+ });
23015
+ } })
23016
+ });
23017
+ //#endregion
22621
23018
  //#region src/plugin/constants/aria-element-roles.ts
22622
23019
  const ELEMENT_ROLE_PAIRS = [
22623
23020
  ["a", "link"],
@@ -22830,7 +23227,7 @@ const noRenderPropChildren = defineRule({
22830
23227
  });
22831
23228
  //#endregion
22832
23229
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22833
- const MESSAGE$13 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23230
+ const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22834
23231
  const isReactDomRenderCall = (node) => {
22835
23232
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22836
23233
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22854,7 +23251,7 @@ const noRenderReturnValue = defineRule({
22854
23251
  if (!isUsedAsReturnValue(node.parent)) return;
22855
23252
  context.report({
22856
23253
  node: node.callee,
22857
- message: MESSAGE$13
23254
+ message: MESSAGE$17
22858
23255
  });
22859
23256
  } })
22860
23257
  });
@@ -23552,7 +23949,7 @@ const getParentComponent = (node) => {
23552
23949
  };
23553
23950
  //#endregion
23554
23951
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23555
- const MESSAGE$12 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23952
+ const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23556
23953
  const noSetState = defineRule({
23557
23954
  id: "no-set-state",
23558
23955
  title: "Local class state forbidden",
@@ -23567,7 +23964,7 @@ const noSetState = defineRule({
23567
23964
  if (!getParentComponent(node)) return;
23568
23965
  context.report({
23569
23966
  node: node.callee,
23570
- message: MESSAGE$12
23967
+ message: MESSAGE$16
23571
23968
  });
23572
23969
  } })
23573
23970
  });
@@ -23729,7 +24126,7 @@ const isAbstractRole = (openingElement, settings) => {
23729
24126
  };
23730
24127
  //#endregion
23731
24128
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23732
- const MESSAGE$11 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
24129
+ 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.";
23733
24130
  const DEFAULT_HANDLERS = [
23734
24131
  "onClick",
23735
24132
  "onMouseDown",
@@ -23789,7 +24186,7 @@ const noStaticElementInteractions = defineRule({
23789
24186
  if (!roleAttribute || !roleAttribute.value) {
23790
24187
  context.report({
23791
24188
  node: node.name,
23792
- message: MESSAGE$11
24189
+ message: MESSAGE$15
23793
24190
  });
23794
24191
  return;
23795
24192
  }
@@ -23799,19 +24196,66 @@ const noStaticElementInteractions = defineRule({
23799
24196
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23800
24197
  context.report({
23801
24198
  node: node.name,
23802
- message: MESSAGE$11
24199
+ message: MESSAGE$15
23803
24200
  });
23804
24201
  return;
23805
24202
  }
23806
24203
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23807
24204
  context.report({
23808
24205
  node: node.name,
23809
- message: MESSAGE$11
24206
+ message: MESSAGE$15
23810
24207
  });
23811
24208
  } };
23812
24209
  }
23813
24210
  });
23814
24211
  //#endregion
24212
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
24213
+ const BOOLEAN_ATTRIBUTES = new Set([
24214
+ "disabled",
24215
+ "checked",
24216
+ "readonly",
24217
+ "required",
24218
+ "selected",
24219
+ "multiple",
24220
+ "autofocus",
24221
+ "autoplay",
24222
+ "controls",
24223
+ "loop",
24224
+ "muted",
24225
+ "open",
24226
+ "reversed",
24227
+ "default",
24228
+ "novalidate",
24229
+ "formnovalidate",
24230
+ "playsinline",
24231
+ "itemscope",
24232
+ "allowfullscreen"
24233
+ ]);
24234
+ const noStringFalseOnBooleanAttribute = defineRule({
24235
+ id: "no-string-false-on-boolean-attribute",
24236
+ title: "String true/false on a boolean attribute",
24237
+ severity: "warn",
24238
+ recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
24239
+ create: (context) => ({ JSXOpeningElement(node) {
24240
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24241
+ const firstCharacter = node.name.name.charCodeAt(0);
24242
+ if (firstCharacter < 97 || firstCharacter > 122) return;
24243
+ for (const attribute of node.attributes) {
24244
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24245
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
24246
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
24247
+ const value = getJsxPropStringValue(attribute);
24248
+ if (value !== "false" && value !== "true") continue;
24249
+ const attributeName = attribute.name.name;
24250
+ const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
24251
+ context.report({
24252
+ node: attribute,
24253
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
24254
+ });
24255
+ }
24256
+ } })
24257
+ });
24258
+ //#endregion
23815
24259
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23816
24260
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23817
24261
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23862,8 +24306,154 @@ const noStringRefs = defineRule({
23862
24306
  }
23863
24307
  });
23864
24308
  //#endregion
24309
+ //#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
24310
+ const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
24311
+ if (token.includes(":")) return false;
24312
+ if (!token.startsWith(prefix)) return false;
24313
+ const value = token.slice(prefix.length);
24314
+ if (value === "" || value === "current") return false;
24315
+ if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
24316
+ return true;
24317
+ });
24318
+ const isCurrentColor = (attribute) => {
24319
+ const value = getJsxPropStringValue(attribute);
24320
+ return value !== null && value.trim().toLowerCase() === "currentcolor";
24321
+ };
24322
+ const noSvgCurrentcolorWithFillClass = defineRule({
24323
+ id: "no-svg-currentcolor-with-fill-class",
24324
+ title: "currentColor fights a fill/stroke class",
24325
+ tags: ["design", "test-noise"],
24326
+ severity: "warn",
24327
+ 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.",
24328
+ create: (context) => ({ JSXOpeningElement(node) {
24329
+ const classNameValue = getStringFromClassNameAttr(node);
24330
+ if (!classNameValue) return;
24331
+ for (const paint of ["fill", "stroke"]) {
24332
+ const attribute = findJsxAttribute(node.attributes, paint);
24333
+ if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
24334
+ context.report({
24335
+ node: attribute,
24336
+ 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.`
24337
+ });
24338
+ return;
24339
+ }
24340
+ }
24341
+ } })
24342
+ });
24343
+ //#endregion
24344
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
24345
+ 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)`).";
24346
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24347
+ const noSyncXhr = defineRule({
24348
+ id: "no-sync-xhr",
24349
+ title: "Synchronous XMLHttpRequest",
24350
+ severity: "warn",
24351
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
24352
+ create: (context) => ({ CallExpression(node) {
24353
+ const callee = node.callee;
24354
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
24355
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
24356
+ const asyncArgument = node.arguments?.[2];
24357
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24358
+ context.report({
24359
+ node,
24360
+ message: MESSAGE$14
24361
+ });
24362
+ } })
24363
+ });
24364
+ //#endregion
24365
+ //#region src/plugin/rules/design/no-tailwind-layout-transition.ts
24366
+ const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
24367
+ const LAYOUT_PROPERTIES = new Set([
24368
+ "width",
24369
+ "height",
24370
+ "min-width",
24371
+ "max-width",
24372
+ "min-height",
24373
+ "max-height",
24374
+ "top",
24375
+ "left",
24376
+ "right",
24377
+ "bottom",
24378
+ "inset",
24379
+ "inset-block",
24380
+ "inset-inline",
24381
+ "margin",
24382
+ "margin-top",
24383
+ "margin-right",
24384
+ "margin-bottom",
24385
+ "margin-left",
24386
+ "margin-block",
24387
+ "margin-inline",
24388
+ "padding",
24389
+ "padding-top",
24390
+ "padding-right",
24391
+ "padding-bottom",
24392
+ "padding-left",
24393
+ "padding-block",
24394
+ "padding-inline"
24395
+ ]);
24396
+ const noTailwindLayoutTransition = defineRule({
24397
+ id: "no-tailwind-layout-transition",
24398
+ title: "Animating a layout property",
24399
+ tags: ["design", "test-noise"],
24400
+ severity: "warn",
24401
+ category: "Performance",
24402
+ 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`.",
24403
+ create: (context) => ({ JSXOpeningElement(node) {
24404
+ const classNameValue = getStringFromClassNameAttr(node);
24405
+ if (!classNameValue) return;
24406
+ for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
24407
+ const animatedProperties = transitionMatch[1];
24408
+ const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
24409
+ if (layoutProperty) context.report({
24410
+ node,
24411
+ message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
24412
+ });
24413
+ }
24414
+ } })
24415
+ });
24416
+ //#endregion
24417
+ //#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
24418
+ 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\"`.";
24419
+ const targetIsBlank = (attribute) => {
24420
+ const stringValue = getJsxPropStringValue(attribute);
24421
+ if (stringValue !== null) return stringValue === "_blank";
24422
+ const value = attribute.value;
24423
+ if (value && isNodeOfType(value, "JSXExpressionContainer")) {
24424
+ const expression = value.expression;
24425
+ if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
24426
+ }
24427
+ return false;
24428
+ };
24429
+ const noTargetBlankWithoutRel = defineRule({
24430
+ id: "no-target-blank-without-rel",
24431
+ title: "target=_blank without rel=noopener",
24432
+ severity: "warn",
24433
+ recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
24434
+ create: (context) => ({ JSXOpeningElement(node) {
24435
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24436
+ const tagName = node.name.name;
24437
+ if (tagName !== "a" && tagName !== "area") return;
24438
+ if (hasJsxSpreadAttribute(node.attributes)) return;
24439
+ const targetAttribute = findJsxAttribute(node.attributes, "target");
24440
+ if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
24441
+ const relAttribute = findJsxAttribute(node.attributes, "rel");
24442
+ if (relAttribute) {
24443
+ const relValue = getJsxPropStringValue(relAttribute);
24444
+ if (relValue === null) return;
24445
+ const tokens = relValue.toLowerCase().split(/\s+/);
24446
+ if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
24447
+ }
24448
+ context.report({
24449
+ node: node.name,
24450
+ message: MESSAGE$13
24451
+ });
24452
+ } })
24453
+ });
24454
+ //#endregion
23865
24455
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23866
- const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24456
+ const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
23867
24457
  const isInsideClassMethod = (node, customClassFactoryNames) => {
23868
24458
  let ancestor = node.parent;
23869
24459
  while (ancestor) {
@@ -23932,7 +24522,7 @@ const noThisInSfc = defineRule({
23932
24522
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
23933
24523
  context.report({
23934
24524
  node,
23935
- message: MESSAGE$10
24525
+ message: MESSAGE$12
23936
24526
  });
23937
24527
  } };
23938
24528
  }
@@ -23970,26 +24560,39 @@ const noTinyText = defineRule({
23970
24560
  });
23971
24561
  //#endregion
23972
24562
  //#region src/plugin/rules/performance/no-transition-all.ts
24563
+ const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
24564
+ 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`.";
23973
24565
  const noTransitionAll = defineRule({
23974
24566
  id: "no-transition-all",
23975
24567
  title: "transition: all animates everything",
23976
24568
  tags: ["test-noise"],
23977
24569
  severity: "warn",
23978
24570
  recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
23979
- create: (context) => ({ JSXAttribute(node) {
23980
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
23981
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
23982
- const expression = node.value.expression;
23983
- if (!isNodeOfType(expression, "ObjectExpression")) return;
23984
- for (const property of expression.properties ?? []) {
23985
- if (!isNodeOfType(property, "Property")) continue;
23986
- if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
23987
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
23988
- node: property,
23989
- message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24571
+ create: (context) => ({
24572
+ JSXAttribute(node) {
24573
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24574
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24575
+ const expression = node.value.expression;
24576
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24577
+ for (const property of expression.properties ?? []) {
24578
+ if (!isNodeOfType(property, "Property")) continue;
24579
+ const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
24580
+ if (key !== "transition" && key !== "transitionProperty") continue;
24581
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
24582
+ node: property,
24583
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24584
+ });
24585
+ }
24586
+ },
24587
+ JSXOpeningElement(node) {
24588
+ const classNameValue = getStringFromClassNameAttr(node);
24589
+ if (!classNameValue) return;
24590
+ if (hasTransitionAllClass(classNameValue)) context.report({
24591
+ node,
24592
+ message: TAILWIND_MESSAGE
23990
24593
  });
23991
24594
  }
23992
- } })
24595
+ })
23993
24596
  });
23994
24597
  //#endregion
23995
24598
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -24033,7 +24636,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
24033
24636
  }
24034
24637
  return stateNames;
24035
24638
  };
24036
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
24037
24639
  const noUncontrolledInput = defineRule({
24038
24640
  id: "no-uncontrolled-input",
24039
24641
  title: "Uncontrolled input value",
@@ -24137,6 +24739,38 @@ const noUnescapedEntities = defineRule({
24137
24739
  } })
24138
24740
  });
24139
24741
  //#endregion
24742
+ //#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
24743
+ const UNINFORMATIVE_LABELS = new Set([
24744
+ "icon",
24745
+ "button",
24746
+ "image",
24747
+ "img",
24748
+ "link",
24749
+ "graphic",
24750
+ "svg",
24751
+ "picture",
24752
+ "element",
24753
+ "field",
24754
+ "input"
24755
+ ]);
24756
+ 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\"`.";
24757
+ const noUninformativeAriaLabel = defineRule({
24758
+ id: "no-uninformative-aria-label",
24759
+ title: "Uninformative aria-label",
24760
+ severity: "warn",
24761
+ recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
24762
+ create: (context) => ({ JSXOpeningElement(node) {
24763
+ const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
24764
+ if (!ariaLabel) return;
24765
+ const labelValue = getJsxPropStringValue(ariaLabel);
24766
+ if (labelValue === null) return;
24767
+ if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
24768
+ node: ariaLabel,
24769
+ message: MESSAGE$11
24770
+ });
24771
+ } })
24772
+ });
24773
+ //#endregion
24140
24774
  //#region src/plugin/constants/dom-aria-properties.ts
24141
24775
  const ARIA_PROPERTY_NAMES = new Set([
24142
24776
  "activedescendant",
@@ -25608,7 +26242,7 @@ const noWideLetterSpacing = defineRule({
25608
26242
  //#endregion
25609
26243
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
25610
26244
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
25611
- const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26245
+ const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
25612
26246
  const resolveSettings$7 = (settings) => {
25613
26247
  const reactDoctor = settings?.["react-doctor"];
25614
26248
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -25642,7 +26276,7 @@ const noWillUpdateSetState = defineRule({
25642
26276
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25643
26277
  context.report({
25644
26278
  node: node.callee,
25645
- message: MESSAGE$9
26279
+ message: MESSAGE$10
25646
26280
  });
25647
26281
  } };
25648
26282
  }
@@ -26520,7 +27154,7 @@ const preactNoRenderArguments = defineRule({
26520
27154
  });
26521
27155
  //#endregion
26522
27156
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
26523
- 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.";
27157
+ 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.";
26524
27158
  const preactPreferOndblclick = defineRule({
26525
27159
  id: "preact-prefer-ondblclick",
26526
27160
  title: "onDoubleClick instead of onDblClick",
@@ -26535,7 +27169,7 @@ const preactPreferOndblclick = defineRule({
26535
27169
  if (!onDoubleClickAttribute) return;
26536
27170
  context.report({
26537
27171
  node: onDoubleClickAttribute,
26538
- message: MESSAGE$8
27172
+ message: MESSAGE$9
26539
27173
  });
26540
27174
  } })
26541
27175
  });
@@ -26575,6 +27209,42 @@ const preactPreferOninput = defineRule({
26575
27209
  } })
26576
27210
  });
26577
27211
  //#endregion
27212
+ //#region src/plugin/rules/design/prefer-dvh-over-vh.ts
27213
+ const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
27214
+ const HEIGHT_KEYS = new Set(["height", "minHeight"]);
27215
+ 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`).";
27216
+ const preferDvhOverVh = defineRule({
27217
+ id: "prefer-dvh-over-vh",
27218
+ title: "Use dvh instead of vh for full height",
27219
+ tags: ["design", "test-noise"],
27220
+ severity: "warn",
27221
+ requires: ["tailwind:3.4"],
27222
+ 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+.)",
27223
+ create: (context) => ({
27224
+ JSXAttribute(node) {
27225
+ const expression = getInlineStyleExpression(node);
27226
+ if (!expression) return;
27227
+ for (const property of expression.properties ?? []) {
27228
+ const key = getStylePropertyKey(property);
27229
+ if (!key || !HEIGHT_KEYS.has(key)) continue;
27230
+ const value = getStylePropertyStringValue(property);
27231
+ if (value && value.trim().toLowerCase() === "100vh") context.report({
27232
+ node: property,
27233
+ message: MESSAGE$8
27234
+ });
27235
+ }
27236
+ },
27237
+ JSXOpeningElement(node) {
27238
+ const classNameValue = getStringFromClassNameAttr(node);
27239
+ if (!classNameValue) return;
27240
+ if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
27241
+ node,
27242
+ message: MESSAGE$8
27243
+ });
27244
+ }
27245
+ })
27246
+ });
27247
+ //#endregion
26578
27248
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
26579
27249
  const preferDynamicImport = defineRule({
26580
27250
  id: "prefer-dynamic-import",
@@ -27166,6 +27836,26 @@ const preferTagOverRole = defineRule({
27166
27836
  } })
27167
27837
  });
27168
27838
  //#endregion
27839
+ //#region src/plugin/rules/design/prefer-truncate-shorthand.ts
27840
+ const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
27841
+ const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
27842
+ const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
27843
+ const preferTruncateShorthand = defineRule({
27844
+ id: "prefer-truncate-shorthand",
27845
+ title: "Use truncate shorthand",
27846
+ tags: ["design", "test-noise"],
27847
+ severity: "warn",
27848
+ recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
27849
+ create: (context) => ({ JSXOpeningElement(node) {
27850
+ const classNameValue = getStringFromClassNameAttr(node);
27851
+ if (!classNameValue) return;
27852
+ if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
27853
+ node,
27854
+ message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
27855
+ });
27856
+ } })
27857
+ });
27858
+ //#endregion
27169
27859
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27170
27860
  const collectFunctionTypedLocalBindings = (componentBody) => {
27171
27861
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -35437,13 +36127,7 @@ const serverNoMutableModuleState = defineRule({
35437
36127
  const collectDeclaredNames = (declaration) => {
35438
36128
  const names = /* @__PURE__ */ new Set();
35439
36129
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35440
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35441
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35442
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35443
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35444
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35445
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35446
- }
36130
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35447
36131
  return names;
35448
36132
  };
35449
36133
  const declarationStartsWithAwait = (declaration) => {
@@ -35453,11 +36137,15 @@ const declarationStartsWithAwait = (declaration) => {
35453
36137
  };
35454
36138
  const declarationReadsAnyName = (declaration, names) => {
35455
36139
  if (names.size === 0) return false;
36140
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35456
36141
  let didRead = false;
35457
- walkAst(declaration, (child) => {
35458
- if (didRead) return;
35459
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35460
- });
36142
+ for (const declarator of declaration.declarations ?? []) {
36143
+ if (!declarator.init) continue;
36144
+ walkAst(declarator.init, (child) => {
36145
+ if (didRead) return;
36146
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
36147
+ });
36148
+ }
35461
36149
  return didRead;
35462
36150
  };
35463
36151
  const serverSequentialIndependentAwait = defineRule({
@@ -36717,7 +37405,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36717
37405
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36718
37406
  scan: scanByPattern({
36719
37407
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36720
- pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
37408
+ pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
36721
37409
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36722
37410
  })
36723
37411
  });
@@ -38842,6 +39530,17 @@ const reactDoctorRules = [
38842
39530
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
38843
39531
  }
38844
39532
  },
39533
+ {
39534
+ key: "react-doctor/no-arbitrary-px-font-size",
39535
+ id: "no-arbitrary-px-font-size",
39536
+ source: "react-doctor",
39537
+ originallyExternal: false,
39538
+ rule: {
39539
+ ...noArbitraryPxFontSize,
39540
+ framework: "global",
39541
+ category: "Accessibility"
39542
+ }
39543
+ },
38845
39544
  {
38846
39545
  key: "react-doctor/no-aria-hidden-on-focusable",
38847
39546
  id: "no-aria-hidden-on-focusable",
@@ -38901,6 +39600,18 @@ const reactDoctorRules = [
38901
39600
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
38902
39601
  }
38903
39602
  },
39603
+ {
39604
+ key: "react-doctor/no-autoplay-without-muted",
39605
+ id: "no-autoplay-without-muted",
39606
+ source: "react-doctor",
39607
+ originallyExternal: false,
39608
+ rule: {
39609
+ ...noAutoplayWithoutMuted,
39610
+ framework: "global",
39611
+ category: "Accessibility",
39612
+ requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39613
+ }
39614
+ },
38904
39615
  {
38905
39616
  key: "react-doctor/no-barrel-import",
38906
39617
  id: "no-barrel-import",
@@ -39054,6 +39765,17 @@ const reactDoctorRules = [
39054
39765
  category: "Maintainability"
39055
39766
  }
39056
39767
  },
39768
+ {
39769
+ key: "react-doctor/no-deprecated-tailwind-class",
39770
+ id: "no-deprecated-tailwind-class",
39771
+ source: "react-doctor",
39772
+ originallyExternal: false,
39773
+ rule: {
39774
+ ...noDeprecatedTailwindClass,
39775
+ framework: "global",
39776
+ category: "Maintainability"
39777
+ }
39778
+ },
39057
39779
  {
39058
39780
  key: "react-doctor/no-derived-state",
39059
39781
  id: "no-derived-state",
@@ -39173,6 +39895,17 @@ const reactDoctorRules = [
39173
39895
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
39174
39896
  }
39175
39897
  },
39898
+ {
39899
+ key: "react-doctor/no-document-write",
39900
+ id: "no-document-write",
39901
+ source: "react-doctor",
39902
+ originallyExternal: false,
39903
+ rule: {
39904
+ ...noDocumentWrite,
39905
+ framework: "global",
39906
+ category: "Performance"
39907
+ }
39908
+ },
39176
39909
  {
39177
39910
  key: "react-doctor/no-dynamic-import-path",
39178
39911
  id: "no-dynamic-import-path",
@@ -39314,6 +40047,17 @@ const reactDoctorRules = [
39314
40047
  category: "Performance"
39315
40048
  }
39316
40049
  },
40050
+ {
40051
+ key: "react-doctor/no-full-viewport-width",
40052
+ id: "no-full-viewport-width",
40053
+ source: "react-doctor",
40054
+ originallyExternal: false,
40055
+ rule: {
40056
+ ...noFullViewportWidth,
40057
+ framework: "global",
40058
+ category: "Maintainability"
40059
+ }
40060
+ },
39317
40061
  {
39318
40062
  key: "react-doctor/no-generic-handler-names",
39319
40063
  id: "no-generic-handler-names",
@@ -39553,6 +40297,17 @@ const reactDoctorRules = [
39553
40297
  category: "Performance"
39554
40298
  }
39555
40299
  },
40300
+ {
40301
+ key: "react-doctor/no-low-contrast-inline-style",
40302
+ id: "no-low-contrast-inline-style",
40303
+ source: "react-doctor",
40304
+ originallyExternal: false,
40305
+ rule: {
40306
+ ...noLowContrastInlineStyle,
40307
+ framework: "global",
40308
+ category: "Accessibility"
40309
+ }
40310
+ },
39556
40311
  {
39557
40312
  key: "react-doctor/no-many-boolean-props",
39558
40313
  id: "no-many-boolean-props",
@@ -39830,6 +40585,17 @@ const reactDoctorRules = [
39830
40585
  category: "Maintainability"
39831
40586
  }
39832
40587
  },
40588
+ {
40589
+ key: "react-doctor/no-redundant-display-class",
40590
+ id: "no-redundant-display-class",
40591
+ source: "react-doctor",
40592
+ originallyExternal: false,
40593
+ rule: {
40594
+ ...noRedundantDisplayClass,
40595
+ framework: "global",
40596
+ category: "Maintainability"
40597
+ }
40598
+ },
39833
40599
  {
39834
40600
  key: "react-doctor/no-redundant-roles",
39835
40601
  id: "no-redundant-roles",
@@ -39982,6 +40748,18 @@ const reactDoctorRules = [
39982
40748
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39983
40749
  }
39984
40750
  },
40751
+ {
40752
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40753
+ id: "no-string-false-on-boolean-attribute",
40754
+ source: "react-doctor",
40755
+ originallyExternal: false,
40756
+ rule: {
40757
+ ...noStringFalseOnBooleanAttribute,
40758
+ framework: "global",
40759
+ category: "Bugs",
40760
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40761
+ }
40762
+ },
39985
40763
  {
39986
40764
  key: "react-doctor/no-string-refs",
39987
40765
  id: "no-string-refs",
@@ -39994,6 +40772,51 @@ const reactDoctorRules = [
39994
40772
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39995
40773
  }
39996
40774
  },
40775
+ {
40776
+ key: "react-doctor/no-svg-currentcolor-with-fill-class",
40777
+ id: "no-svg-currentcolor-with-fill-class",
40778
+ source: "react-doctor",
40779
+ originallyExternal: false,
40780
+ rule: {
40781
+ ...noSvgCurrentcolorWithFillClass,
40782
+ framework: "global",
40783
+ category: "Maintainability"
40784
+ }
40785
+ },
40786
+ {
40787
+ key: "react-doctor/no-sync-xhr",
40788
+ id: "no-sync-xhr",
40789
+ source: "react-doctor",
40790
+ originallyExternal: false,
40791
+ rule: {
40792
+ ...noSyncXhr,
40793
+ framework: "global",
40794
+ category: "Performance"
40795
+ }
40796
+ },
40797
+ {
40798
+ key: "react-doctor/no-tailwind-layout-transition",
40799
+ id: "no-tailwind-layout-transition",
40800
+ source: "react-doctor",
40801
+ originallyExternal: false,
40802
+ rule: {
40803
+ ...noTailwindLayoutTransition,
40804
+ framework: "global",
40805
+ category: "Performance"
40806
+ }
40807
+ },
40808
+ {
40809
+ key: "react-doctor/no-target-blank-without-rel",
40810
+ id: "no-target-blank-without-rel",
40811
+ source: "react-doctor",
40812
+ originallyExternal: false,
40813
+ rule: {
40814
+ ...noTargetBlankWithoutRel,
40815
+ framework: "global",
40816
+ category: "Accessibility",
40817
+ requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40818
+ }
40819
+ },
39997
40820
  {
39998
40821
  key: "react-doctor/no-this-in-sfc",
39999
40822
  id: "no-this-in-sfc",
@@ -40063,6 +40886,18 @@ const reactDoctorRules = [
40063
40886
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
40064
40887
  }
40065
40888
  },
40889
+ {
40890
+ key: "react-doctor/no-uninformative-aria-label",
40891
+ id: "no-uninformative-aria-label",
40892
+ source: "react-doctor",
40893
+ originallyExternal: false,
40894
+ rule: {
40895
+ ...noUninformativeAriaLabel,
40896
+ framework: "global",
40897
+ category: "Accessibility",
40898
+ requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40899
+ }
40900
+ },
40066
40901
  {
40067
40902
  key: "react-doctor/no-unknown-property",
40068
40903
  id: "no-unknown-property",
@@ -40272,6 +41107,17 @@ const reactDoctorRules = [
40272
41107
  category: "Bugs"
40273
41108
  }
40274
41109
  },
41110
+ {
41111
+ key: "react-doctor/prefer-dvh-over-vh",
41112
+ id: "prefer-dvh-over-vh",
41113
+ source: "react-doctor",
41114
+ originallyExternal: false,
41115
+ rule: {
41116
+ ...preferDvhOverVh,
41117
+ framework: "global",
41118
+ category: "Maintainability"
41119
+ }
41120
+ },
40275
41121
  {
40276
41122
  key: "react-doctor/prefer-dynamic-import",
40277
41123
  id: "prefer-dynamic-import",
@@ -40376,6 +41222,17 @@ const reactDoctorRules = [
40376
41222
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
40377
41223
  }
40378
41224
  },
41225
+ {
41226
+ key: "react-doctor/prefer-truncate-shorthand",
41227
+ id: "prefer-truncate-shorthand",
41228
+ source: "react-doctor",
41229
+ originallyExternal: false,
41230
+ rule: {
41231
+ ...preferTruncateShorthand,
41232
+ framework: "global",
41233
+ category: "Maintainability"
41234
+ }
41235
+ },
40379
41236
  {
40380
41237
  key: "react-doctor/prefer-use-effect-event",
40381
41238
  id: "prefer-use-effect-event",