oxlint-plugin-react-doctor 0.5.6-dev.451beeb → 0.5.6-dev.6b8e756

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 +504 -0
  2. package/dist/index.js +853 -173
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1861,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
1861
1861
  });
1862
1862
  //#endregion
1863
1863
  //#region src/plugin/rules/a11y/anchor-has-content.ts
1864
- const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1864
+ const MESSAGE$64 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
1865
1865
  const anchorHasContent = defineRule({
1866
1866
  id: "anchor-has-content",
1867
1867
  title: "Anchor has no content",
@@ -1877,7 +1877,7 @@ const anchorHasContent = defineRule({
1877
1877
  for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
1878
1878
  context.report({
1879
1879
  node: opening.name,
1880
- message: MESSAGE$59
1880
+ message: MESSAGE$64
1881
1881
  });
1882
1882
  } })
1883
1883
  });
@@ -2271,7 +2271,7 @@ const parseJsxValue = (value) => {
2271
2271
  };
2272
2272
  //#endregion
2273
2273
  //#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
2274
- const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2274
+ const MESSAGE$63 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
2275
2275
  const ariaActivedescendantHasTabindex = defineRule({
2276
2276
  id: "aria-activedescendant-has-tabindex",
2277
2277
  title: "aria-activedescendant missing tabindex",
@@ -2289,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
2289
2289
  if (tabIndexValue === null || tabIndexValue >= -1) return;
2290
2290
  context.report({
2291
2291
  node: node.name,
2292
- message: MESSAGE$58
2292
+ message: MESSAGE$63
2293
2293
  });
2294
2294
  return;
2295
2295
  }
2296
2296
  if (isInteractiveElement(tag, node)) return;
2297
2297
  context.report({
2298
2298
  node: node.name,
2299
- message: MESSAGE$58
2299
+ message: MESSAGE$63
2300
2300
  });
2301
2301
  } })
2302
2302
  });
@@ -3085,7 +3085,7 @@ const artifactBaasAuthoritySurface = defineRule({
3085
3085
  scan: scanByPattern({
3086
3086
  shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
3087
3087
  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],
3088
+ 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
3089
  message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
3090
3090
  })
3091
3091
  });
@@ -4272,7 +4272,7 @@ const asyncParallel = defineRule({
4272
4272
  });
4273
4273
  //#endregion
4274
4274
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4275
- const MESSAGE$57 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4275
+ const MESSAGE$62 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
4276
4276
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4277
4277
  const STORAGE_GLOBALS = new Set([
4278
4278
  "window",
@@ -4306,7 +4306,7 @@ const authTokenInWebStorage = defineRule({
4306
4306
  if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4307
4307
  context.report({
4308
4308
  node,
4309
- message: MESSAGE$57
4309
+ message: MESSAGE$62
4310
4310
  });
4311
4311
  },
4312
4312
  AssignmentExpression(node) {
@@ -4317,7 +4317,7 @@ const authTokenInWebStorage = defineRule({
4317
4317
  if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4318
4318
  context.report({
4319
4319
  node: target,
4320
- message: MESSAGE$57
4320
+ message: MESSAGE$62
4321
4321
  });
4322
4322
  }
4323
4323
  })
@@ -4694,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
4694
4694
  //#endregion
4695
4695
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4696
4696
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4697
- const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4697
+ const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
4698
4698
  const KEY_HANDLERS = [
4699
4699
  "onKeyUp",
4700
4700
  "onKeyDown",
@@ -4726,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
4726
4726
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4727
4727
  context.report({
4728
4728
  node: node.name,
4729
- message: MESSAGE$56
4729
+ message: MESSAGE$61
4730
4730
  });
4731
4731
  } };
4732
4732
  }
@@ -4841,7 +4841,7 @@ const isReactComponentName = (name) => {
4841
4841
  };
4842
4842
  //#endregion
4843
4843
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4844
- const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4844
+ const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
4845
4845
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4846
4846
  const DEFAULT_LABELLING_PROPS = [
4847
4847
  "alt",
@@ -5002,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
5002
5002
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
5003
5003
  context.report({
5004
5004
  node: opening,
5005
- message: MESSAGE$55
5005
+ message: MESSAGE$60
5006
5006
  });
5007
5007
  } };
5008
5008
  }
@@ -5131,6 +5131,7 @@ const dangerousHtmlSink = defineRule({
5131
5131
  return findings;
5132
5132
  }
5133
5133
  });
5134
+ const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5134
5135
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5135
5136
  const VAGUE_BUTTON_LABELS = new Set([
5136
5137
  "continue",
@@ -5429,10 +5430,10 @@ const noVagueButtonLabel = defineRule({
5429
5430
  });
5430
5431
  //#endregion
5431
5432
  //#region src/plugin/utils/has-jsx-spread-attribute.ts
5432
- const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
5434
  //#endregion
5434
5435
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5435
- const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
5436
+ const 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
5437
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5437
5438
  const NAME_PROVIDING_ATTRIBUTES = [
5438
5439
  "aria-label",
@@ -5451,11 +5452,11 @@ const dialogHasAccessibleName = defineRule({
5451
5452
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5452
5453
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5453
5454
  if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5454
- if (hasJsxSpreadAttribute$1(node.attributes)) return;
5455
+ if (hasJsxSpreadAttribute(node.attributes)) return;
5455
5456
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5456
5457
  context.report({
5457
5458
  node: node.name,
5458
- message: MESSAGE$54
5459
+ message: MESSAGE$59
5459
5460
  });
5460
5461
  } })
5461
5462
  });
@@ -5494,7 +5495,7 @@ const isEs6Component = (node) => {
5494
5495
  };
5495
5496
  //#endregion
5496
5497
  //#region src/plugin/rules/react-builtins/display-name.ts
5497
- const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5498
+ const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
5498
5499
  const DEFAULT_ADDITIONAL_HOCS = [
5499
5500
  "observer",
5500
5501
  "lazy",
@@ -5697,7 +5698,7 @@ const displayName = defineRule({
5697
5698
  const reportAt = (node) => {
5698
5699
  context.report({
5699
5700
  node,
5700
- message: MESSAGE$53
5701
+ message: MESSAGE$58
5701
5702
  });
5702
5703
  };
5703
5704
  return {
@@ -7845,7 +7846,7 @@ const forbidElements = defineRule({
7845
7846
  });
7846
7847
  //#endregion
7847
7848
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7848
- const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7849
+ const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
7849
7850
  const forwardRefUsesRef = defineRule({
7850
7851
  id: "forward-ref-uses-ref",
7851
7852
  title: "forwardRef without ref parameter",
@@ -7865,7 +7866,7 @@ const forwardRefUsesRef = defineRule({
7865
7866
  if (isNodeOfType(onlyParam, "RestElement")) return;
7866
7867
  context.report({
7867
7868
  node: inner,
7868
- message: MESSAGE$52
7869
+ message: MESSAGE$57
7869
7870
  });
7870
7871
  } })
7871
7872
  });
@@ -7902,7 +7903,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7902
7903
  });
7903
7904
  //#endregion
7904
7905
  //#region src/plugin/rules/a11y/heading-has-content.ts
7905
- const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7906
+ const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
7906
7907
  const DEFAULT_HEADING_TAGS = [
7907
7908
  "h1",
7908
7909
  "h2",
@@ -7935,7 +7936,7 @@ const headingHasContent = defineRule({
7935
7936
  if (isHiddenFromScreenReader(node, context.settings)) return;
7936
7937
  context.report({
7937
7938
  node,
7938
- message: MESSAGE$51
7939
+ message: MESSAGE$56
7939
7940
  });
7940
7941
  } };
7941
7942
  }
@@ -8073,7 +8074,7 @@ const hooksNoNanInDeps = defineRule({
8073
8074
  });
8074
8075
  //#endregion
8075
8076
  //#region src/plugin/rules/a11y/html-has-lang.ts
8076
- const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8077
+ const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
8077
8078
  const resolveSettings$38 = (settings) => {
8078
8079
  const reactDoctor = settings?.["react-doctor"];
8079
8080
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8121,7 +8122,7 @@ const htmlHasLang = defineRule({
8121
8122
  if (!lang) {
8122
8123
  context.report({
8123
8124
  node: node.name,
8124
- message: MESSAGE$50
8125
+ message: MESSAGE$55
8125
8126
  });
8126
8127
  return;
8127
8128
  }
@@ -8129,13 +8130,13 @@ const htmlHasLang = defineRule({
8129
8130
  if (verdict === "missing" || verdict === "empty") {
8130
8131
  context.report({
8131
8132
  node: lang,
8132
- message: MESSAGE$50
8133
+ message: MESSAGE$55
8133
8134
  });
8134
8135
  return;
8135
8136
  }
8136
8137
  if (hasSpread && !lang) context.report({
8137
8138
  node: node.name,
8138
- message: MESSAGE$50
8139
+ message: MESSAGE$55
8139
8140
  });
8140
8141
  } };
8141
8142
  }
@@ -8349,7 +8350,7 @@ const htmlNoNestedInteractive = defineRule({
8349
8350
  });
8350
8351
  //#endregion
8351
8352
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8352
- const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8353
+ const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
8353
8354
  const evaluateTitleValue = (value) => {
8354
8355
  if (!value) return "missing";
8355
8356
  if (isNodeOfType(value, "Literal")) {
@@ -8389,14 +8390,14 @@ const iframeHasTitle = defineRule({
8389
8390
  if (!titleAttr) {
8390
8391
  if (hasSpread || tag === "iframe") context.report({
8391
8392
  node: node.name,
8392
- message: MESSAGE$49
8393
+ message: MESSAGE$54
8393
8394
  });
8394
8395
  return;
8395
8396
  }
8396
8397
  const verdict = evaluateTitleValue(titleAttr.value);
8397
8398
  if (verdict === "missing" || verdict === "empty") context.report({
8398
8399
  node: titleAttr,
8399
- message: MESSAGE$49
8400
+ message: MESSAGE$54
8400
8401
  });
8401
8402
  } })
8402
8403
  });
@@ -8500,7 +8501,7 @@ const iframeMissingSandbox = defineRule({
8500
8501
  });
8501
8502
  //#endregion
8502
8503
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8503
- const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8504
+ const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
8504
8505
  const DEFAULT_COMPONENTS = ["img"];
8505
8506
  const DEFAULT_REDUNDANT_WORDS = [
8506
8507
  "image",
@@ -8565,7 +8566,7 @@ const imgRedundantAlt = defineRule({
8565
8566
  if (!altAttribute) return;
8566
8567
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8567
8568
  node: altAttribute,
8568
- message: MESSAGE$48
8569
+ message: MESSAGE$53
8569
8570
  });
8570
8571
  } };
8571
8572
  }
