oxlint-plugin-react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.eafac9d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +635 -5
  2. package/dist/index.js +1085 -160
  3. package/package.json +2 -2
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$57 = "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$57
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$56 = "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$56
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$56
2299
+ message: MESSAGE$63
2300
2300
  });
2301
2301
  } })
2302
2302
  });
@@ -3104,6 +3104,76 @@ const AUTH_FUNCTION_NAMES = new Set([
3104
3104
  "getAuth",
3105
3105
  "validateSession"
3106
3106
  ]);
3107
+ const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
3108
+ const AUTH_STANDALONE_NOUN_TOKENS = new Set([
3109
+ "signedin",
3110
+ "loggedin",
3111
+ "signin"
3112
+ ]);
3113
+ const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
3114
+ "require",
3115
+ "ensure",
3116
+ "assert",
3117
+ "verify",
3118
+ "validate",
3119
+ "check",
3120
+ "protect",
3121
+ "enforce",
3122
+ "guard",
3123
+ "gate",
3124
+ "restrict",
3125
+ "is",
3126
+ "has",
3127
+ "can",
3128
+ "must"
3129
+ ]);
3130
+ const AUTH_GETTER_VERB_TOKENS = new Set([
3131
+ "get",
3132
+ "fetch",
3133
+ "load",
3134
+ "read",
3135
+ "resolve",
3136
+ "retrieve",
3137
+ "use"
3138
+ ]);
3139
+ const AUTH_QUALIFIER_TOKENS = new Set([
3140
+ "current",
3141
+ "my",
3142
+ "own"
3143
+ ]);
3144
+ const AUTH_STRONG_NOUN_TOKENS = new Set([
3145
+ "session",
3146
+ "sessions",
3147
+ "login",
3148
+ "admin",
3149
+ "admins",
3150
+ "superadmin",
3151
+ "superuser",
3152
+ "role",
3153
+ "roles",
3154
+ "permission",
3155
+ "permissions",
3156
+ "jwt",
3157
+ "identity",
3158
+ "principal",
3159
+ "credential",
3160
+ "credentials"
3161
+ ]);
3162
+ const AUTH_WEAK_NOUN_TOKENS = new Set([
3163
+ "user",
3164
+ "users",
3165
+ "account",
3166
+ "accounts",
3167
+ "token",
3168
+ "tokens",
3169
+ "access",
3170
+ "me",
3171
+ "viewer",
3172
+ "caller",
3173
+ "subject",
3174
+ "scope",
3175
+ "scopes"
3176
+ ]);
3107
3177
  const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
3108
3178
  const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
3109
3179
  const SECRET_PATTERNS = [
@@ -4202,7 +4272,7 @@ const asyncParallel = defineRule({
4202
4272
  });
4203
4273
  //#endregion
4204
4274
  //#region src/plugin/rules/security/auth-token-in-web-storage.ts
4205
- const MESSAGE$55 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
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.";
4206
4276
  const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
4207
4277
  const STORAGE_GLOBALS = new Set([
4208
4278
  "window",
@@ -4236,7 +4306,7 @@ const authTokenInWebStorage = defineRule({
4236
4306
  if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
4237
4307
  context.report({
4238
4308
  node,
4239
- message: MESSAGE$55
4309
+ message: MESSAGE$62
4240
4310
  });
4241
4311
  },
4242
4312
  AssignmentExpression(node) {
@@ -4247,7 +4317,7 @@ const authTokenInWebStorage = defineRule({
4247
4317
  if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
4248
4318
  context.report({
4249
4319
  node: target,
4250
- message: MESSAGE$55
4320
+ message: MESSAGE$62
4251
4321
  });
4252
4322
  }
4253
4323
  })
@@ -4624,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
4624
4694
  //#endregion
4625
4695
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
4626
4696
  const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
4627
- const MESSAGE$54 = "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`.";
4628
4698
  const KEY_HANDLERS = [
4629
4699
  "onKeyUp",
4630
4700
  "onKeyDown",
@@ -4656,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
4656
4726
  if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
4657
4727
  context.report({
4658
4728
  node: node.name,
4659
- message: MESSAGE$54
4729
+ message: MESSAGE$61
4660
4730
  });
4661
4731
  } };
4662
4732
  }
@@ -4771,7 +4841,7 @@ const isReactComponentName = (name) => {
4771
4841
  };
4772
4842
  //#endregion
4773
4843
  //#region src/plugin/rules/a11y/control-has-associated-label.ts
4774
- const MESSAGE$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
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`.";
4775
4845
  const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
4776
4846
  const DEFAULT_LABELLING_PROPS = [
4777
4847
  "alt",
@@ -4932,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
4932
5002
  for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
4933
5003
  context.report({
4934
5004
  node: opening,
4935
- message: MESSAGE$53
5005
+ message: MESSAGE$60
4936
5006
  });
4937
5007
  } };
4938
5008
  }
@@ -5061,6 +5131,7 @@ const dangerousHtmlSink = defineRule({
5061
5131
  return findings;
5062
5132
  }
5063
5133
  });
5134
+ const WCAG_CONTRAST_NORMAL_MIN = 4.5;
5064
5135
  const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
5065
5136
  const VAGUE_BUTTON_LABELS = new Set([
5066
5137
  "continue",
@@ -5359,10 +5430,10 @@ const noVagueButtonLabel = defineRule({
5359
5430
  });
5360
5431
  //#endregion
5361
5432
  //#region src/plugin/utils/has-jsx-spread-attribute.ts
5362
- const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5433
+ const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
5363
5434
  //#endregion
5364
5435
  //#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
5365
- const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
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.";
5366
5437
  const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
5367
5438
  const NAME_PROVIDING_ATTRIBUTES = [
5368
5439
  "aria-label",
@@ -5381,11 +5452,11 @@ const dialogHasAccessibleName = defineRule({
5381
5452
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
5382
5453
  const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
5383
5454
  if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
5384
- if (hasJsxSpreadAttribute$1(node.attributes)) return;
5455
+ if (hasJsxSpreadAttribute(node.attributes)) return;
5385
5456
  if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
5386
5457
  context.report({
5387
5458
  node: node.name,
5388
- message: MESSAGE$52
5459
+ message: MESSAGE$59
5389
5460
  });
5390
5461
  } })
5391
5462
  });
@@ -5424,7 +5495,7 @@ const isEs6Component = (node) => {
5424
5495
  };
5425
5496
  //#endregion
5426
5497
  //#region src/plugin/rules/react-builtins/display-name.ts
5427
- const MESSAGE$51 = "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`.";
5428
5499
  const DEFAULT_ADDITIONAL_HOCS = [
5429
5500
  "observer",
5430
5501
  "lazy",
@@ -5627,7 +5698,7 @@ const displayName = defineRule({
5627
5698
  const reportAt = (node) => {
5628
5699
  context.report({
5629
5700
  node,
5630
- message: MESSAGE$51
5701
+ message: MESSAGE$58
5631
5702
  });
5632
5703
  };
5633
5704
  return {
@@ -7775,7 +7846,7 @@ const forbidElements = defineRule({
7775
7846
  });
7776
7847
  //#endregion
7777
7848
  //#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
7778
- const MESSAGE$50 = "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`.";
7779
7850
  const forwardRefUsesRef = defineRule({
7780
7851
  id: "forward-ref-uses-ref",
7781
7852
  title: "forwardRef without ref parameter",
@@ -7795,7 +7866,7 @@ const forwardRefUsesRef = defineRule({
7795
7866
  if (isNodeOfType(onlyParam, "RestElement")) return;
7796
7867
  context.report({
7797
7868
  node: inner,
7798
- message: MESSAGE$50
7869
+ message: MESSAGE$57
7799
7870
  });
7800
7871
  } })
7801
7872
  });
@@ -7832,7 +7903,7 @@ const gitProviderUrlInjectionRisk = defineRule({
7832
7903
  });
7833
7904
  //#endregion
7834
7905
  //#region src/plugin/rules/a11y/heading-has-content.ts
7835
- const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
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`.";
7836
7907
  const DEFAULT_HEADING_TAGS = [
7837
7908
  "h1",
7838
7909
  "h2",
@@ -7865,7 +7936,7 @@ const headingHasContent = defineRule({
7865
7936
  if (isHiddenFromScreenReader(node, context.settings)) return;
7866
7937
  context.report({
7867
7938
  node,
7868
- message: MESSAGE$49
7939
+ message: MESSAGE$56
7869
7940
  });
7870
7941
  } };
7871
7942
  }
@@ -8003,7 +8074,7 @@ const hooksNoNanInDeps = defineRule({
8003
8074
  });
8004
8075
  //#endregion
8005
8076
  //#region src/plugin/rules/a11y/html-has-lang.ts
8006
- const MESSAGE$48 = "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`.";
8007
8078
  const resolveSettings$38 = (settings) => {
8008
8079
  const reactDoctor = settings?.["react-doctor"];
8009
8080
  return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
@@ -8051,7 +8122,7 @@ const htmlHasLang = defineRule({
8051
8122
  if (!lang) {
8052
8123
  context.report({
8053
8124
  node: node.name,
8054
- message: MESSAGE$48
8125
+ message: MESSAGE$55
8055
8126
  });
8056
8127
  return;
8057
8128
  }
@@ -8059,13 +8130,13 @@ const htmlHasLang = defineRule({
8059
8130
  if (verdict === "missing" || verdict === "empty") {
8060
8131
  context.report({
8061
8132
  node: lang,
8062
- message: MESSAGE$48
8133
+ message: MESSAGE$55
8063
8134
  });
8064
8135
  return;
8065
8136
  }
8066
8137
  if (hasSpread && !lang) context.report({
8067
8138
  node: node.name,
8068
- message: MESSAGE$48
8139
+ message: MESSAGE$55
8069
8140
  });
8070
8141
  } };
8071
8142
  }
@@ -8279,7 +8350,7 @@ const htmlNoNestedInteractive = defineRule({
8279
8350
  });
8280
8351
  //#endregion
8281
8352
  //#region src/plugin/rules/a11y/iframe-has-title.ts
8282
- const MESSAGE$47 = "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.";
8283
8354
  const evaluateTitleValue = (value) => {
8284
8355
  if (!value) return "missing";
8285
8356
  if (isNodeOfType(value, "Literal")) {
@@ -8319,14 +8390,14 @@ const iframeHasTitle = defineRule({
8319
8390
  if (!titleAttr) {
8320
8391
  if (hasSpread || tag === "iframe") context.report({
8321
8392
  node: node.name,
8322
- message: MESSAGE$47
8393
+ message: MESSAGE$54
8323
8394
  });
8324
8395
  return;
8325
8396
  }
8326
8397
  const verdict = evaluateTitleValue(titleAttr.value);
8327
8398
  if (verdict === "missing" || verdict === "empty") context.report({
8328
8399
  node: titleAttr,
8329
- message: MESSAGE$47
8400
+ message: MESSAGE$54
8330
8401
  });
8331
8402
  } })
8332
8403
  });
@@ -8430,7 +8501,7 @@ const iframeMissingSandbox = defineRule({
8430
8501
  });
8431
8502
  //#endregion
8432
8503
  //#region src/plugin/rules/a11y/img-redundant-alt.ts
8433
- const MESSAGE$46 = "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.";
8434
8505
  const DEFAULT_COMPONENTS = ["img"];
8435
8506
  const DEFAULT_REDUNDANT_WORDS = [
8436
8507
  "image",
@@ -8495,7 +8566,7 @@ const imgRedundantAlt = defineRule({
8495
8566
  if (!altAttribute) return;
8496
8567
  if (altValueRedundant(altAttribute, settings.words)) context.report({
8497
8568
  node: altAttribute,
8498
- message: MESSAGE$46
8569
+ message: MESSAGE$53
8499
8570
  });
8500
8571
  } };
8501
8572
  }
@@ -10852,7 +10923,7 @@ const jsxMaxDepth = defineRule({
10852
10923
  });
10853
10924
  //#endregion
10854
10925
  //#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
10855
- const MESSAGE$45 = "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.";
10856
10927
  const LITERAL_TEXT_TAGS = new Set([
10857
10928
  "code",
10858
10929
  "pre",
@@ -10888,7 +10959,7 @@ const jsxNoCommentTextnodes = defineRule({
10888
10959
  if (isInsideLiteralTextTag(node)) return;
10889
10960
  context.report({
10890
10961
  node,
10891
- message: MESSAGE$45
10962
+ message: MESSAGE$52
10892
10963
  });
10893
10964
  } })
10894
10965
  });
@@ -10919,7 +10990,7 @@ const isInsideFunctionScope = (node) => {
10919
10990
  };
10920
10991
  //#endregion
10921
10992
  //#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
10922
- const MESSAGE$44 = "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.";
10923
10994
  const CONTEXT_MODULES$1 = [
10924
10995
  "react",
10925
10996
  "use-context-selector",
@@ -11017,7 +11088,7 @@ const jsxNoConstructedContextValues = defineRule({
11017
11088
  if (!isConstructedValue(innerExpression)) continue;
11018
11089
  context.report({
11019
11090
  node: attribute,
11020
- message: MESSAGE$44
11091
+ message: MESSAGE$51
11021
11092
  });
11022
11093
  }
11023
11094
  }
@@ -11103,7 +11174,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
11103
11174
  };
11104
11175
  //#endregion
11105
11176
  //#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
11106
- const MESSAGE$43 = "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.";
11107
11178
  const KNOWN_SLOT_PROP_NAMES = new Set([
11108
11179
  "icon",
11109
11180
  "Icon",
@@ -11372,7 +11443,7 @@ const jsxNoJsxAsProp = defineRule({
11372
11443
  if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
11373
11444
  context.report({
11374
11445
  node,
11375
- message: MESSAGE$43
11446
+ message: MESSAGE$50
11376
11447
  });
11377
11448
  }
11378
11449
  };
@@ -11660,7 +11731,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
11660
11731
  ];
11661
11732
  //#endregion
11662
11733
  //#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
11663
- const MESSAGE$42 = "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.";
11664
11735
  const isDataArrayPropName = (propName) => {
11665
11736
  if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
11666
11737
  for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -11744,7 +11815,7 @@ const jsxNoNewArrayAsProp = defineRule({
11744
11815
  if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
11745
11816
  context.report({
11746
11817
  node,
11747
- message: MESSAGE$42
11818
+ message: MESSAGE$49
11748
11819
  });
11749
11820
  }
11750
11821
  };
@@ -12002,7 +12073,7 @@ const SAFE_RECEIVER_NAMES = new Set([
12002
12073
  ]);
12003
12074
  //#endregion
12004
12075
  //#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
12005
- const MESSAGE$41 = "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.";
12006
12077
  const isAccessorPredicateName = (propName) => {
12007
12078
  for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
12008
12079
  if (propName.length <= prefix.length) continue;
@@ -12208,7 +12279,7 @@ const jsxNoNewFunctionAsProp = defineRule({
12208
12279
  if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
12209
12280
  context.report({
12210
12281
  node,
12211
- message: MESSAGE$41
12282
+ message: MESSAGE$48
12212
12283
  });
12213
12284
  }
12214
12285
  };
@@ -12428,7 +12499,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
12428
12499
  ];
12429
12500
  //#endregion
12430
12501
  //#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
12431
- const MESSAGE$40 = "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.";
12432
12503
  const isConfigObjectPropName = (propName) => {
12433
12504
  if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
12434
12505
  for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
@@ -12516,7 +12587,7 @@ const jsxNoNewObjectAsProp = defineRule({
12516
12587
  if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
12517
12588
  context.report({
12518
12589
  node,
12519
- message: MESSAGE$40
12590
+ message: MESSAGE$47
12520
12591
  });
12521
12592
  }
12522
12593
  };
@@ -12524,7 +12595,7 @@ const jsxNoNewObjectAsProp = defineRule({
12524
12595
  });
12525
12596
  //#endregion
12526
12597
  //#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
12527
- const MESSAGE$39 = "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.";
12528
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;
12529
12600
  const resolveSettings$28 = (settings) => {
12530
12601
  const reactDoctor = settings?.["react-doctor"];
@@ -12565,7 +12636,7 @@ const jsxNoScriptUrl = defineRule({
12565
12636
  if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
12566
12637
  if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
12567
12638
  node: attribute,
12568
- message: MESSAGE$39
12639
+ message: MESSAGE$46
12569
12640
  });
12570
12641
  }
12571
12642
  } };
@@ -12880,7 +12951,7 @@ const jsxPropsNoSpreadMulti = defineRule({
12880
12951
  });
12881
12952
  //#endregion
12882
12953
  //#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
12883
- const MESSAGE$38 = "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.";
12884
12955
  const resolveSettings$25 = (settings) => {
12885
12956
  const reactDoctor = settings?.["react-doctor"];
12886
12957
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
@@ -12921,7 +12992,7 @@ const jsxPropsNoSpreading = defineRule({
12921
12992
  }
12922
12993
  context.report({
12923
12994
  node: attribute,
12924
- message: MESSAGE$38
12995
+ message: MESSAGE$45
12925
12996
  });
12926
12997
  }
12927
12998
  } };
@@ -13149,7 +13220,7 @@ const labelHasAssociatedControl = defineRule({
13149
13220
  });
13150
13221
  //#endregion
13151
13222
  //#region src/plugin/rules/a11y/lang.ts
13152
- const MESSAGE$37 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
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`.";
13153
13224
  const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
13154
13225
  "aa",
13155
13226
  "ab",
@@ -13361,7 +13432,7 @@ const lang = defineRule({
13361
13432
  if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
13362
13433
  context.report({
13363
13434
  node: langAttr,
13364
- message: MESSAGE$37
13435
+ message: MESSAGE$44
13365
13436
  });
13366
13437
  return;
13367
13438
  }
@@ -13370,7 +13441,7 @@ const lang = defineRule({
13370
13441
  if (value === null) return;
13371
13442
  if (!isValidLangTag(value)) context.report({
13372
13443
  node: langAttr,
13373
- message: MESSAGE$37
13444
+ message: MESSAGE$44
13374
13445
  });
13375
13446
  } })
13376
13447
  });
@@ -13414,7 +13485,7 @@ const mdxSsrExecutionRisk = defineRule({
13414
13485
  });
13415
13486
  //#endregion
13416
13487
  //#region src/plugin/rules/a11y/media-has-caption.ts
13417
- const MESSAGE$36 = "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>`.";
13418
13489
  const DEFAULT_AUDIO = ["audio"];
13419
13490
  const DEFAULT_VIDEO = ["video"];
13420
13491
  const DEFAULT_TRACK = ["track"];
@@ -13455,7 +13526,7 @@ const mediaHasCaption = defineRule({
13455
13526
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
13456
13527
  context.report({
13457
13528
  node: node.name,
13458
- message: MESSAGE$36
13529
+ message: MESSAGE$43
13459
13530
  });
13460
13531
  return;
13461
13532
  }
@@ -13472,7 +13543,7 @@ const mediaHasCaption = defineRule({
13472
13543
  return kindValue.value.toLowerCase() === "captions";
13473
13544
  })) context.report({
13474
13545
  node: node.name,
13475
- message: MESSAGE$36
13546
+ message: MESSAGE$43
13476
13547
  });
13477
13548
  } };
13478
13549
  }
@@ -15273,7 +15344,7 @@ const nextjsNoVercelOgImport = defineRule({
15273
15344
  });
15274
15345
  //#endregion
15275
15346
  //#region src/plugin/rules/a11y/no-access-key.ts
15276
- const MESSAGE$35 = "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.";
15277
15348
  const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
15278
15349
  const noAccessKey = defineRule({
15279
15350
  id: "no-access-key",
@@ -15290,7 +15361,7 @@ const noAccessKey = defineRule({
15290
15361
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
15291
15362
  context.report({
15292
15363
  node: accessKey,
15293
- message: MESSAGE$35
15364
+ message: MESSAGE$42
15294
15365
  });
15295
15366
  return;
15296
15367
  }
@@ -15300,7 +15371,7 @@ const noAccessKey = defineRule({
15300
15371
  if (isUndefinedIdentifier(expression)) return;
15301
15372
  context.report({
15302
15373
  node: accessKey,
15303
- message: MESSAGE$35
15374
+ message: MESSAGE$42
15304
15375
  });
15305
15376
  }
15306
15377
  } })
@@ -15782,8 +15853,41 @@ const noAdjustStateOnPropChange = defineRule({
15782
15853
  } })
15783
15854
  });