@@ -10922,7 +10923,7 @@ const jsxMaxDepth = defineRule({
10922
10923
  });
10923
10924
  //#endregion
10924
10925
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10925
- const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10926
+ const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
10926
10927
  const LITERAL_TEXT_TAGS = new Set([
10927
10928
  "code",
10928
10929
  "pre",
@@ -10958,7 +10959,7 @@ const jsxNoCommentTextnodes = defineRule({
10958
10959
  if (isInsideLiteralTextTag(node)) return;
10959
10960
  context.report({
10960
10961
  node,
10961
- message: MESSAGE$47
10962
+ message: MESSAGE$52
10962
10963
  });
10963
10964
  } })
10964
10965
  });
@@ -10989,7 +10990,7 @@ const isInsideFunctionScope = (node) => {
10989
10990
  };
10990
10991
  //#endregion
10991
10992
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10992
- const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
10993
+ const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
10993
10994
  const CONTEXT_MODULES$1 = [
10994
10995
  "react",
10995
10996
  "use-context-selector",
@@ -11087,7 +11088,7 @@ const jsxNoConstructedContextValues = defineRule({
11087
11088
  if (!isConstructedValue(innerExpression)) continue;
11088
11089
  context.report({
11089
11090
  node: attribute,
11090
- message: MESSAGE$46
11091
+ message: MESSAGE$51
11091
11092
  });
11092
11093
  }
11093
11094
  }
@@ -11173,7 +11174,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11173
11174
  };
11174
11175
  //#endregion
11175
11176
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11176
- const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
11177
+ const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
11177
11178
  const KNOWN_SLOT_PROP_NAMES = new Set([
11178
11179
  "icon",
11179
11180
  "Icon",
@@ -11442,7 +11443,7 @@ const jsxNoJsxAsProp = defineRule({
11442
11443
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11443
11444
  context.report({
11444
11445
  node,
11445
- message: MESSAGE$45
11446
+ message: MESSAGE$50
11446
11447
  });
11447
11448
  }
11448
11449
  };
@@ -11730,7 +11731,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11730
11731
  ];
11731
11732
  //#endregion
11732
11733
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11733
- const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
11734
+ const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
11734
11735
  const isDataArrayPropName = (propName) => {
11735
11736
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11736
11737
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11814,7 +11815,7 @@ const jsxNoNewArrayAsProp = defineRule({
11814
11815
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11815
11816
  context.report({
11816
11817
  node,
11817
- message: MESSAGE$44
11818
+ message: MESSAGE$49
11818
11819
  });
11819
11820
  }
11820
11821
  };
@@ -12072,7 +12073,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12072
12073
  ]);
12073
12074
  //#endregion
12074
12075
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
12075
- const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
12076
+ const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
12076
12077
  const isAccessorPredicateName = (propName) => {
12077
12078
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12078
12079
  if (propName.length <= prefix.length) continue;
@@ -12278,7 +12279,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12278
12279
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12279
12280
  context.report({
12280
12281
  node,
12281
- message: MESSAGE$43
12282
+ message: MESSAGE$48
12282
12283
  });
12283
12284
  }
12284
12285
  };
@@ -12498,7 +12499,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12498
12499
  ];
12499
12500
  //#endregion
12500
12501
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12501
- const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
12502
+ const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
12502
12503
  const isConfigObjectPropName = (propName) => {
12503
12504
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12504
12505
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12586,7 +12587,7 @@ const jsxNoNewObjectAsProp = defineRule({
12586
12587
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12587
12588
  context.report({
12588
12589
  node,
12589
- message: MESSAGE$42
12590
+ message: MESSAGE$47
12590
12591
  });
12591
12592
  }
12592
12593
  };
@@ -12594,7 +12595,7 @@ const jsxNoNewObjectAsProp = defineRule({
12594
12595
  });
12595
12596
  //#endregion
12596
12597
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12597
- const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12598
+ const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
12598
12599
  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
12600
  const resolveSettings$28 = (settings) => {
12600
12601
  const reactDoctor = settings?.["react-doctor"];
@@ -12635,7 +12636,7 @@ const jsxNoScriptUrl = defineRule({
12635
12636
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12636
12637
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12637
12638
  node: attribute,
12638
- message: MESSAGE$41
12639
+ message: MESSAGE$46
12639
12640
  });
12640
12641
  }
12641
12642
  } };
@@ -12950,7 +12951,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12950
12951
  });
12951
12952
  //#endregion
12952
12953
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12953
- const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
12954
+ const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
12954
12955
  const resolveSettings$25 = (settings) => {
12955
12956
  const reactDoctor = settings?.["react-doctor"];
12956
12957
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12991,7 +12992,7 @@ const jsxPropsNoSpreading = defineRule({
12991
12992
  }
12992
12993
  context.report({
12993
12994
  node: attribute,
12994
- message: MESSAGE$40
12995
+ message: MESSAGE$45
12995
12996
  });
12996
12997
  }
12997
12998
  } };
@@ -13219,7 +13220,7 @@ const labelHasAssociatedControl = defineRule({
13219
13220
  });
13220
13221
  //#endregion
13221
13222
  //#region src/plugin/rules/a11y/lang.ts
13222
- const MESSAGE$39 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13223
+ const MESSAGE$44 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
13223
13224
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13224
13225
  "aa",
13225
13226
  "ab",
@@ -13431,7 +13432,7 @@ const lang = defineRule({
13431
13432
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13432
13433
  context.report({
13433
13434
  node: langAttr,
13434
- message: MESSAGE$39
13435
+ message: MESSAGE$44
13435
13436
  });
13436
13437
  return;
13437
13438
  }
@@ -13440,7 +13441,7 @@ const lang = defineRule({
13440
13441
  if (value === null) return;
13441
13442
  if (!isValidLangTag(value)) context.report({
13442
13443
  node: langAttr,
13443
- message: MESSAGE$39
13444
+ message: MESSAGE$44
13444
13445
  });
13445
13446
  } })
13446
13447
  });
@@ -13484,7 +13485,7 @@ const mdxSsrExecutionRisk = defineRule({
13484
13485
  });
13485
13486
  //#endregion
13486
13487
  //#region src/plugin/rules/a11y/media-has-caption.ts
13487
- const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13488
+ const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
13488
13489
  const DEFAULT_AUDIO = ["audio"];
13489
13490
  const DEFAULT_VIDEO = ["video"];
13490
13491
  const DEFAULT_TRACK = ["track"];
@@ -13525,7 +13526,7 @@ const mediaHasCaption = defineRule({
13525
13526
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13526
13527
  context.report({
13527
13528
  node: node.name,
13528
- message: MESSAGE$38
13529
+ message: MESSAGE$43
13529
13530
  });
13530
13531
  return;
13531
13532
  }
@@ -13542,7 +13543,7 @@ const mediaHasCaption = defineRule({
13542
13543
  return kindValue.value.toLowerCase() === "captions";
13543
13544
  })) context.report({
13544
13545
  node: node.name,
13545
- message: MESSAGE$38
13546
+ message: MESSAGE$43
13546
13547
  });
13547
13548
  } };
13548
13549
  }
@@ -15343,7 +15344,7 @@ const nextjsNoVercelOgImport = defineRule({
15343
15344
  });
15344
15345
  //#endregion
15345
15346
  //#region src/plugin/rules/a11y/no-access-key.ts
15346
- const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15347
+ const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
15347
15348
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15348
15349
  const noAccessKey = defineRule({
15349
15350
  id: "no-access-key",
@@ -15360,7 +15361,7 @@ const noAccessKey = defineRule({
15360
15361
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15361
15362
  context.report({
15362
15363
  node: accessKey,
15363
- message: MESSAGE$37
15364
+ message: MESSAGE$42
15364
15365
  });
15365
15366
  return;
15366
15367
  }
@@ -15370,7 +15371,7 @@ const noAccessKey = defineRule({
15370
15371
  if (isUndefinedIdentifier(expression)) return;
15371
15372
  context.report({
15372
15373
  node: accessKey,
15373
- message: MESSAGE$37
15374
+ message: MESSAGE$42
15374
15375
  });
15375
15376
  }
15376
15377
  } })
@@ -15852,8 +15853,41 @@ const noAdjustStateOnPropChange = defineRule({
15852
15853
  } })
15853
15854
  });
15854
15855
  //#endregion