15784
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
15785
15889
  //#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
15786
- const MESSAGE$34 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
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.";
15787
15891
  const noAriaHiddenOnFocusable = defineRule({
15788
15892
  id: "no-aria-hidden-on-focusable",
15789
15893
  title: "aria-hidden on focusable element",
@@ -15810,7 +15914,7 @@ const noAriaHiddenOnFocusable = defineRule({
15810
15914
  const isImplicitlyFocusable = isInteractiveElement(tag, node);
15811
15915
  if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
15812
15916
  node: ariaHidden,
15813
- message: MESSAGE$34
15917
+ message: MESSAGE$41
15814
15918
  });
15815
15919
  } })
15816
15920
  });
@@ -16178,7 +16282,7 @@ const noArrayIndexAsKey = defineRule({
16178
16282
  });
16179
16283
  //#endregion
16180
16284
  //#region src/plugin/rules/react-builtins/no-array-index-key.ts
16181
- const MESSAGE$33 = "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.";
16182
16286
  const SECOND_INDEX_METHODS = new Set([
16183
16287
  "every",
16184
16288
  "filter",
@@ -16382,7 +16486,7 @@ const noArrayIndexKey = defineRule({
16382
16486
  }
16383
16487
  context.report({
16384
16488
  node: keyAttribute,
16385
- message: MESSAGE$33
16489
+ message: MESSAGE$40
16386
16490
  });
16387
16491
  },
16388
16492
  CallExpression(node) {
@@ -16402,7 +16506,7 @@ const noArrayIndexKey = defineRule({
16402
16506
  if (propName !== "key") continue;
16403
16507
  if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
16404
16508
  node: property,
16405
- message: MESSAGE$33
16509
+ message: MESSAGE$40
16406
16510
  });
16407
16511
  }
16408
16512
  }
@@ -16410,7 +16514,7 @@ const noArrayIndexKey = defineRule({
16410
16514
  });
16411
16515
  //#endregion
16412
16516
  //#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
16413
- const MESSAGE$32 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
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.";
16414
16518
  const noAsyncEffectCallback = defineRule({
16415
16519
  id: "no-async-effect-callback",
16416
16520
  title: "Async effect callback",
@@ -16424,13 +16528,13 @@ const noAsyncEffectCallback = defineRule({
16424
16528
  if (!callback.async) return;
16425
16529
  context.report({
16426
16530
  node: callback,
16427
- message: MESSAGE$32
16531
+ message: MESSAGE$39
16428
16532
  });
16429
16533
  } })
16430
16534
  });
16431
16535
  //#endregion
16432
16536
  //#region src/plugin/rules/a11y/no-autofocus.ts
16433
- const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
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.";
16434
16538
  const resolveSettings$21 = (settings) => {
16435
16539
  const reactDoctor = settings?.["react-doctor"];
16436
16540
  return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
@@ -16486,12 +16590,45 @@ const noAutofocus = defineRule({
16486
16590
  }
16487
16591
  context.report({
16488
16592
  node: autoFocusAttribute,
16489
- message: MESSAGE$31
16593
+ message: MESSAGE$38
16490
16594
  });
16491
16595
  } };
16492
16596
  }
16493
16597
  });
16494
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
16495
16632
  //#region src/plugin/utils/create-relative-import-source.ts
16496
16633
  const createRelativeImportSource = (filename, targetFilePath) => {
16497
16634
  const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
@@ -16990,7 +17127,7 @@ const noChainStateUpdates = defineRule({
16990
17127
  });
16991
17128
  //#endregion
16992
17129
  //#region src/plugin/rules/react-builtins/no-children-prop.ts
16993
- const MESSAGE$30 = "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.";
16994
17131
  const noChildrenProp = defineRule({
16995
17132
  id: "no-children-prop",
16996
17133
  title: "Children passed as a prop",
@@ -17002,7 +17139,7 @@ const noChildrenProp = defineRule({
17002
17139
  if (node.name.name !== "children") return;
17003
17140
  context.report({
17004
17141
  node: node.name,
17005
- message: MESSAGE$30
17142
+ message: MESSAGE$36
17006
17143
  });
17007
17144
  },
17008
17145
  CallExpression(node) {
@@ -17015,7 +17152,7 @@ const noChildrenProp = defineRule({
17015
17152
  const propertyKey = property.key;
17016
17153
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
17017
17154
  node: propertyKey,
17018
- message: MESSAGE$30
17155
+ message: MESSAGE$36
17019
17156
  });
17020
17157
  }
17021
17158
  }
@@ -17023,7 +17160,7 @@ const noChildrenProp = defineRule({
17023
17160
  });
17024
17161
  //#endregion
17025
17162
  //#region src/plugin/rules/react-builtins/no-clone-element.ts
17026
- const MESSAGE$29 = "`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.";
17027
17164
  const noCloneElement = defineRule({
17028
17165
  id: "no-clone-element",
17029
17166
  title: "cloneElement makes child props fragile",
@@ -17036,7 +17173,7 @@ const noCloneElement = defineRule({
17036
17173
  if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
17037
17174
  if (isImportedFromModule(node, "cloneElement", "react")) context.report({
17038
17175
  node: callee,
17039
- message: MESSAGE$29
17176
+ message: MESSAGE$35
17040
17177
  });
17041
17178
  return;
17042
17179
  }
@@ -17049,7 +17186,7 @@ const noCloneElement = defineRule({
17049
17186
  if (!isImportedFromModule(node, callee.object.name, "react")) return;
17050
17187
  context.report({
17051
17188
  node: callee,
17052
- message: MESSAGE$29
17189
+ message: MESSAGE$35
17053
17190
  });
17054
17191
  }
17055
17192
  } })
@@ -17098,7 +17235,7 @@ const enclosingComponentOrHookName = (node) => {
17098
17235
  };
17099
17236
  //#endregion
17100
17237
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
17101
- const MESSAGE$28 = "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.";
17102
17239
  const CONTEXT_MODULES = [
17103
17240
  "react",
17104
17241
  "use-context-selector",
@@ -17134,13 +17271,13 @@ const noCreateContextInRender = defineRule({
17134
17271
  if (!componentOrHookName) return;
17135
17272
  context.report({
17136
17273
  node,
17137
- message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
17274
+ message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
17138
17275
  });
17139
17276
  } })
17140
17277
  });
17141
17278
  //#endregion
17142
17279
  //#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
17143
- const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
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.";
17144
17281
  const noCreateRefInFunctionComponent = defineRule({
17145
17282
  id: "no-create-ref-in-function-component",
17146
17283
  title: "createRef in function component",
@@ -17159,7 +17296,7 @@ const noCreateRefInFunctionComponent = defineRule({
17159
17296
  if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
17160
17297
  context.report({
17161
17298
  node,
17162
- message: MESSAGE$27
17299
+ message: MESSAGE$33
17163
17300
  });
17164
17301
  } })
17165
17302
  });
@@ -17299,7 +17436,7 @@ const noCreateStoreInRender = defineRule({
17299
17436
  });
17300
17437
  //#endregion
17301
17438
  //#region src/plugin/rules/react-builtins/no-danger.ts
17302
- const MESSAGE$26 = "`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.";
17303
17440
  const noDanger = defineRule({
17304
17441
  id: "no-danger",
17305
17442
  title: "Raw HTML injection can run unsafe markup",
@@ -17312,7 +17449,7 @@ const noDanger = defineRule({
17312
17449
  if (!propAttribute) return;
17313
17450
  context.report({
17314
17451
  node: propAttribute.name,
17315
- message: MESSAGE$26
17452
+ message: MESSAGE$32
17316
17453
  });
17317
17454
  },
17318
17455
  CallExpression(node) {
@@ -17324,7 +17461,7 @@ const noDanger = defineRule({
17324
17461
  const propertyKey = property.key;
17325
17462
  if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
17326
17463
  node: propertyKey,
17327
- message: MESSAGE$26
17464
+ message: MESSAGE$32
17328
17465
  });
17329
17466
  }
17330
17467
  }
@@ -17332,7 +17469,7 @@ const noDanger = defineRule({
17332
17469
  });
17333
17470
  //#endregion
17334
17471
  //#region src/plugin/rules/react-builtins/no-danger-with-children.ts
17335
- const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17472
+ const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
17336
17473
  const isLineBreak = (child) => {
17337
17474
  if (!isNodeOfType(child, "JSXText")) return false;
17338
17475
  return child.value.trim().length === 0 && child.value.includes("\n");
@@ -17402,7 +17539,7 @@ const noDangerWithChildren = defineRule({
17402
17539
  if (!hasChildrenProp && !hasNestedChildren) return;
17403
17540
  if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
17404
17541
  node: opening,
17405
- message: MESSAGE$25
17542
+ message: MESSAGE$31
17406
17543
  });
17407
17544
  },
17408
17545
  CallExpression(node) {
@@ -17414,7 +17551,7 @@ const noDangerWithChildren = defineRule({
17414
17551
  if (!propsShape.hasDangerously) return;
17415
17552
  if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
17416
17553
  node,
17417
- message: MESSAGE$25
17554
+ message: MESSAGE$31
17418
17555
  });
17419
17556
  }
17420
17557
  })
@@ -17579,6 +17716,37 @@ const noDefaultProps = defineRule({
17579
17716
  } })
17580
17717
  });
17581
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
17582
17750
  //#region src/plugin/utils/is-initial-only-prop-name.ts