15856
+ //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
15857
+ const getStringFromClassNameAttr = (node) => {
15858
+ if (!isNodeOfType(node, "JSXOpeningElement")) return null;
15859
+ const classAttr = findJsxAttribute(node.attributes ?? [], "className");
15860
+ if (!classAttr?.value) return null;
15861
+ if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
15862
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
15863
+ if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
15864
+ return null;
15865
+ };
15866
+ //#endregion
15867
+ //#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
15868
+ const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
15869
+ const noArbitraryPxFontSize = defineRule({
15870
+ id: "no-arbitrary-px-font-size",
15871
+ title: "Pixel arbitrary font size",
15872
+ tags: ["design", "test-noise"],
15873
+ severity: "warn",
15874
+ category: "Accessibility",
15875
+ recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
15876
+ create: (context) => ({ JSXOpeningElement(node) {
15877
+ const classNameValue = getStringFromClassNameAttr(node);
15878
+ if (!classNameValue) return;
15879
+ for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
15880
+ const rem = parseFloat(match[1]) / 16;
15881
+ context.report({
15882
+ node,
15883
+ message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
15884
+ });
15885
+ }
15886
+ } })
15887
+ });
15888
+ //#endregion
15855
15889
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15856
- const MESSAGE$36 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15890
+ const MESSAGE$41 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
15857
15891
  const noAriaHiddenOnFocusable = defineRule({
15858
15892
  id: "no-aria-hidden-on-focusable",
15859
15893
  title: "aria-hidden on focusable element",
@@ -15880,7 +15914,7 @@ const noAriaHiddenOnFocusable = defineRule({
15880
15914
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15881
15915
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15882
15916
  node: ariaHidden,
15883
- message: MESSAGE$36
15917
+ message: MESSAGE$41
15884
15918
  });
15885
15919
  } })
15886
15920
  });
@@ -16248,7 +16282,7 @@ const noArrayIndexAsKey = defineRule({
16248
16282
  });
16249
16283
  //#endregion
16250
16284
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16251
- const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
16285
+ const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
16252
16286
  const SECOND_INDEX_METHODS = new Set([
16253
16287
  "every",
16254
16288
  "filter",
@@ -16452,7 +16486,7 @@ const noArrayIndexKey = defineRule({
16452
16486
  }
16453
16487
  context.report({
16454
16488
  node: keyAttribute,
16455
- message: MESSAGE$35
16489
+ message: MESSAGE$40
16456
16490
  });
16457
16491
  },
16458
16492
  CallExpression(node) {
@@ -16472,7 +16506,7 @@ const noArrayIndexKey = defineRule({
16472
16506
  if (propName !== "key") continue;
16473
16507
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16474
16508
  node: property,
16475
- message: MESSAGE$35
16509
+ message: MESSAGE$40
16476
16510
  });
16477
16511
  }
16478
16512
  }
@@ -16480,7 +16514,7 @@ const noArrayIndexKey = defineRule({
16480
16514
  });
16481
16515
  //#endregion
16482
16516
  //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16483
- const MESSAGE$34 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16517
+ const MESSAGE$39 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
16484
16518
  const noAsyncEffectCallback = defineRule({
16485
16519
  id: "no-async-effect-callback",
16486
16520
  title: "Async effect callback",
@@ -16494,13 +16528,13 @@ const noAsyncEffectCallback = defineRule({
16494
16528
  if (!callback.async) return;
16495
16529
  context.report({
16496
16530
  node: callback,
16497
- message: MESSAGE$34
16531
+ message: MESSAGE$39
16498
16532
  });
16499
16533
  } })
16500
16534
  });
16501
16535
  //#endregion
16502
16536
  //#region src/plugin/rules/a11y/no-autofocus.ts
16503
- const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16537
+ const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
16504
16538
  const resolveSettings$21 = (settings) => {
16505
16539
  const reactDoctor = settings?.["react-doctor"];
16506
16540
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16556,12 +16590,45 @@ const noAutofocus = defineRule({
16556
16590
  }
16557
16591
  context.report({
16558
16592
  node: autoFocusAttribute,
16559
- message: MESSAGE$33
16593
+ message: MESSAGE$38
16560
16594
  });
16561
16595
  } };
16562
16596
  }
16563
16597
  });
16564
16598
  //#endregion
16599
+ //#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
16600
+ const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
16601
+ const resolveStaticBoolean = (attribute) => {
16602
+ const value = attribute.value;
16603
+ if (!value) return true;
16604
+ const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
16605
+ if (isNodeOfType(literal, "Literal")) {
16606
+ if (literal.value === true || literal.value === "true") return true;
16607
+ if (literal.value === false || literal.value === "false") return false;
16608
+ }
16609
+ return null;
16610
+ };
16611
+ const noAutoplayWithoutMuted = defineRule({
16612
+ id: "no-autoplay-without-muted",
16613
+ title: "Autoplaying media without muted",
16614
+ severity: "warn",
16615
+ recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
16616
+ create: (context) => ({ JSXOpeningElement(node) {
16617
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
16618
+ const tagName = node.name.name;
16619
+ if (tagName !== "video" && tagName !== "audio") return;
16620
+ if (hasJsxSpreadAttribute(node.attributes)) return;
16621
+ const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
16622
+ if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
16623
+ const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
16624
+ if (muted && resolveStaticBoolean(muted) !== false) return;
16625
+ context.report({
16626
+ node: node.name,
16627
+ message: MESSAGE$37
16628
+ });
16629
+ } })
16630
+ });
16631
+ //#endregion
16565
16632
  //#region src/plugin/utils/create-relative-import-source.ts
16566
16633
  const createRelativeImportSource = (filename, targetFilePath) => {
16567
16634
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -17060,7 +17127,7 @@ const noChainStateUpdates = defineRule({
17060
17127
  });
17061
17128
  //#endregion
17062
17129
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
17063
- const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17130
+ const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
17064
17131
  const noChildrenProp = defineRule({
17065
17132
  id: "no-children-prop",
17066
17133
  title: "Children passed as a prop",
@@ -17072,7 +17139,7 @@ const noChildrenProp = defineRule({
17072
17139
  if (node.name.name !== "children") return;
17073
17140
  context.report({
17074
17141
  node: node.name,
17075
- message: MESSAGE$32
17142
+ message: MESSAGE$36
17076
17143
  });
17077
17144
  },
17078
17145
  CallExpression(node) {
@@ -17085,7 +17152,7 @@ const noChildrenProp = defineRule({
17085
17152
  const propertyKey = property.key;
17086
17153
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17087
17154
  node: propertyKey,
17088
- message: MESSAGE$32
17155
+ message: MESSAGE$36
17089
17156
  });
17090
17157
  }
17091
17158
  }
@@ -17093,7 +17160,7 @@ const noChildrenProp = defineRule({
17093
17160
  });
17094
17161
  //#endregion
17095
17162
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
17096
- const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17163
+ const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
17097
17164
  const noCloneElement = defineRule({
17098
17165
  id: "no-clone-element",
17099
17166
  title: "cloneElement makes child props fragile",
@@ -17106,7 +17173,7 @@ const noCloneElement = defineRule({
17106
17173
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17107
17174
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17108
17175
  node: callee,
17109
- message: MESSAGE$31
17176
+ message: MESSAGE$35
17110
17177
  });
17111
17178
  return;
17112
17179
  }
@@ -17119,7 +17186,7 @@ const noCloneElement = defineRule({
17119
17186
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17120
17187
  context.report({
17121
17188
  node: callee,
17122
- message: MESSAGE$31
17189
+ message: MESSAGE$35
17123
17190
  });
17124
17191
  }
17125
17192
  } })
@@ -17168,7 +17235,7 @@ const enclosingComponentOrHookName = (node) => {
17168
17235
  };
17169
17236
  //#endregion
17170
17237
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
17171
- const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17238
+ const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
17172
17239
  const CONTEXT_MODULES = [
17173
17240
  "react",
17174
17241
  "use-context-selector",
@@ -17204,13 +17271,13 @@ const noCreateContextInRender = defineRule({
17204
17271
  if (!componentOrHookName) return;
17205
17272
  context.report({
17206
17273
  node,
17207
- message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
17274
+ message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17208
17275
  });
17209
17276
  } })
17210
17277
  });
17211
17278
  //#endregion
17212
17279
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17213
- const MESSAGE$29 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17280
+ const MESSAGE$33 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
17214
17281
  const noCreateRefInFunctionComponent = defineRule({
17215
17282
  id: "no-create-ref-in-function-component",
17216
17283
  title: "createRef in function component",
@@ -17229,7 +17296,7 @@ const noCreateRefInFunctionComponent = defineRule({
17229
17296
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17230
17297
  context.report({
17231
17298
  node,
17232
- message: MESSAGE$29
17299
+ message: MESSAGE$33
17233
17300
  });
17234
17301
  } })
17235
17302
  });
@@ -17369,7 +17436,7 @@ const noCreateStoreInRender = defineRule({
17369
17436
  });
17370
17437
  //#endregion
17371
17438
  //#region src/plugin/rules/react-builtins/no-danger.ts
17372
- const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17439
+ const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
17373
17440
  const noDanger = defineRule({
17374
17441
  id: "no-danger",
17375
17442
  title: "Raw HTML injection can run unsafe markup",
@@ -17382,7 +17449,7 @@ const noDanger = defineRule({
17382
17449
  if (!propAttribute) return;
17383
17450
  context.report({
17384
17451
  node: propAttribute.name,
17385
- message: MESSAGE$28
17452
+ message: MESSAGE$32
17386
17453
  });
17387
17454
  },
17388
17455
  CallExpression(node) {
@@ -17394,7 +17461,7 @@ const noDanger = defineRule({
17394
17461
  const propertyKey = property.key;
17395
17462
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17396
17463
  node: propertyKey,
17397
- message: MESSAGE$28
17464
+ message: MESSAGE$32
17398
17465
  });
17399
17466
  }
17400
17467
  }
@@ -17402,7 +17469,7 @@ const noDanger = defineRule({
17402
17469
  });
17403
17470
  //#endregion
17404
17471
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17405
- const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17472
+ const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17406
17473
  const isLineBreak = (child) => {
17407
17474
  if (!isNodeOfType(child, "JSXText")) return false;
17408
17475
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17472,7 +17539,7 @@ const noDangerWithChildren = defineRule({
17472
17539
  if (!hasChildrenProp && !hasNestedChildren) return;
17473
17540
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17474
17541
  node: opening,
17475
- message: MESSAGE$27
17542
+ message: MESSAGE$31
17476
17543
  });
17477
17544
  },
17478
17545
  CallExpression(node) {
@@ -17484,7 +17551,7 @@ const noDangerWithChildren = defineRule({
17484
17551
  if (!propsShape.hasDangerously) return;
17485
17552
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17486
17553
  node,
17487
- message: MESSAGE$27
17554
+ message: MESSAGE$31
17488
17555
  });
17489
17556
  }
17490
17557
  })