17583
17751
  const isInitialOnlyPropName = (propName) => {
17584
17752
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
@@ -17991,7 +18159,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
17991
18159
  //#endregion
17992
18160
  //#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
17993
18161
  const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
17994
- const MESSAGE$24 = "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`.";
17995
18163
  const resolveSettings$20 = (settings) => {
17996
18164
  const reactDoctor = settings?.["react-doctor"];
17997
18165
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
@@ -18010,7 +18178,7 @@ const noDidMountSetState = defineRule({
18010
18178
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18011
18179
  context.report({
18012
18180
  node: node.callee,
18013
- message: MESSAGE$24
18181
+ message: MESSAGE$30
18014
18182
  });
18015
18183
  } };
18016
18184
  }
@@ -18018,7 +18186,7 @@ const noDidMountSetState = defineRule({
18018
18186
  //#endregion
18019
18187
  //#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
18020
18188
  const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
18021
- const MESSAGE$23 = "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.";
18022
18190
  const resolveSettings$19 = (settings) => {
18023
18191
  const reactDoctor = settings?.["react-doctor"];
18024
18192
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -18037,7 +18205,7 @@ const noDidUpdateSetState = defineRule({
18037
18205
  if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
18038
18206
  context.report({
18039
18207
  node: node.callee,
18040
- message: MESSAGE$23
18208
+ message: MESSAGE$29
18041
18209
  });
18042
18210
  } };
18043
18211
  }
@@ -18060,7 +18228,7 @@ const isStateMemberExpression = (node) => {
18060
18228
  };
18061
18229
  //#endregion
18062
18230
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
18063
- const MESSAGE$22 = "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.";
18064
18232
  const shouldIgnoreMutation = (node) => {
18065
18233
  let isConstructor = false;
18066
18234
  let isInsideCallExpression = false;
@@ -18082,7 +18250,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
18082
18250
  if (shouldIgnoreMutation(reportNode)) return;
18083
18251
  context.report({
18084
18252
  node: reportNode,
18085
- message: MESSAGE$22
18253
+ message: MESSAGE$28
18086
18254
  });
18087
18255
  };
18088
18256
  const noDirectMutationState = defineRule({
@@ -18292,6 +18460,26 @@ const noDocumentStartViewTransition = defineRule({
18292
18460
  } })
18293
18461
  });
18294
18462
  //#endregion
18463
+ //#region src/plugin/rules/js-performance/no-document-write.ts
18464
+ const MESSAGE$27 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
18465
+ const WRITE_METHODS = new Set(["write", "writeln"]);
18466
+ const noDocumentWrite = defineRule({
18467
+ id: "no-document-write",
18468
+ title: "document.write/writeln",
18469
+ severity: "warn",
18470
+ recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
18471
+ create: (context) => ({ CallExpression(node) {
18472
+ const callee = node.callee;
18473
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
18474
+ if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
18475
+ if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
18476
+ context.report({
18477
+ node,
18478
+ message: MESSAGE$27
18479
+ });
18480
+ } })
18481
+ });
18482
+ //#endregion
18295
18483
  //#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
18296
18484
  const noDynamicImportPath = defineRule({
18297
18485
  id: "no-dynamic-import-path",
@@ -19670,7 +19858,7 @@ const ALLOWED_NAMESPACES = new Set([
19670
19858
  "ReactDOM",
19671
19859
  "ReactDom"
19672
19860
  ]);
19673
- const MESSAGE$21 = "`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.";
19674
19862
  const noFindDomNode = defineRule({
19675
19863
  id: "no-find-dom-node",
19676
19864
  title: "findDOMNode breaks component encapsulation",
@@ -19681,7 +19869,7 @@ const noFindDomNode = defineRule({
19681
19869
  if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
19682
19870
  context.report({
19683
19871
  node: callee,
19684
- message: MESSAGE$21
19872
+ message: MESSAGE$26
19685
19873
  });
19686
19874
  return;
19687
19875
  }
@@ -19692,7 +19880,7 @@ const noFindDomNode = defineRule({
19692
19880
  if (callee.property.name !== "findDOMNode") return;
19693
19881
  context.report({
19694
19882
  node: callee.property,
19695
- message: MESSAGE$21
19883
+ message: MESSAGE$26
19696
19884
  });
19697
19885
  }
19698
19886
  } })
@@ -19733,6 +19921,41 @@ const noFullLodashImport = defineRule({
19733
19921
  } })
19734
19922
  });
19735
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
19736
19959
  //#region src/plugin/rules/architecture/no-generic-handler-names.ts
19737
19960
  const noGenericHandlerNames = defineRule({
19738
19961
  id: "no-generic-handler-names",
@@ -19795,7 +20018,7 @@ const noGiantComponent = defineRule({
19795
20018
  });
19796
20019
  //#endregion
19797
20020
  //#region src/plugin/constants/style.ts
19798
- const LAYOUT_PROPERTIES = new Set([
20021
+ const LAYOUT_PROPERTIES$1 = new Set([
19799
20022
  "width",
19800
20023
  "height",
19801
20024
  "top",
@@ -19865,17 +20088,6 @@ const noGlobalCssVariableAnimation = defineRule({
19865
20088
  } })
19866
20089
  });
19867
20090
  //#endregion
19868
- //#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
19869
- const getStringFromClassNameAttr = (node) => {
19870
- if (!isNodeOfType(node, "JSXOpeningElement")) return null;
19871
- const classAttr = findJsxAttribute(node.attributes ?? [], "className");
19872
- if (!classAttr?.value) return null;
19873
- if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
19874
- if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
19875
- 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;
19876
- return null;
19877
- };
19878
- //#endregion
19879
20091
  //#region src/plugin/rules/design/no-gradient-text.ts
19880
20092
  const noGradientText = defineRule({
19881
20093
  id: "no-gradient-text",
@@ -19934,7 +20146,7 @@ const noGrayOnColoredBackground = defineRule({
19934
20146
  });
19935
20147
  //#endregion
19936
20148
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
19937
- const MESSAGE$20 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
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.";
19938
20150
  const noImgLazyWithHighFetchpriority = defineRule({
19939
20151
  id: "no-img-lazy-with-high-fetchpriority",
19940
20152
  title: "Lazy image with high fetchPriority",
@@ -19948,7 +20160,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
19948
20160
  if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
19949
20161
  context.report({
19950
20162
  node: node.name,
19951
- message: MESSAGE$20
20163
+ message: MESSAGE$24
19952
20164
  });
19953
20165
  } })
19954
20166
  });
@@ -20183,7 +20395,7 @@ const noIsMounted = defineRule({
20183
20395
  });
20184
20396
  //#endregion
20185
20397
  //#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
20186
- const MESSAGE$19 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
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)`.";
20187
20399
  const isJsonMethodCall = (node, method) => {
20188
20400
  if (!isNodeOfType(node, "CallExpression")) return false;
20189
20401
  const callee = node.callee;
@@ -20200,13 +20412,13 @@ const noJsonParseStringifyClone = defineRule({
20200
20412
  if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
20201
20413
  context.report({
20202
20414
  node,
20203
- message: MESSAGE$19
20415
+ message: MESSAGE$23
20204
20416
  });
20205
20417
  } })
20206
20418
  });
20207
20419
  //#endregion
20208
20420
  //#region src/plugin/rules/correctness/no-jsx-element-type.ts
20209
- const MESSAGE$18 = "`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.";
20210
20422
  const isJsxElementTypeReference = (node) => {
20211
20423
  if (!isNodeOfType(node, "TSTypeReference")) return false;
20212
20424
  const typeName = node.typeName;
@@ -20223,7 +20435,7 @@ const checkReturnType = (context, returnType) => {
20223
20435
  if (!typeAnnotation) return;
20224
20436
  if (isJsxElementTypeReference(typeAnnotation)) context.report({
20225
20437
  node: typeAnnotation,
20226
- message: MESSAGE$18
20438
+ message: MESSAGE$22
20227
20439
  });
20228
20440
  };
20229
20441
  const noJsxElementType = defineRule({
@@ -20333,7 +20545,7 @@ const noLayoutPropertyAnimation = defineRule({
20333
20545
  let propertyName = null;
20334
20546
  if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
20335
20547
  else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
20336
- if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
20548
+ if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
20337
20549
  node: property,
20338
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`
20339
20551
  });
@@ -20523,6 +20735,134 @@ const noLongTransitionDuration = defineRule({
20523
20735
  } })
20524
20736
  });
20525
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
20526
20866
  //#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
20527
20867
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
20528
20868
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
@@ -20678,7 +21018,7 @@ const noMoment = defineRule({
20678
21018
  });
20679
21019
  //#endregion
20680
21020
  //#region src/plugin/rules/react-builtins/no-multi-comp.ts
20681
- const MESSAGE$17 = "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.";
20682
21022
  const resolveSettings$16 = (settings) => {
20683
21023
  const reactDoctor = settings?.["react-doctor"];
20684
21024
  return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
@@ -21000,7 +21340,7 @@ const noMultiComp = defineRule({
21000
21340
  if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
21001
21341
  for (const component of flagged.slice(1)) context.report({
21002
21342
  node: component.reportNode,
21003
- message: MESSAGE$17
21343
+ message: MESSAGE$21
21004
21344
  });
21005
21345
  } };
21006
21346
  }
@@ -21168,7 +21508,7 @@ const resolveReducerFunction = (node, currentFilename) => {
21168
21508
  };
21169
21509
  //#endregion
21170
21510
  //#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
21171
- const MESSAGE$16 = "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.";
21172
21512
  const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
21173
21513
  "copyWithin",
21174
21514
  "fill",
@@ -21378,7 +21718,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21378
21718
  reportedNodes.add(options.crossFileConsumerCallSite);
21379
21719
  context.report({
21380
21720
  node: options.crossFileConsumerCallSite,
21381
- message: `${MESSAGE$16} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21721
+ message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
21382
21722
  });
21383
21723
  return;
21384
21724
  }
@@ -21387,7 +21727,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
21387
21727
  reportedNodes.add(mutation.node);
21388
21728
  context.report({
21389
21729
  node: mutation.node,
21390
- message: MESSAGE$16
21730
+ message: MESSAGE$20
21391
21731
  });
21392
21732
  }
21393
21733
  };
@@ -21659,7 +21999,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
21659
21999
  });
21660
22000
  //#endregion
21661
22001
  //#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
21662
- const MESSAGE$15 = "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.";
21663
22003
  const resolveSettings$14 = (settings) => {
21664
22004
  const reactDoctor = settings?.["react-doctor"];
21665
22005
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -21687,7 +22027,7 @@ const noNoninteractiveTabindex = defineRule({
21687
22027
  if (numeric === null) {
21688
22028
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
21689
22029
  node: tabIndex,
21690
- message: MESSAGE$15
22030
+ message: MESSAGE$19
21691
22031
  });
21692
22032
  return;
21693
22033
  }
@@ -21700,7 +22040,7 @@ const noNoninteractiveTabindex = defineRule({
21700
22040
  if (!roleAttribute) {
21701
22041
  context.report({
21702
22042
  node: tabIndex,
21703
- message: MESSAGE$15
22043
+ message: MESSAGE$19
21704
22044
  });
21705
22045
  return;
21706
22046
  }
@@ -21714,20 +22054,12 @@ const noNoninteractiveTabindex = defineRule({
21714
22054
  }
21715
22055
  context.report({
21716
22056
  node: tabIndex,
21717
- message: MESSAGE$15
22057
+ message: MESSAGE$19
21718
22058
  });
21719
22059
  } };
21720
22060
  }
21721
22061
  });
21722
22062
  //#endregion
21723
- //#region src/plugin/rules/design/utils/get-style-property-number-value.ts
21724
- const getStylePropertyNumberValue = (property) => {
21725
- if (!isNodeOfType(property, "Property")) return null;
21726
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
21727
- if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
21728
- return null;
21729
- };
21730
- //#endregion
21731
22063
  //#region src/plugin/rules/design/no-outline-none.ts
21732
22064
  const noOutlineNone = defineRule({
21733
22065
  id: "no-outline-none",
@@ -22405,7 +22737,7 @@ const noRandomKey = defineRule({
22405
22737
  });
22406
22738
  //#endregion
22407
22739
  //#region src/plugin/rules/react-builtins/no-react-children.ts
22408
- const MESSAGE$14 = "`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.";
22409
22741
  const isChildrenIdentifier = (node, contextNode) => {
22410
22742
  if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
22411
22743
  return isImportedFromModule(contextNode, "Children", "react");
@@ -22431,13 +22763,13 @@ const noReactChildren = defineRule({
22431
22763
  if (isChildrenIdentifier(memberObject, node)) {
22432
22764
  context.report({
22433
22765
  node: calleeOuter,
22434
- message: MESSAGE$14
22766
+ message: MESSAGE$18
22435
22767
  });
22436
22768
  return;
22437
22769
  }
22438
22770
  if (isReactNamespaceMember(memberObject, node)) context.report({
22439
22771
  node: calleeOuter,
22440
- message: MESSAGE$14
22772
+ message: MESSAGE$18
22441
22773
  });
22442
22774
  } })
22443
22775
  });
@@ -22548,6 +22880,86 @@ const noReact19DeprecatedApis = defineRule({
22548
22880
  })
22549
22881
  });
22550
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
22551
22963
  //#region src/plugin/constants/aria-element-roles.ts
22552
22964
  const ELEMENT_ROLE_PAIRS = [
22553
22965
  ["a", "link"],
@@ -22760,7 +23172,7 @@ const noRenderPropChildren = defineRule({
22760
23172
  });
22761
23173
  //#endregion
22762
23174
  //#region src/plugin/rules/react-builtins/no-render-return-value.ts
22763
- const MESSAGE$13 = "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.";
22764
23176
  const isReactDomRenderCall = (node) => {
22765
23177
  if (!isNodeOfType(node.callee, "MemberExpression")) return false;
22766
23178
  if (!isNodeOfType(node.callee.object, "Identifier")) return false;
@@ -22784,7 +23196,7 @@ const noRenderReturnValue = defineRule({
22784
23196
  if (!isUsedAsReturnValue(node.parent)) return;
22785
23197
  context.report({
22786
23198
  node: node.callee,
22787
- message: MESSAGE$13
23199
+ message: MESSAGE$17
22788
23200
  });
22789
23201
  } })
22790
23202
  });
@@ -22944,11 +23356,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
22944
23356
  return "unknown";
22945
23357
  };
22946
23358
  //#endregion
22947
- //#region src/plugin/utils/get-identifier-trailing-word.ts
22948
- const getIdentifierTrailingWord = (identifierName) => {
22949
- return identifierName.match(/[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g)?.at(-1)?.toLowerCase() ?? identifierName.toLowerCase();
23359
+ //#region src/plugin/utils/tokenize-identifier-words.ts
23360
+ const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
23361
+ const tokenizeIdentifierWords = (identifierName) => {
23362
+ const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
23363
+ if (!words) return [];
23364
+ return words.map((word) => word.toLowerCase());
22950
23365
  };
22951
23366
  //#endregion
23367
+ //#region src/plugin/utils/get-identifier-trailing-word.ts
23368
+ const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
23369
+ //#endregion
22952
23370
  //#region src/plugin/constants/tanstack.ts
22953
23371
  const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
22954
23372
  const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
@@ -23476,7 +23894,7 @@ const getParentComponent = (node) => {
23476
23894
  };
23477
23895
  //#endregion
23478
23896
  //#region src/plugin/rules/react-builtins/no-set-state.ts
23479
- const MESSAGE$12 = "`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.";
23480
23898
  const noSetState = defineRule({
23481
23899
  id: "no-set-state",
23482
23900
  title: "Local class state forbidden",
@@ -23491,7 +23909,7 @@ const noSetState = defineRule({
23491
23909
  if (!getParentComponent(node)) return;
23492
23910
  context.report({
23493
23911
  node: node.callee,
23494
- message: MESSAGE$12
23912
+ message: MESSAGE$16
23495
23913
  });
23496
23914
  } })
23497
23915
  });
@@ -23653,7 +24071,7 @@ const isAbstractRole = (openingElement, settings) => {
23653
24071
  };
23654
24072
  //#endregion
23655
24073
  //#region src/plugin/rules/a11y/no-static-element-interactions.ts
23656
- const MESSAGE$11 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
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.";
23657
24075
  const DEFAULT_HANDLERS = [
23658
24076
  "onClick",
23659
24077
  "onMouseDown",
@@ -23713,7 +24131,7 @@ const noStaticElementInteractions = defineRule({
23713
24131
  if (!roleAttribute || !roleAttribute.value) {
23714
24132
  context.report({
23715
24133
  node: node.name,
23716
- message: MESSAGE$11
24134
+ message: MESSAGE$15
23717
24135
  });
23718
24136
  return;
23719
24137
  }
@@ -23723,19 +24141,66 @@ const noStaticElementInteractions = defineRule({
23723
24141
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
23724
24142
  context.report({
23725
24143
  node: node.name,
23726
- message: MESSAGE$11
24144
+ message: MESSAGE$15
23727
24145
  });
23728
24146
  return;
23729
24147
  }
23730
24148
  if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
23731
24149
  context.report({
23732
24150
  node: node.name,
23733
- message: MESSAGE$11
24151
+ message: MESSAGE$15
23734
24152
  });
23735
24153
  } };
23736
24154
  }
23737
24155
  });
23738
24156
  //#endregion
24157
+ //#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
24158
+ const BOOLEAN_ATTRIBUTES = new Set([
24159
+ "disabled",
24160
+ "checked",
24161
+ "readonly",
24162
+ "required",
24163
+ "selected",
24164
+ "multiple",
24165
+ "autofocus",
24166
+ "autoplay",
24167
+ "controls",
24168
+ "loop",
24169
+ "muted",
24170
+ "open",
24171
+ "reversed",
24172
+ "default",
24173
+ "novalidate",
24174
+ "formnovalidate",
24175
+ "playsinline",
24176
+ "itemscope",
24177
+ "allowfullscreen"
24178
+ ]);
24179
+ const noStringFalseOnBooleanAttribute = defineRule({
24180
+ id: "no-string-false-on-boolean-attribute",
24181
+ title: "String true/false on a boolean attribute",
24182
+ severity: "warn",
24183
+ recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
24184
+ create: (context) => ({ JSXOpeningElement(node) {
24185
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24186
+ const firstCharacter = node.name.name.charCodeAt(0);
24187
+ if (firstCharacter < 97 || firstCharacter > 122) return;
24188
+ for (const attribute of node.attributes) {
24189
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24190
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
24191
+ if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
24192
+ const value = getJsxPropStringValue(attribute);
24193
+ if (value !== "false" && value !== "true") continue;
24194
+ const attributeName = attribute.name.name;
24195
+ const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
24196
+ context.report({
24197
+ node: attribute,
24198
+ message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
24199
+ });
24200
+ }
24201
+ } })
24202
+ });
24203
+ //#endregion
23739
24204
  //#region src/plugin/rules/react-builtins/no-string-refs.ts
23740
24205
  const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
23741
24206
  const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
@@ -23786,8 +24251,154 @@ const noStringRefs = defineRule({
23786
24251
  }
23787
24252
  });
23788
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
24289
+ //#region src/plugin/rules/js-performance/no-sync-xhr.ts
24290
+ const MESSAGE$14 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
24291
+ const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
24292
+ const noSyncXhr = defineRule({
24293
+ id: "no-sync-xhr",
24294
+ title: "Synchronous XMLHttpRequest",
24295
+ severity: "warn",
24296
+ recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
24297
+ create: (context) => ({ CallExpression(node) {
24298
+ const callee = node.callee;
24299
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
24300
+ if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
24301
+ const asyncArgument = node.arguments?.[2];
24302
+ if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
24303
+ context.report({
24304
+ node,
24305
+ message: MESSAGE$14
24306
+ });
24307
+ } })
24308
+ });
24309
+ //#endregion
24310
+ //#region src/plugin/rules/design/no-tailwind-layout-transition.ts
24311
+ const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
24312
+ const LAYOUT_PROPERTIES = new Set([
24313
+ "width",
24314
+ "height",
24315
+ "min-width",
24316
+ "max-width",
24317
+ "min-height",
24318
+ "max-height",
24319
+ "top",
24320
+ "left",
24321
+ "right",
24322
+ "bottom",
24323
+ "inset",
24324
+ "inset-block",
24325
+ "inset-inline",
24326
+ "margin",
24327
+ "margin-top",
24328
+ "margin-right",
24329
+ "margin-bottom",
24330
+ "margin-left",
24331
+ "margin-block",
24332
+ "margin-inline",
24333
+ "padding",
24334
+ "padding-top",
24335
+ "padding-right",
24336
+ "padding-bottom",
24337
+ "padding-left",
24338
+ "padding-block",
24339
+ "padding-inline"
24340
+ ]);
24341
+ const noTailwindLayoutTransition = defineRule({
24342
+ id: "no-tailwind-layout-transition",
24343
+ title: "Animating a layout property",
24344
+ tags: ["design", "test-noise"],
24345
+ severity: "warn",
24346
+ category: "Performance",
24347
+ recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
24348
+ create: (context) => ({ JSXOpeningElement(node) {
24349
+ const classNameValue = getStringFromClassNameAttr(node);
24350
+ if (!classNameValue) return;
24351
+ for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
24352
+ const animatedProperties = transitionMatch[1];
24353
+ const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
24354
+ if (layoutProperty) context.report({
24355
+ node,
24356
+ message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
24357
+ });
24358
+ }
24359
+ } })
24360
+ });
24361
+ //#endregion
24362
+ //#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
24363
+ const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
24364
+ const targetIsBlank = (attribute) => {
24365
+ const stringValue = getJsxPropStringValue(attribute);
24366
+ if (stringValue !== null) return stringValue === "_blank";
24367
+ const value = attribute.value;
24368
+ if (value && isNodeOfType(value, "JSXExpressionContainer")) {
24369
+ const expression = value.expression;
24370
+ if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
24371
+ }
24372
+ return false;
24373
+ };
24374
+ const noTargetBlankWithoutRel = defineRule({
24375
+ id: "no-target-blank-without-rel",
24376
+ title: "target=_blank without rel=noopener",
24377
+ severity: "warn",
24378
+ recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
24379
+ create: (context) => ({ JSXOpeningElement(node) {
24380
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
24381
+ const tagName = node.name.name;
24382
+ if (tagName !== "a" && tagName !== "area") return;
24383
+ if (hasJsxSpreadAttribute(node.attributes)) return;
24384
+ const targetAttribute = findJsxAttribute(node.attributes, "target");
24385
+ if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
24386
+ const relAttribute = findJsxAttribute(node.attributes, "rel");
24387
+ if (relAttribute) {
24388
+ const relValue = getJsxPropStringValue(relAttribute);
24389
+ if (relValue === null) return;
24390
+ const tokens = relValue.toLowerCase().split(/\s+/);
24391
+ if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
24392
+ }
24393
+ context.report({
24394
+ node: node.name,
24395
+ message: MESSAGE$13
24396
+ });
24397
+ } })
24398
+ });
24399
+ //#endregion
23789
24400
  //#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