@@ -17649,6 +17716,37 @@ const noDefaultProps = defineRule({
17649
17716
  } })
17650
17717
  });
17651
17718
  //#endregion
17719
+ //#region src/plugin/utils/get-class-name-tokens.ts
17720
+ const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
17721
+ //#endregion
17722
+ //#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
17723
+ const renameDeprecatedToken = (token) => {
17724
+ if (token === "overflow-ellipsis") return "text-ellipsis";
17725
+ if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
17726
+ if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
17727
+ if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
17728
+ return null;
17729
+ };
17730
+ const noDeprecatedTailwindClass = defineRule({
17731
+ id: "no-deprecated-tailwind-class",
17732
+ title: "Deprecated Tailwind v4 utility",
17733
+ tags: ["design", "test-noise"],
17734
+ severity: "warn",
17735
+ requires: ["tailwind:4"],
17736
+ recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
17737
+ create: (context) => ({ JSXOpeningElement(node) {
17738
+ const classNameValue = getStringFromClassNameAttr(node);
17739
+ if (!classNameValue) return;
17740
+ for (const token of getClassNameTokens(classNameValue)) {
17741
+ const replacement = renameDeprecatedToken(token);
17742
+ if (replacement) context.report({
17743
+ node,
17744
+ message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
17745
+ });
17746
+ }
17747
+ } })
17748
+ });
17749
+ //#endregion
17652
17750
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17653
17751
  const isInitialOnlyPropName = (propName) => {
17654
17752
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -18061,7 +18159,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
18061
18159
  //#endregion
18062
18160
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
18063
18161
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
18064
- const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18162
+ const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
18065
18163
  const resolveSettings$20 = (settings) => {
18066
18164
  const reactDoctor = settings?.["react-doctor"];
18067
18165
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18080,7 +18178,7 @@ const noDidMountSetState = defineRule({
18080
18178
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18081
18179
  context.report({
18082
18180
  node: node.callee,
18083
- message: MESSAGE$26
18181
+ message: MESSAGE$30
18084
18182
  });
18085
18183
  } };
18086
18184
  }
@@ -18088,7 +18186,7 @@ const noDidMountSetState = defineRule({
18088
18186
  //#endregion
18089
18187
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18090
18188
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
18091
- const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18189
+ const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
18092
18190
  const resolveSettings$19 = (settings) => {
18093
18191
  const reactDoctor = settings?.["react-doctor"];
18094
18192
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18107,7 +18205,7 @@ const noDidUpdateSetState = defineRule({
18107
18205
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18108
18206
  context.report({
18109
18207
  node: node.callee,
18110
- message: MESSAGE$25
18208
+ message: MESSAGE$29
18111
18209
  });
18112
18210
  } };
18113
18211
  }
@@ -18130,7 +18228,7 @@ const isStateMemberExpression = (node) => {
18130
18228
  };
18131
18229
  //#endregion
18132
18230
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
18133
- const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18231
+ const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
18134
18232
  const shouldIgnoreMutation = (node) => {
18135
18233
  let isConstructor = false;
18136
18234
  let isInsideCallExpression = false;
@@ -18152,7 +18250,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18152
18250
  if (shouldIgnoreMutation(reportNode)) return;
18153
18251
  context.report({
18154
18252
  node: reportNode,
18155
- message: MESSAGE$24
18253
+ message: MESSAGE$28
18156
18254
  });
18157
18255
  };
18158
18256
  const noDirectMutationState = defineRule({
@@ -18363,7 +18461,7 @@ const noDocumentStartViewTransition = defineRule({
18363
18461
  });
18364
18462
  //#endregion
18365
18463
  //#region src/plugin/rules/js-performance/no-document-write.ts
18366
- const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18464
+ const MESSAGE$27 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18367
18465
  const WRITE_METHODS = new Set(["write", "writeln"]);
18368
18466
  const noDocumentWrite = defineRule({
18369
18467
  id: "no-document-write",
@@ -18377,7 +18475,7 @@ const noDocumentWrite = defineRule({
18377
18475
  if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18378
18476
  context.report({
18379
18477
  node,
18380
- message: MESSAGE$23
18478
+ message: MESSAGE$27
18381
18479
  });
18382
18480
  } })
18383
18481
  });
@@ -19760,7 +19858,7 @@ const ALLOWED_NAMESPACES = new Set([
19760
19858
  "ReactDOM",
19761
19859
  "ReactDom"
19762
19860
  ]);
19763
- const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19861
+ const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
19764
19862
  const noFindDomNode = defineRule({
19765
19863
  id: "no-find-dom-node",
19766
19864
  title: "findDOMNode breaks component encapsulation",
@@ -19771,7 +19869,7 @@ const noFindDomNode = defineRule({
19771
19869
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19772
19870
  context.report({
19773
19871
  node: callee,
19774
- message: MESSAGE$22
19872
+ message: MESSAGE$26
19775
19873
  });
19776
19874
  return;
19777
19875
  }
@@ -19782,7 +19880,7 @@ const noFindDomNode = defineRule({
19782
19880
  if (callee.property.name !== "findDOMNode") return;
19783
19881
  context.report({
19784
19882
  node: callee.property,
19785
- message: MESSAGE$22
19883
+ message: MESSAGE$26
19786
19884
  });
19787
19885
  }
19788
19886
  } })
@@ -19823,6 +19921,41 @@ const noFullLodashImport = defineRule({
19823
19921
  } })
19824
19922
  });
19825
19923
  //#endregion
19924
+ //#region src/plugin/rules/design/no-full-viewport-width.ts
19925
+ const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
19926
+ const WIDTH_KEYS = new Set(["width", "minWidth"]);
19927
+ const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
19928
+ const noFullViewportWidth = defineRule({
19929
+ id: "no-full-viewport-width",
19930
+ title: "Full viewport width causes overflow",
19931
+ tags: ["design", "test-noise"],
19932
+ severity: "warn",
19933
+ recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
19934
+ create: (context) => ({
19935
+ JSXAttribute(node) {
19936
+ const expression = getInlineStyleExpression(node);
19937
+ if (!expression) return;
19938
+ for (const property of expression.properties ?? []) {
19939
+ const key = getStylePropertyKey(property);
19940
+ if (!key || !WIDTH_KEYS.has(key)) continue;
19941
+ const value = getStylePropertyStringValue(property);
19942
+ if (value && value.trim().toLowerCase() === "100vw") context.report({
19943
+ node: property,
19944
+ message: MESSAGE$25
19945
+ });
19946
+ }
19947
+ },
19948
+ JSXOpeningElement(node) {
19949
+ const classNameValue = getStringFromClassNameAttr(node);
19950
+ if (!classNameValue) return;
19951
+ if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
19952
+ node,
19953
+ message: MESSAGE$25
19954
+ });
19955
+ }
19956
+ })
19957
+ });
19958
+ //#endregion
19826
19959
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19827
19960
  const noGenericHandlerNames = defineRule({
19828
19961
  id: "no-generic-handler-names",
@@ -19885,7 +20018,7 @@ const noGiantComponent = defineRule({
19885
20018
  });
19886
20019
  //#endregion
19887
20020
  //#region src/plugin/constants/style.ts
19888
- const LAYOUT_PROPERTIES = new Set([
20021
+ const LAYOUT_PROPERTIES$1 = new Set([
19889
20022
  "width",
19890
20023
  "height",
19891
20024
  "top",
@@ -19955,17 +20088,6 @@ const noGlobalCssVariableAnimation = defineRule({
19955
20088
  } })
19956
20089
  });
19957
20090
  //#endregion
19958
- //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
19959
- const getStringFromClassNameAttr = (node) => {
19960
- if (!isNodeOfType(node, "JSXOpeningElement")) return null;
19961
- const classAttr = findJsxAttribute(node.attributes ?? [], "className");
19962
- if (!classAttr?.value) return null;
19963
- if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
19964
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
19965
- 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;
19966
- return null;
19967
- };
19968
- //#endregion
19969
20091
  //#region src/plugin/rules/design/no-gradient-text.ts
19970
20092
  const noGradientText = defineRule({
19971
20093
  id: "no-gradient-text",
@@ -20024,7 +20146,7 @@ const noGrayOnColoredBackground = defineRule({
20024
20146
  });
20025
20147
  //#endregion
20026
20148
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
20027
- const MESSAGE$21 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20149
+ const MESSAGE$24 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
20028
20150
  const noImgLazyWithHighFetchpriority = defineRule({
20029
20151
  id: "no-img-lazy-with-high-fetchpriority",
20030
20152
  title: "Lazy image with high fetchPriority",
@@ -20038,7 +20160,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
20038
20160
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
20039
20161
  context.report({
20040
20162
  node: node.name,
20041
- message: MESSAGE$21
20163
+ message: MESSAGE$24
20042
20164
  });
20043
20165
  } })
20044
20166
  });
@@ -20273,7 +20395,7 @@ const noIsMounted = defineRule({
20273
20395
  });
20274
20396
  //#endregion
20275
20397
  //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20276
- const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20398
+ const MESSAGE$23 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
20277
20399
  const isJsonMethodCall = (node, method) => {
20278
20400
  if (!isNodeOfType(node, "CallExpression")) return false;
20279
20401
  const callee = node.callee;
@@ -20290,13 +20412,13 @@ const noJsonParseStringifyClone = defineRule({
20290
20412
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20291
20413
  context.report({
20292
20414
  node,
20293
- message: MESSAGE$20
20415
+ message: MESSAGE$23
20294
20416
  });
20295
20417
  } })
20296
20418
  });