23790
- 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`.";
23791
24402
  const isInsideClassMethod = (node, customClassFactoryNames) => {
23792
24403
  let ancestor = node.parent;
23793
24404
  while (ancestor) {
@@ -23856,7 +24467,7 @@ const noThisInSfc = defineRule({
23856
24467
  if (!looksLikeFunctionComponent(enclosingFunction)) return;
23857
24468
  context.report({
23858
24469
  node,
23859
- message: MESSAGE$10
24470
+ message: MESSAGE$12
23860
24471
  });
23861
24472
  } };
23862
24473
  }
@@ -23894,26 +24505,39 @@ const noTinyText = defineRule({
23894
24505
  });
23895
24506
  //#endregion
23896
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`.";
23897
24510
  const noTransitionAll = defineRule({
23898
24511
  id: "no-transition-all",
23899
24512
  title: "transition: all animates everything",
23900
24513
  tags: ["test-noise"],
23901
24514
  severity: "warn",
23902
24515
  recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
23903
- create: (context) => ({ JSXAttribute(node) {
23904
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
23905
- if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
23906
- const expression = node.value.expression;
23907
- if (!isNodeOfType(expression, "ObjectExpression")) return;
23908
- for (const property of expression.properties ?? []) {
23909
- if (!isNodeOfType(property, "Property")) continue;
23910
- if ((isNodeOfType(property.key, "Identifier") ? property.key.name : null) !== "transition") continue;
23911
- if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.startsWith("all")) context.report({
23912
- node: property,
23913
- 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
23914
24538
  });
23915
24539
  }
23916
- } })
24540
+ })
23917
24541
  });
23918
24542
  //#endregion
23919
24543
  //#region src/plugin/rules/correctness/no-uncontrolled-input.ts
@@ -23957,7 +24581,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
23957
24581
  }
23958
24582
  return stateNames;
23959
24583
  };
23960
- const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
23961
24584
  const noUncontrolledInput = defineRule({
23962
24585
  id: "no-uncontrolled-input",
23963
24586
  title: "Uncontrolled input value",
@@ -24061,6 +24684,38 @@ const noUnescapedEntities = defineRule({
24061
24684
  } })
24062
24685
  });
24063
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
24064
24719
  //#region src/plugin/constants/dom-aria-properties.ts
24065
24720
  const ARIA_PROPERTY_NAMES = new Set([
24066
24721
  "activedescendant",
@@ -25532,7 +26187,7 @@ const noWideLetterSpacing = defineRule({
25532
26187
  //#endregion
25533
26188
  //#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
25534
26189
  const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
25535
- 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.";
25536
26191
  const resolveSettings$7 = (settings) => {
25537
26192
  const reactDoctor = settings?.["react-doctor"];
25538
26193
  return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
@@ -25566,7 +26221,7 @@ const noWillUpdateSetState = defineRule({
25566
26221
  if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
25567
26222
  context.report({
25568
26223
  node: node.callee,
25569
- message: MESSAGE$9
26224
+ message: MESSAGE$10
25570
26225
  });
25571
26226
  } };
25572
26227
  }
@@ -26444,7 +27099,7 @@ const preactNoRenderArguments = defineRule({
26444
27099
  });
26445
27100
  //#endregion
26446
27101
  //#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
26447
- 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.";
26448
27103
  const preactPreferOndblclick = defineRule({
26449
27104
  id: "preact-prefer-ondblclick",
26450
27105
  title: "onDoubleClick instead of onDblClick",
@@ -26459,7 +27114,7 @@ const preactPreferOndblclick = defineRule({
26459
27114
  if (!onDoubleClickAttribute) return;
26460
27115
  context.report({
26461
27116
  node: onDoubleClickAttribute,
26462
- message: MESSAGE$8
27117
+ message: MESSAGE$9
26463
27118
  });
26464
27119
  } })
26465
27120
  });
@@ -26499,6 +27154,42 @@ const preactPreferOninput = defineRule({
26499
27154
  } })
26500
27155
  });
26501
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
26502
27193
  //#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
26503
27194
  const preferDynamicImport = defineRule({
26504
27195
  id: "prefer-dynamic-import",
@@ -27090,6 +27781,26 @@ const preferTagOverRole = defineRule({
27090
27781
  } })
27091
27782
  });
27092
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
27093
27804
  //#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
27094
27805
  const collectFunctionTypedLocalBindings = (componentBody) => {
27095
27806
  const functionTypedLocals = /* @__PURE__ */ new Set();
@@ -34895,6 +35606,47 @@ const serverAfterNonblocking = defineRule({
34895
35606
  }
34896
35607
  });
34897
35608
  //#endregion
35609
+ //#region src/plugin/utils/is-auth-guard-name.ts
35610
+ const SIGNED_IN_HEAD_TOKENS = new Set([
35611
+ "signed",
35612
+ "logged",
35613
+ "sign"
35614
+ ]);
35615
+ const mergeSignedInTokens = (tokens) => {
35616
+ const mergedTokens = [];
35617
+ for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
35618
+ const currentToken = tokens[tokenIndex];
35619
+ if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
35620
+ mergedTokens.push(`${currentToken}in`);
35621
+ tokenIndex += 1;
35622
+ continue;
35623
+ }
35624
+ mergedTokens.push(currentToken);
35625
+ }
35626
+ return mergedTokens;
35627
+ };
35628
+ const isAuthGuardName = (calleeName) => {
35629
+ const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
35630
+ if (tokens.length === 0) return false;
35631
+ let hasAssertiveVerb = false;
35632
+ let hasGetterVerb = false;
35633
+ let hasQualifier = false;
35634
+ let hasStrongNoun = false;
35635
+ let hasWeakNoun = false;
35636
+ for (const token of tokens) {
35637
+ if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
35638
+ if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
35639
+ if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
35640
+ if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
35641
+ if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
35642
+ if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
35643
+ }
35644
+ if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
35645
+ if (hasGetterVerb && hasStrongNoun) return true;
35646
+ if (hasQualifier && hasWeakNoun) return true;
35647
+ return false;
35648
+ };
35649
+ //#endregion
34898
35650
  //#region src/plugin/rules/server/server-auth-actions.ts
34899
35651
  const isAsyncFunctionLikeNode = (node) => {
34900
35652
  if (!node) return false;
@@ -34937,9 +35689,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
34937
35689
  const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
34938
35690
  const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
34939
35691
  if (!calleeNode) return null;
34940
- if (isNodeOfType(calleeNode, "Identifier")) return allowedFunctionNames.has(calleeNode.name) ? calleeNode.name : null;
35692
+ if (isNodeOfType(calleeNode, "Identifier")) {
35693
+ const calleeName = calleeNode.name;
35694
+ return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
35695
+ }
34941
35696
  if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
34942
35697
  const methodName = calleeNode.property.name;
35698
+ if (isAuthGuardName(methodName)) return methodName;
34943
35699
  if (!allowedFunctionNames.has(methodName)) return null;
34944
35700
  if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
34945
35701
  return methodName;
@@ -38721,6 +39477,17 @@ const reactDoctorRules = [
38721
39477
  requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
38722
39478
  }
38723
39479
  },
39480
+ {
39481
+ key: "react-doctor/no-arbitrary-px-font-size",
39482
+ id: "no-arbitrary-px-font-size",
39483
+ source: "react-doctor",
39484
+ originallyExternal: false,
39485
+ rule: {
39486
+ ...noArbitraryPxFontSize,
39487
+ framework: "global",
39488
+ category: "Accessibility"
39489
+ }
39490
+ },
38724
39491
  {
38725
39492
  key: "react-doctor/no-aria-hidden-on-focusable",
38726
39493
  id: "no-aria-hidden-on-focusable",
@@ -38780,6 +39547,18 @@ const reactDoctorRules = [
38780
39547
  requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
38781
39548
  }
38782
39549
  },