20297
20419
  //#endregion
20298
20420
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
20299
- const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20421
+ const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
20300
20422
  const isJsxElementTypeReference = (node) => {
20301
20423
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20302
20424
  const typeName = node.typeName;
@@ -20313,7 +20435,7 @@ const checkReturnType = (context, returnType) => {
20313
20435
  if (!typeAnnotation) return;
20314
20436
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20315
20437
  node: typeAnnotation,
20316
- message: MESSAGE$19
20438
+ message: MESSAGE$22
20317
20439
  });
20318
20440
  };
20319
20441
  const noJsxElementType = defineRule({
@@ -20423,7 +20545,7 @@ const noLayoutPropertyAnimation = defineRule({
20423
20545
  let propertyName = null;
20424
20546
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20425
20547
  else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
20426
- if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20548
+ if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
20427
20549
  node: property,
20428
20550
  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`
20429
20551
  });
@@ -20613,6 +20735,134 @@ const noLongTransitionDuration = defineRule({
20613
20735
  } })
20614
20736
  });
20615
20737
  //#endregion
20738
+ //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
20739
+ const getStylePropertyNumberValue = (property) => {
20740
+ if (!isNodeOfType(property, "Property")) return null;
20741
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
20742
+ if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
20743
+ return null;
20744
+ };
20745
+ //#endregion
20746
+ //#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
20747
+ const linearizeChannel = (channel) => {
20748
+ const normalized = channel / 255;
20749
+ return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
20750
+ };
20751
+ const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
20752
+ const getWcagContrastRatio = (foreground, background) => {
20753
+ const foregroundLuminance = relativeLuminance(foreground);
20754
+ const backgroundLuminance = relativeLuminance(background);
20755
+ const lighter = Math.max(foregroundLuminance, backgroundLuminance);
20756
+ const darker = Math.min(foregroundLuminance, backgroundLuminance);
20757
+ return (lighter + .05) / (darker + .05);
20758
+ };
20759
+ //#endregion
20760
+ //#region src/plugin/rules/design/no-low-contrast-inline-style.ts
20761
+ const UNRESOLVABLE = new Set([
20762
+ "transparent",
20763
+ "currentcolor",
20764
+ "inherit",
20765
+ "initial",
20766
+ "unset",
20767
+ "revert",
20768
+ "none"
20769
+ ]);
20770
+ const resolveOpaqueColor = (raw) => {
20771
+ const value = raw.trim().toLowerCase();
20772
+ if (UNRESOLVABLE.has(value)) return null;
20773
+ if (value === "white") return {
20774
+ red: 255,
20775
+ green: 255,
20776
+ blue: 255
20777
+ };
20778
+ if (value === "black") return {
20779
+ red: 0,
20780
+ green: 0,
20781
+ blue: 0
20782
+ };
20783
+ if (value.startsWith("var(")) return null;
20784
+ if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
20785
+ if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
20786
+ if (value.startsWith("rgb")) {
20787
+ const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
20788
+ if (inner.includes("/") || inner.split(",").length >= 4) return null;
20789
+ }
20790
+ return parseColorToRgb(value);
20791
+ };
20792
+ const toPx = (property) => {
20793
+ const numberValue = getStylePropertyNumberValue(property);
20794
+ if (numberValue !== null) return numberValue;
20795
+ const stringValue = getStylePropertyStringValue(property);
20796
+ if (stringValue === null) return null;
20797
+ const pxMatch = stringValue.match(/^([\d.]+)px$/);
20798
+ if (pxMatch) return parseFloat(pxMatch[1]);
20799
+ const remMatch = stringValue.match(/^([\d.]+)rem$/);
20800
+ if (remMatch) return parseFloat(remMatch[1]) * 16;
20801
+ return null;
20802
+ };
20803
+ const isBoldWeight = (property) => {
20804
+ const numberValue = getStylePropertyNumberValue(property);
20805
+ if (numberValue !== null) return numberValue >= 700;
20806
+ const stringValue = getStylePropertyStringValue(property);
20807
+ if (stringValue === null) return false;
20808
+ if (stringValue === "bold" || stringValue === "bolder") return true;
20809
+ const numericWeight = Number(stringValue);
20810
+ return Number.isFinite(numericWeight) && numericWeight >= 700;
20811
+ };
20812
+ const noLowContrastInlineStyle = defineRule({
20813
+ id: "no-low-contrast-inline-style",
20814
+ title: "Low-contrast text in inline style",
20815
+ tags: ["test-noise"],
20816
+ severity: "warn",
20817
+ category: "Accessibility",
20818
+ recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
20819
+ create: (context) => ({ JSXAttribute(node) {
20820
+ const expression = getInlineStyleExpression(node);
20821
+ if (!expression) return;
20822
+ const properties = expression.properties ?? [];
20823
+ if (properties.some((property) => property.type === "SpreadElement")) return;
20824
+ let foreground = null;
20825
+ let backgroundColorRaw = null;
20826
+ let backgroundShorthandRaw = null;
20827
+ let backgroundIsUnknown = false;
20828
+ let fontSizePx = null;
20829
+ let isBold = false;
20830
+ for (const property of properties) {
20831
+ const key = getStylePropertyKey(property);
20832
+ if (!key) continue;
20833
+ if (key === "backgroundImage") {
20834
+ backgroundIsUnknown = true;
20835
+ continue;
20836
+ }
20837
+ if (key === "fontSize" && property.type === "Property") {
20838
+ fontSizePx = toPx(property);
20839
+ continue;
20840
+ }
20841
+ if (key === "fontWeight" && property.type === "Property") {
20842
+ isBold = isBoldWeight(property);
20843
+ continue;
20844
+ }
20845
+ const stringValue = getStylePropertyStringValue(property);
20846
+ if (key === "color") {
20847
+ if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
20848
+ } else if (key === "backgroundColor") backgroundColorRaw = stringValue;
20849
+ else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
20850
+ else backgroundShorthandRaw = stringValue;
20851
+ }
20852
+ if (backgroundIsUnknown) return;
20853
+ if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
20854
+ const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
20855
+ const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
20856
+ if (!foreground || !background) return;
20857
+ const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
20858
+ const ratio = getWcagContrastRatio(foreground, background);
20859
+ if (ratio < threshold) context.report({
20860
+ node,
20861
+ message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
20862
+ });
20863
+ } })
20864
+ });
20865
+ //#endregion
20616
20866
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20617
20867
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20618
20868
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -20768,7 +21018,7 @@ const noMoment = defineRule({
20768
21018
  });
20769
21019
  //#endregion
20770
21020
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20771
- const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
21021
+ const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
20772
21022
  const resolveSettings$16 = (settings) => {
20773
21023
  const reactDoctor = settings?.["react-doctor"];
20774
21024
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21090,7 +21340,7 @@ const noMultiComp = defineRule({
21090
21340
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21091
21341
  for (const component of flagged.slice(1)) context.report({
21092
21342
  node: component.reportNode,
21093
- message: MESSAGE$18
21343
+ message: MESSAGE$21
21094
21344
  });
21095
21345
  } };
21096
21346
  }
@@ -21258,7 +21508,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21258
21508
  };
21259
21509
  //#endregion
21260
21510
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
21261
- const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
21511
+ const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
21262
21512
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21263
21513
  "copyWithin",
21264
21514
  "fill",
@@ -21468,7 +21718,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21468
21718
  reportedNodes.add(options.crossFileConsumerCallSite);
21469
21719
  context.report({
21470
21720
  node: options.crossFileConsumerCallSite,
21471
- message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21721
+ message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21472
21722
  });
21473
21723
  return;
21474
21724
  }
@@ -21477,7 +21727,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21477
21727
  reportedNodes.add(mutation.node);
21478
21728
  context.report({
21479
21729
  node: mutation.node,
21480
- message: MESSAGE$17
21730
+ message: MESSAGE$20
21481
21731
  });
21482
21732
  }
21483
21733
  };
@@ -21749,7 +21999,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21749
21999
  });
21750
22000
  //#endregion
21751
22001
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21752
- const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
22002
+ const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
21753
22003
  const resolveSettings$14 = (settings) => {
21754
22004
  const reactDoctor = settings?.["react-doctor"];
21755
22005
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21777,7 +22027,7 @@ const noNoninteractiveTabindex = defineRule({
21777
22027
  if (numeric === null) {
21778
22028
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21779
22029
  node: tabIndex,
21780
- message: MESSAGE$16
22030
+ message: MESSAGE$19
21781
22031
  });
21782
22032
  return;
21783
22033
  }
@@ -21790,7 +22040,7 @@ const noNoninteractiveTabindex = defineRule({
21790
22040
  if (!roleAttribute) {
21791
22041
  context.report({
21792
22042
  node: tabIndex,
21793
- message: MESSAGE$16
22043
+ message: MESSAGE$19
21794
22044
  });
21795
22045
  return;
21796
22046
  }
@@ -21804,20 +22054,12 @@ const noNoninteractiveTabindex = defineRule({
21804
22054
  }
21805
22055
  context.report({
21806
22056
  node: tabIndex,
21807
- message: MESSAGE$16
22057
+ message: MESSAGE$19
21808
22058
  });
21809
22059
  } };
21810
22060
  }
21811
22061
  });
21812
22062
  //#endregion
21813
- //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21814
- const getStylePropertyNumberValue = (property) => {
21815
- if (!isNodeOfType(property, "Property")) return null;
21816
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21817
- if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21818
- return null;
21819
- };
21820
- //#endregion
21821
22063
  //#region src/plugin/rules/design/no-outline-none.ts
21822
22064
  const noOutlineNone = defineRule({
21823
22065
  id: "no-outline-none",
@@ -22495,7 +22737,7 @@ const noRandomKey = defineRule({
22495
22737
  });
22496
22738
  //#endregion
22497
22739
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22498
- const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22740
+ const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
22499
22741
  const isChildrenIdentifier = (node, contextNode) => {
22500
22742
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22501
22743
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22521,13 +22763,13 @@ const noReactChildren = defineRule({
22521
22763
  if (isChildrenIdentifier(memberObject, node)) {
22522
22764
  context.report({
22523
22765
  node: calleeOuter,
22524
- message: MESSAGE$15
22766
+ message: MESSAGE$18
22525
22767
  });
22526
22768
  return;
22527
22769
  }
22528
22770
  if (isReactNamespaceMember(memberObject, node)) context.report({
22529
22771
  node: calleeOuter,
22530
- message: MESSAGE$15
22772
+ message: MESSAGE$18
22531
22773
  });
22532
22774
  } })
22533
22775
  });
@@ -22638,6 +22880,86 @@ const noReact19DeprecatedApis = defineRule({
22638
22880
  })
22639
22881
  });
22640
22882
  //#endregion
22883
+ //#region src/plugin/rules/design/no-redundant-display-class.ts
22884
+ const BLOCK_DEFAULT_TAGS = new Set([
22885
+ "div",
22886
+ "p",
22887
+ "section",
22888
+ "article",
22889
+ "main",
22890
+ "header",
22891
+ "footer",
22892
+ "nav",
22893
+ "aside",
22894
+ "figure",
22895
+ "figcaption",
22896
+ "blockquote",
22897
+ "form",
22898
+ "fieldset",
22899
+ "address",
22900
+ "pre",
22901
+ "ul",
22902
+ "ol",
22903
+ "dl",
22904
+ "dt",
22905
+ "dd",
22906
+ "h1",
22907
+ "h2",
22908
+ "h3",
22909
+ "h4",
22910
+ "h5",
22911
+ "h6"
22912
+ ]);
22913
+ const INLINE_DEFAULT_TAGS = new Set([
22914
+ "span",
22915
+ "a",
22916
+ "b",
22917
+ "i",
22918
+ "em",
22919
+ "strong",
22920
+ "small",
22921
+ "code",
22922
+ "abbr",
22923
+ "cite",
22924
+ "label",
22925
+ "mark",
22926
+ "q",
22927
+ "s",
22928
+ "u",
22929
+ "sub",
22930
+ "sup",
22931
+ "kbd",
22932
+ "samp",
22933
+ "var",
22934
+ "time"
22935
+ ]);
22936
+ const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
22937
+ const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
22938
+ const noRedundantDisplayClass = defineRule({
22939
+ id: "no-redundant-display-class",
22940
+ title: "Redundant display utility",
22941
+ tags: ["design", "test-noise"],
22942
+ severity: "warn",
22943
+ recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
22944
+ create: (context) => ({ JSXOpeningElement(node) {
22945
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
22946
+ const tagName = node.name.name;
22947
+ const classNameValue = getStringFromClassNameAttr(node);
22948
+ if (!classNameValue) return;
22949
+ if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
22950
+ context.report({
22951
+ node,
22952
+ message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
22953
+ });
22954
+ return;
22955
+ }
22956
+ if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
22957
+ node,
22958
+ message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
22959
+ });
22960
+ } })
22961
+ });
22962
+ //#endregion
22641
22963
  //#region src/plugin/constants/aria-element-roles.ts
22642
22964
  const ELEMENT_ROLE_PAIRS = [
22643
22965
  ["a", "link"],
@@ -22850,7 +23172,7 @@ const noRenderPropChildren = defineRule({
22850
23172
  });
22851
23173
  //#endregion
22852
23174
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22853
- const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
23175
+ const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
22854
23176
  const isReactDomRenderCall = (node) => {
22855
23177
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22856
23178
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22874,7 +23196,7 @@ const noRenderReturnValue = defineRule({
22874
23196
  if (!isUsedAsReturnValue(node.parent)) return;
22875
23197
  context.report({
22876
23198
  node: node.callee,
22877
- message: MESSAGE$14
23199
+ message: MESSAGE$17
22878
23200
  });
22879
23201
  } })
22880
23202
  });
@@ -23572,7 +23894,7 @@ const getParentComponent = (node) => {
23572
23894
  };
23573
23895
  //#endregion
23574
23896
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23575
- const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23897
+ const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
23576
23898
  const noSetState = defineRule({
23577
23899
  id: "no-set-state",
23578
23900
  title: "Local class state forbidden",
@@ -23587,7 +23909,7 @@ const noSetState = defineRule({
23587
23909
  if (!getParentComponent(node)) return;
23588
23910
  context.report({
23589
23911
  node: node.callee,
23590
- message: MESSAGE$13
23912
+ message: MESSAGE$16
23591
23913
  });
23592
23914
  } })
23593
23915
  });
@@ -23749,7 +24071,7 @@ const isAbstractRole = (openingElement, settings) => {
23749
24071
  };
23750
24072
  //#endregion
23751
24073
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23752
- const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
24074
+ const MESSAGE$15 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
23753
24075
  const DEFAULT_HANDLERS = [
23754
24076
  "onClick",
23755
24077
  "onMouseDown",
@@ -23809,7 +24131,7 @@ const noStaticElementInteractions = defineRule({
23809
24131
  if (!roleAttribute || !roleAttribute.value) {
23810
24132
  context.report({
23811
24133
  node: node.name,
23812
- message: MESSAGE$12
24134
+ message: MESSAGE$15
23813
24135
  });
23814
24136
  return;
23815
24137
  }
@@ -23819,14 +24141,14 @@ const noStaticElementInteractions = defineRule({
23819
24141
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23820
24142
  context.report({
23821
24143
  node: node.name,
23822
- message: MESSAGE$12
24144
+ message: MESSAGE$15
23823
24145
  });
23824
24146
  return;
23825
24147
  }
23826
24148
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23827
24149
  context.report({
23828
24150
  node: node.name,
23829
- message: MESSAGE$12
24151
+ message: MESSAGE$15
23830
24152
  });
23831
24153
  } };
23832
24154
  }
@@ -23929,8 +24251,43 @@ const noStringRefs = defineRule({
23929
24251
  }
23930
24252
  });
23931
24253
  //#endregion
24254
+ //#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
24255
+ const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
24256
+ if (token.includes(":")) return false;
24257
+ if (!token.startsWith(prefix)) return false;
24258
+ const value = token.slice(prefix.length);
24259
+ if (value === "" || value === "current") return false;
24260
+ if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
24261
+ return true;
24262
+ });
24263
+ const isCurrentColor = (attribute) => {
24264
+ const value = getJsxPropStringValue(attribute);
24265
+ return value !== null && value.trim().toLowerCase() === "currentcolor";
24266
+ };
24267
+ const noSvgCurrentcolorWithFillClass = defineRule({
24268
+ id: "no-svg-currentcolor-with-fill-class",
24269
+ title: "currentColor fights a fill/stroke class",
24270
+ tags: ["design", "test-noise"],
24271
+ severity: "warn",
24272
+ recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
24273
+ create: (context) => ({ JSXOpeningElement(node) {
24274
+ const classNameValue = getStringFromClassNameAttr(node);
24275
+ if (!classNameValue) return;
24276
+ for (const paint of ["fill", "stroke"]) {
24277
+ const attribute = findJsxAttribute(node.attributes, paint);
24278
+ if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
24279
+ context.report({
24280
+ node: attribute,
24281
+ message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
24282
+ });
24283
+ return;
24284
+ }
24285
+ }
24286
+ } })
24287
+ });
24288
+ //#endregion
23932
24289
  //#region src/plugin/rules/js-performance/no-sync-xhr.ts
23933
- const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
24290
+ const MESSAGE$14 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
23934
24291
  const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
23935
24292
  const noSyncXhr = defineRule({
23936
24293
  id: "no-sync-xhr",
@@ -23945,13 +24302,103 @@ const noSyncXhr = defineRule({
23945
24302
  if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
23946
24303
  context.report({
23947
24304
  node,
23948
- message: MESSAGE$11
24305
+ message: MESSAGE$14
24306
+ });
24307
+ } })
24308
+ });
24309
+ //#endregion
24310
+ //#region src/plugin/rules/design/no-tailwind-layout-transition.ts
24311
+ const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
24312
+ const LAYOUT_PROPERTIES = new Set([
24313
+ "width",
24314
+ "height",
24315
+ "min-width",
24316
+ "max-width",
24317
+ "min-height",
24318
+ "max-height",
24319
+ "top",
24320
+ "left",
24321
+ "right",
24322
+ "bottom",
24323
+ "inset",
24324
+ "inset-block",
24325
+ "inset-inline",
24326
+ "margin",
24327
+ "margin-top",
24328
+ "margin-right",
24329
+ "margin-bottom",
24330
+ "margin-left",
24331
+ "margin-block",
24332
+ "margin-inline",
24333
+ "padding",
24334
+ "padding-top",
24335
+ "padding-right",
24336
+ "padding-bottom",
24337
+ "padding-left",
24338
+ "padding-block",
24339
+ "padding-inline"
24340
+ ]);
24341
+ const noTailwindLayoutTransition = defineRule({
24342
+ id: "no-tailwind-layout-transition",
24343
+ title: "Animating a layout property",
24344
+ tags: ["design", "test-noise"],
24345
+ severity: "warn",
24346
+ category: "Performance",
24347
+ recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
24348
+ create: (context) => ({ JSXOpeningElement(node) {
24349
+ const classNameValue = getStringFromClassNameAttr(node);
24350
+ if (!classNameValue) return;
24351
+ for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
24352
+ const animatedProperties = transitionMatch[1];
24353
+ const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
24354
+ if (layoutProperty) context.report({
24355
+ node,
24356
+ message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
24357
+ });
24358
+ }
24359
+ } })
24360
+ });
24361
+ //#endregion
24362
+ //#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
24363
+ const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
24364
+ const targetIsBlank = (attribute) => {
24365
+ const stringValue = getJsxPropStringValue(attribute);
24366
+ if (stringValue !== null) return stringValue === "_blank";
24367
+ const value = attribute.value;
24368
+ if (value && isNodeOfType(value, "JSXExpressionContainer")) {
24369
+ const expression = value.expression;
24370
+ if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
24371
+ }
24372
+ return false;
24373
+ };
24374
+ const noTargetBlankWithoutRel = defineRule({
24375
+ id: "no-target-blank-without-rel",
24376
+ title: "target=_blank without rel=noopener",
24377
+ severity: "warn",
24378
+ recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
24379
+ create: (context) => ({ JSXOpeningElement(node) {
24380
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24381
+ const tagName = node.name.name;
24382
+ if (tagName !== "a" && tagName !== "area") return;
24383
+ if (hasJsxSpreadAttribute(node.attributes)) return;
24384
+ const targetAttribute = findJsxAttribute(node.attributes, "target");
24385
+ if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
24386
+ const relAttribute = findJsxAttribute(node.attributes, "rel");
24387
+ if (relAttribute) {
24388
+ const relValue = getJsxPropStringValue(relAttribute);
24389
+ if (relValue === null) return;
24390
+ const tokens = relValue.toLowerCase().split(/\s+/);
24391
+ if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
24392
+ }
24393
+ context.report({
24394
+ node: node.name,
24395
+ message: MESSAGE$13
23949
24396
  });
23950
24397
  } })
23951
24398
  });
23952
24399
  //#endregion
23953
24400
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23954
- const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
24401
+ const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
23955
24402
  const isInsideClassMethod = (node, customClassFactoryNames) => {
23956
24403
  let ancestor = node.parent;
23957
24404
  while (ancestor) {
@@ -24020,7 +24467,7 @@ const noThisInSfc = defineRule({
24020
24467
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
24021
24468
  context.report({
24022
24469
  node,
24023
- message: MESSAGE$10
24470
+ message: MESSAGE$12
24024
24471
  });
24025
24472
  } };
24026
24473
  }
@@ -24058,26 +24505,39 @@ const noTinyText = defineRule({
24058
24505
  });
24059
24506
  //#endregion
24060
24507
  //#region src/plugin/rules/performance/no-transition-all.ts
24508
+ const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
24509
+ const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
24061
24510
  const noTransitionAll = defineRule({
24062
24511
  id: "no-transition-all",
24063
24512
  title: "transition: all animates everything",
24064
24513
  tags: ["test-noise"],
24065
24514
  severity: "warn",
24066
24515
  recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
24067
- create: (context) => ({ JSXAttribute(node) {
24068
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24069
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24070
- const expression = node.value.expression;
24071
- if (!isNodeOfType(expression, "ObjectExpression")) return;
24072
- for (const property of expression.properties ?? []) {
24073
- if (!isNodeOfType(property, "Property")) continue;
24074
- if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
24075
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
24076
- node: property,
24077
- message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24516
+ create: (context) => ({
24517
+ JSXAttribute(node) {
24518
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
24519
+ if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
24520
+ const expression = node.value.expression;
24521
+ if (!isNodeOfType(expression, "ObjectExpression")) return;
24522
+ for (const property of expression.properties ?? []) {
24523
+ if (!isNodeOfType(property, "Property")) continue;
24524
+ const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
24525
+ if (key !== "transition" && key !== "transitionProperty") continue;
24526
+ if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
24527
+ node: property,
24528
+ message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
24529
+ });
24530
+ }
24531
+ },
24532
+ JSXOpeningElement(node) {
24533
+ const classNameValue = getStringFromClassNameAttr(node);
24534
+ if (!classNameValue) return;
24535
+ if (hasTransitionAllClass(classNameValue)) context.report({
24536
+ node,
24537
+ message: TAILWIND_MESSAGE
24078
24538
  });
24079
24539
  }
24080
- } })
24540
+ })
24081
24541
  });
24082
24542
  //#endregion
24083
24543
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -24121,7 +24581,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
24121
24581
  }
24122
24582
  return stateNames;
24123
24583
  };
24124
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
24125
24584
  const noUncontrolledInput = defineRule({
24126
24585
  id: "no-uncontrolled-input",
24127
24586
  title: "Uncontrolled input value",
@@ -24225,6 +24684,38 @@ const noUnescapedEntities = defineRule({
24225
24684
  } })
24226
24685
  });
24227
24686
  //#endregion
24687
+ //#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
24688
+ const UNINFORMATIVE_LABELS = new Set([
24689
+ "icon",
24690
+ "button",
24691
+ "image",
24692
+ "img",
24693
+ "link",
24694
+ "graphic",
24695
+ "svg",
24696
+ "picture",
24697
+ "element",
24698
+ "field",
24699
+ "input"
24700
+ ]);
24701
+ const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
24702
+ const noUninformativeAriaLabel = defineRule({
24703
+ id: "no-uninformative-aria-label",
24704
+ title: "Uninformative aria-label",
24705
+ severity: "warn",
24706
+ recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
24707
+ create: (context) => ({ JSXOpeningElement(node) {
24708
+ const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
24709
+ if (!ariaLabel) return;
24710
+ const labelValue = getJsxPropStringValue(ariaLabel);
24711
+ if (labelValue === null) return;
24712
+ if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
24713
+ node: ariaLabel,
24714
+ message: MESSAGE$11
24715
+ });
24716
+ } })
24717
+ });
24718
+ //#endregion
24228
24719
  //#region src/plugin/constants/dom-aria-properties.ts
24229
24720
  const ARIA_PROPERTY_NAMES = new Set([
24230
24721
  "activedescendant",
@@ -25696,7 +26187,7 @@ const noWideLetterSpacing = defineRule({
25696
26187
  //#endregion
25697
26188
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
25698
26189
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
25699
- const MESSAGE$9 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
26190
+ const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
25700
26191
  const resolveSettings$7 = (settings) => {
25701
26192
  const reactDoctor = settings?.["react-doctor"];
25702
26193
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -25730,7 +26221,7 @@ const noWillUpdateSetState = defineRule({
25730
26221
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25731
26222
  context.report({
25732
26223
  node: node.callee,
25733
- message: MESSAGE$9
26224
+ message: MESSAGE$10
25734
26225
  });
25735
26226
  } };
25736
26227
  }
@@ -26608,7 +27099,7 @@ const preactNoRenderArguments = defineRule({
26608
27099
  });
26609
27100
  //#endregion
26610
27101
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
26611
- 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.";
27102
+ const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
26612
27103
  const preactPreferOndblclick = defineRule({
26613
27104
  id: "preact-prefer-ondblclick",
26614
27105
  title: "onDoubleClick instead of onDblClick",
@@ -26623,7 +27114,7 @@ const preactPreferOndblclick = defineRule({
26623
27114
  if (!onDoubleClickAttribute) return;
26624
27115
  context.report({
26625
27116
  node: onDoubleClickAttribute,
26626
- message: MESSAGE$8
27117
+ message: MESSAGE$9
26627
27118
  });
26628
27119
  } })
26629
27120
  });
@@ -26663,6 +27154,42 @@ const preactPreferOninput = defineRule({
26663
27154
  } })
26664
27155
  });
26665
27156
  //#endregion
27157
+ //#region src/plugin/rules/design/prefer-dvh-over-vh.ts
27158
+ const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
27159
+ const HEIGHT_KEYS = new Set(["height", "minHeight"]);
27160
+ const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
27161
+ const preferDvhOverVh = defineRule({
27162
+ id: "prefer-dvh-over-vh",
27163
+ title: "Use dvh instead of vh for full height",
27164
+ tags: ["design", "test-noise"],
27165
+ severity: "warn",
27166
+ requires: ["tailwind:3.4"],
27167
+ recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
27168
+ create: (context) => ({
27169
+ JSXAttribute(node) {
27170
+ const expression = getInlineStyleExpression(node);
27171
+ if (!expression) return;
27172
+ for (const property of expression.properties ?? []) {
27173
+ const key = getStylePropertyKey(property);
27174
+ if (!key || !HEIGHT_KEYS.has(key)) continue;
27175
+ const value = getStylePropertyStringValue(property);
27176
+ if (value && value.trim().toLowerCase() === "100vh") context.report({
27177
+ node: property,
27178
+ message: MESSAGE$8
27179
+ });
27180
+ }
27181
+ },
27182
+ JSXOpeningElement(node) {
27183
+ const classNameValue = getStringFromClassNameAttr(node);
27184
+ if (!classNameValue) return;
27185
+ if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
27186
+ node,
27187
+ message: MESSAGE$8
27188
+ });
27189
+ }
27190
+ })
27191
+ });
27192
+ //#endregion
26666
27193
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
26667
27194
  const preferDynamicImport = defineRule({
26668
27195
  id: "prefer-dynamic-import",
@@ -27254,6 +27781,26 @@ const preferTagOverRole = defineRule({
27254
27781
  } })
27255
27782
  });
27256
27783
  //#endregion
27784
+ //#region src/plugin/rules/design/prefer-truncate-shorthand.ts
27785
+ const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
27786
+ const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
27787
+ const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
27788
+ const preferTruncateShorthand = defineRule({
27789
+ id: "prefer-truncate-shorthand",
27790
+ title: "Use truncate shorthand",
27791
+ tags: ["design", "test-noise"],
27792
+ severity: "warn",
27793
+ recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
27794
+ create: (context) => ({ JSXOpeningElement(node) {
27795
+ const classNameValue = getStringFromClassNameAttr(node);
27796
+ if (!classNameValue) return;
27797
+ if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
27798
+ node,
27799
+ message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
27800
+ });
27801
+ } })
27802
+ });
27803
+ //#endregion
27257
27804
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27258
27805
  const collectFunctionTypedLocalBindings = (componentBody) => {
27259
27806
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -35525,13 +36072,7 @@ const serverNoMutableModuleState = defineRule({
35525
36072
  const collectDeclaredNames = (declaration) => {
35526
36073
  const names = /* @__PURE__ */ new Set();
35527
36074
  if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
35528
- for (const declarator of declaration.declarations ?? []) if (isNodeOfType(declarator.id, "Identifier")) names.add(declarator.id.name);
35529
- else if (isNodeOfType(declarator.id, "ObjectPattern")) {
35530
- for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
35531
- else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
35532
- } else if (isNodeOfType(declarator.id, "ArrayPattern")) {
35533
- for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
35534
- }
36075
+ for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
35535
36076
  return names;
35536
36077
  };
35537
36078
  const declarationStartsWithAwait = (declaration) => {
@@ -35541,11 +36082,15 @@ const declarationStartsWithAwait = (declaration) => {
35541
36082
  };
35542
36083
  const declarationReadsAnyName = (declaration, names) => {
35543
36084
  if (names.size === 0) return false;
36085
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
35544
36086
  let didRead = false;
35545
- walkAst(declaration, (child) => {
35546
- if (didRead) return;
35547
- if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
35548
- });
36087
+ for (const declarator of declaration.declarations ?? []) {
36088
+ if (!declarator.init) continue;
36089
+ walkAst(declarator.init, (child) => {
36090
+ if (didRead) return;
36091
+ if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
36092
+ });
36093
+ }
35549
36094
  return didRead;
35550
36095
  };
35551
36096
  const serverSequentialIndependentAwait = defineRule({
@@ -36805,7 +37350,7 @@ const urlPrefilledPrivilegedAction = defineRule({
36805
37350
  recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
36806
37351
  scan: scanByPattern({
36807
37352
  shouldScan: (file) => isClientSourcePath(file.relativePath),
36808
- 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,
37353
+ 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,
36809
37354
  message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
36810
37355
  })
36811
37356
  });
@@ -38930,6 +39475,17 @@ const reactDoctorRules = [
38930
39475
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
38931
39476
  }
38932
39477
  },
39478
+ {
39479
+ key: "react-doctor/no-arbitrary-px-font-size",
39480
+ id: "no-arbitrary-px-font-size",
39481
+ source: "react-doctor",
39482
+ originallyExternal: false,
39483
+ rule: {
39484
+ ...noArbitraryPxFontSize,
39485
+ framework: "global",
39486
+ category: "Accessibility"
39487
+ }
39488
+ },
38933
39489
  {
38934
39490
  key: "react-doctor/no-aria-hidden-on-focusable",
38935
39491
  id: "no-aria-hidden-on-focusable",
@@ -38989,6 +39545,18 @@ const reactDoctorRules = [
38989
39545
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
38990
39546
  }
38991
39547
  },
39548
+ {
39549
+ key: "react-doctor/no-autoplay-without-muted",
39550
+ id: "no-autoplay-without-muted",
39551
+ source: "react-doctor",
39552
+ originallyExternal: false,
39553
+ rule: {
39554
+ ...noAutoplayWithoutMuted,
39555
+ framework: "global",
39556
+ category: "Accessibility",
39557
+ requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39558
+ }
39559
+ },
38992
39560
  {
38993
39561
  key: "react-doctor/no-barrel-import",
38994
39562
  id: "no-barrel-import",
@@ -39142,6 +39710,17 @@ const reactDoctorRules = [
39142
39710
  category: "Maintainability"
39143
39711
  }
39144
39712
  },
39713
+ {
39714
+ key: "react-doctor/no-deprecated-tailwind-class",
39715
+ id: "no-deprecated-tailwind-class",
39716
+ source: "react-doctor",
39717
+ originallyExternal: false,
39718
+ rule: {
39719
+ ...noDeprecatedTailwindClass,
39720
+ framework: "global",
39721
+ category: "Maintainability"
39722
+ }
39723
+ },
39145
39724
  {
39146
39725
  key: "react-doctor/no-derived-state",
39147
39726
  id: "no-derived-state",
@@ -39413,6 +39992,17 @@ const reactDoctorRules = [
39413
39992
  category: "Performance"
39414
39993
  }
39415
39994
  },
39995
+ {
39996
+ key: "react-doctor/no-full-viewport-width",
39997
+ id: "no-full-viewport-width",
39998
+ source: "react-doctor",
39999
+ originallyExternal: false,
40000
+ rule: {
40001
+ ...noFullViewportWidth,
40002
+ framework: "global",
40003
+ category: "Maintainability"
40004
+ }
40005
+ },
39416
40006
  {
39417
40007
  key: "react-doctor/no-generic-handler-names",
39418
40008
  id: "no-generic-handler-names",
@@ -39652,6 +40242,17 @@ const reactDoctorRules = [
39652
40242
  category: "Performance"
39653
40243
  }
39654
40244
  },
40245
+ {
40246
+ key: "react-doctor/no-low-contrast-inline-style",
40247
+ id: "no-low-contrast-inline-style",
40248
+ source: "react-doctor",
40249
+ originallyExternal: false,
40250
+ rule: {
40251
+ ...noLowContrastInlineStyle,
40252
+ framework: "global",
40253
+ category: "Accessibility"
40254
+ }
40255
+ },
39655
40256
  {
39656
40257
  key: "react-doctor/no-many-boolean-props",
39657
40258
  id: "no-many-boolean-props",
@@ -39929,6 +40530,17 @@ const reactDoctorRules = [
39929
40530
  category: "Maintainability"
39930
40531
  }
39931
40532
  },
40533
+ {
40534
+ key: "react-doctor/no-redundant-display-class",
40535
+ id: "no-redundant-display-class",
40536
+ source: "react-doctor",
40537
+ originallyExternal: false,
40538
+ rule: {
40539
+ ...noRedundantDisplayClass,
40540
+ framework: "global",
40541
+ category: "Maintainability"
40542
+ }
40543
+ },
39932
40544
  {
39933
40545
  key: "react-doctor/no-redundant-roles",
39934
40546
  id: "no-redundant-roles",
@@ -40105,6 +40717,17 @@ const reactDoctorRules = [
40105
40717
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
40106
40718
  }
40107
40719
  },
40720
+ {
40721
+ key: "react-doctor/no-svg-currentcolor-with-fill-class",
40722
+ id: "no-svg-currentcolor-with-fill-class",
40723
+ source: "react-doctor",
40724
+ originallyExternal: false,
40725
+ rule: {
40726
+ ...noSvgCurrentcolorWithFillClass,
40727
+ framework: "global",
40728
+ category: "Maintainability"
40729
+ }
40730
+ },
40108
40731
  {
40109
40732
  key: "react-doctor/no-sync-xhr",
40110
40733
  id: "no-sync-xhr",
@@ -40116,6 +40739,29 @@ const reactDoctorRules = [
40116
40739
  category: "Performance"
40117
40740
  }
40118
40741
  },
40742
+ {
40743
+ key: "react-doctor/no-tailwind-layout-transition",
40744
+ id: "no-tailwind-layout-transition",
40745
+ source: "react-doctor",
40746
+ originallyExternal: false,
40747
+ rule: {
40748
+ ...noTailwindLayoutTransition,
40749
+ framework: "global",
40750
+ category: "Performance"
40751
+ }
40752
+ },
40753
+ {
40754
+ key: "react-doctor/no-target-blank-without-rel",
40755
+ id: "no-target-blank-without-rel",
40756
+ source: "react-doctor",
40757
+ originallyExternal: false,
40758
+ rule: {
40759
+ ...noTargetBlankWithoutRel,
40760
+ framework: "global",
40761
+ category: "Accessibility",
40762
+ requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40763
+ }
40764
+ },
40119
40765
  {
40120
40766
  key: "react-doctor/no-this-in-sfc",
40121
40767
  id: "no-this-in-sfc",
@@ -40185,6 +40831,18 @@ const reactDoctorRules = [
40185
40831
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
40186
40832
  }
40187
40833
  },
40834
+ {
40835
+ key: "react-doctor/no-uninformative-aria-label",
40836
+ id: "no-uninformative-aria-label",
40837
+ source: "react-doctor",
40838
+ originallyExternal: false,
40839
+ rule: {
40840
+ ...noUninformativeAriaLabel,
40841
+ framework: "global",
40842
+ category: "Accessibility",
40843
+ requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40844
+ }
40845
+ },
40188
40846
  {
40189
40847
  key: "react-doctor/no-unknown-property",
40190
40848
  id: "no-unknown-property",
@@ -40394,6 +41052,17 @@ const reactDoctorRules = [
40394
41052
  category: "Bugs"
40395
41053
  }
40396
41054
  },
41055
+ {
41056
+ key: "react-doctor/prefer-dvh-over-vh",
41057
+ id: "prefer-dvh-over-vh",
41058
+ source: "react-doctor",
41059
+ originallyExternal: false,
41060
+ rule: {
41061
+ ...preferDvhOverVh,
41062
+ framework: "global",
41063
+ category: "Maintainability"
41064
+ }
41065
+ },
40397
41066
  {
40398
41067
  key: "react-doctor/prefer-dynamic-import",
40399
41068
  id: "prefer-dynamic-import",
@@ -40498,6 +41167,17 @@ const reactDoctorRules = [
40498
41167
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
40499
41168
  }
40500
41169
  },
41170
+ {
41171
+ key: "react-doctor/prefer-truncate-shorthand",
41172
+ id: "prefer-truncate-shorthand",
41173
+ source: "react-doctor",
41174
+ originallyExternal: false,
41175
+ rule: {
41176
+ ...preferTruncateShorthand,
41177
+ framework: "global",
41178
+ category: "Maintainability"
41179
+ }
41180
+ },
40501
41181
  {
40502
41182
  key: "react-doctor/prefer-use-effect-event",
40503
41183
  id: "prefer-use-effect-event",