39550
+ {
39551
+ key: "react-doctor/no-autoplay-without-muted",
39552
+ id: "no-autoplay-without-muted",
39553
+ source: "react-doctor",
39554
+ originallyExternal: false,
39555
+ rule: {
39556
+ ...noAutoplayWithoutMuted,
39557
+ framework: "global",
39558
+ category: "Accessibility",
39559
+ requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
39560
+ }
39561
+ },
38783
39562
  {
38784
39563
  key: "react-doctor/no-barrel-import",
38785
39564
  id: "no-barrel-import",
@@ -38933,6 +39712,17 @@ const reactDoctorRules = [
38933
39712
  category: "Maintainability"
38934
39713
  }
38935
39714
  },
39715
+ {
39716
+ key: "react-doctor/no-deprecated-tailwind-class",
39717
+ id: "no-deprecated-tailwind-class",
39718
+ source: "react-doctor",
39719
+ originallyExternal: false,
39720
+ rule: {
39721
+ ...noDeprecatedTailwindClass,
39722
+ framework: "global",
39723
+ category: "Maintainability"
39724
+ }
39725
+ },
38936
39726
  {
38937
39727
  key: "react-doctor/no-derived-state",
38938
39728
  id: "no-derived-state",
@@ -39052,6 +39842,17 @@ const reactDoctorRules = [
39052
39842
  requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
39053
39843
  }
39054
39844
  },
39845
+ {
39846
+ key: "react-doctor/no-document-write",
39847
+ id: "no-document-write",
39848
+ source: "react-doctor",
39849
+ originallyExternal: false,
39850
+ rule: {
39851
+ ...noDocumentWrite,
39852
+ framework: "global",
39853
+ category: "Performance"
39854
+ }
39855
+ },
39055
39856
  {
39056
39857
  key: "react-doctor/no-dynamic-import-path",
39057
39858
  id: "no-dynamic-import-path",
@@ -39193,6 +39994,17 @@ const reactDoctorRules = [
39193
39994
  category: "Performance"
39194
39995
  }
39195
39996
  },
39997
+ {
39998
+ key: "react-doctor/no-full-viewport-width",
39999
+ id: "no-full-viewport-width",
40000
+ source: "react-doctor",
40001
+ originallyExternal: false,
40002
+ rule: {
40003
+ ...noFullViewportWidth,
40004
+ framework: "global",
40005
+ category: "Maintainability"
40006
+ }
40007
+ },
39196
40008
  {
39197
40009
  key: "react-doctor/no-generic-handler-names",
39198
40010
  id: "no-generic-handler-names",
@@ -39432,6 +40244,17 @@ const reactDoctorRules = [
39432
40244
  category: "Performance"
39433
40245
  }
39434
40246
  },
40247
+ {
40248
+ key: "react-doctor/no-low-contrast-inline-style",
40249
+ id: "no-low-contrast-inline-style",
40250
+ source: "react-doctor",
40251
+ originallyExternal: false,
40252
+ rule: {
40253
+ ...noLowContrastInlineStyle,
40254
+ framework: "global",
40255
+ category: "Accessibility"
40256
+ }
40257
+ },
39435
40258
  {
39436
40259
  key: "react-doctor/no-many-boolean-props",
39437
40260
  id: "no-many-boolean-props",
@@ -39709,6 +40532,17 @@ const reactDoctorRules = [
39709
40532
  category: "Maintainability"
39710
40533
  }
39711
40534
  },
40535
+ {
40536
+ key: "react-doctor/no-redundant-display-class",
40537
+ id: "no-redundant-display-class",
40538
+ source: "react-doctor",
40539
+ originallyExternal: false,
40540
+ rule: {
40541
+ ...noRedundantDisplayClass,
40542
+ framework: "global",
40543
+ category: "Maintainability"
40544
+ }
40545
+ },
39712
40546
  {
39713
40547
  key: "react-doctor/no-redundant-roles",
39714
40548
  id: "no-redundant-roles",
@@ -39861,6 +40695,18 @@ const reactDoctorRules = [
39861
40695
  requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
39862
40696
  }
39863
40697
  },
40698
+ {
40699
+ key: "react-doctor/no-string-false-on-boolean-attribute",
40700
+ id: "no-string-false-on-boolean-attribute",
40701
+ source: "react-doctor",
40702
+ originallyExternal: false,
40703
+ rule: {
40704
+ ...noStringFalseOnBooleanAttribute,
40705
+ framework: "global",
40706
+ category: "Bugs",
40707
+ requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
40708
+ }
40709
+ },
39864
40710
  {
39865
40711
  key: "react-doctor/no-string-refs",
39866
40712
  id: "no-string-refs",
@@ -39873,6 +40719,51 @@ const reactDoctorRules = [
39873
40719
  requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
39874
40720
  }
39875
40721
  },
40722
+ {
40723
+ key: "react-doctor/no-svg-currentcolor-with-fill-class",
40724
+ id: "no-svg-currentcolor-with-fill-class",
40725
+ source: "react-doctor",
40726
+ originallyExternal: false,
40727
+ rule: {
40728
+ ...noSvgCurrentcolorWithFillClass,
40729
+ framework: "global",
40730
+ category: "Maintainability"
40731
+ }
40732
+ },
40733
+ {
40734
+ key: "react-doctor/no-sync-xhr",
40735
+ id: "no-sync-xhr",
40736
+ source: "react-doctor",
40737
+ originallyExternal: false,
40738
+ rule: {
40739
+ ...noSyncXhr,
40740
+ framework: "global",
40741
+ category: "Performance"
40742
+ }
40743
+ },
40744
+ {
40745
+ key: "react-doctor/no-tailwind-layout-transition",
40746
+ id: "no-tailwind-layout-transition",
40747
+ source: "react-doctor",
40748
+ originallyExternal: false,
40749
+ rule: {
40750
+ ...noTailwindLayoutTransition,
40751
+ framework: "global",
40752
+ category: "Performance"
40753
+ }
40754
+ },
40755
+ {
40756
+ key: "react-doctor/no-target-blank-without-rel",
40757
+ id: "no-target-blank-without-rel",
40758
+ source: "react-doctor",
40759
+ originallyExternal: false,
40760
+ rule: {
40761
+ ...noTargetBlankWithoutRel,
40762
+ framework: "global",
40763
+ category: "Accessibility",
40764
+ requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
40765
+ }
40766
+ },
39876
40767
  {
39877
40768
  key: "react-doctor/no-this-in-sfc",
39878
40769
  id: "no-this-in-sfc",
@@ -39942,6 +40833,18 @@ const reactDoctorRules = [
39942
40833
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
39943
40834
  }
39944
40835
  },
40836
+ {
40837
+ key: "react-doctor/no-uninformative-aria-label",
40838
+ id: "no-uninformative-aria-label",
40839
+ source: "react-doctor",
40840
+ originallyExternal: false,
40841
+ rule: {
40842
+ ...noUninformativeAriaLabel,
40843
+ framework: "global",
40844
+ category: "Accessibility",
40845
+ requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
40846
+ }
40847
+ },
39945
40848
  {
39946
40849
  key: "react-doctor/no-unknown-property",
39947
40850
  id: "no-unknown-property",
@@ -40151,6 +41054,17 @@ const reactDoctorRules = [
40151
41054
  category: "Bugs"
40152
41055
  }
40153
41056
  },
41057
+ {
41058
+ key: "react-doctor/prefer-dvh-over-vh",
41059
+ id: "prefer-dvh-over-vh",
41060
+ source: "react-doctor",
41061
+ originallyExternal: false,
41062
+ rule: {
41063
+ ...preferDvhOverVh,
41064
+ framework: "global",
41065
+ category: "Maintainability"
41066
+ }
41067
+ },
40154
41068
  {
40155
41069
  key: "react-doctor/prefer-dynamic-import",
40156
41070
  id: "prefer-dynamic-import",
@@ -40255,6 +41169,17 @@ const reactDoctorRules = [
40255
41169
  requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
40256
41170
  }
40257
41171
  },
41172
+ {
41173
+ key: "react-doctor/prefer-truncate-shorthand",
41174
+ id: "prefer-truncate-shorthand",
41175
+ source: "react-doctor",
41176
+ originallyExternal: false,
41177
+ rule: {
41178
+ ...preferTruncateShorthand,
41179
+ framework: "global",
41180
+ category: "Maintainability"
41181
+ }
41182
+ },
40258
41183
  {
40259
41184
  key: "react-doctor/prefer-use-effect-event",
40260
41185
  id: "prefer-use-effect-event",